This is an automated email from the ASF dual-hosted git repository.
frankgh pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-sidecar.git
The following commit(s) were added to refs/heads/trunk by this push:
new b3b41225 CASSSIDECAR-475: Restore job fails with "Keyspace does not
exist" for keyspaces created with quoted mixed-case names (#365)
b3b41225 is described below
commit b3b412257629b53621136c8bc0cda33a35aefd93
Author: bianca-stanciu29 <[email protected]>
AuthorDate: Tue Jul 14 18:36:47 2026 +0300
CASSSIDECAR-475: Restore job fails with "Keyspace does not exist" for
keyspaces created with quoted mixed-case names (#365)
Patch by Bianca Stanciu; reviewed by Shailaja Koppu, Yifan Cai, Francisco
Guerrero for CASSSIDECAR-475
---
CHANGES.txt | 1 +
.../RestoreJobDiscovererPhaseSignalIntTest.java | 2 +-
.../cluster/locator/CachedLocalTokenRanges.java | 7 +-
.../CassandraClientTokenRingProvider.java | 8 +-
.../validations/ValidateTableExistenceHandler.java | 12 +-
.../cassandra/sidecar/utils/MetadataUtils.java | 53 +++++++-
.../CassandraClientTokenRingProviderTest.java | 99 +++++++++++++++
.../locator/CachedLocalTokenRangesTest.java | 119 +++++++++++++++++
.../cassandra/sidecar/utils/MetadataUtilsTest.java | 141 +++++++++++++++++++++
9 files changed, 429 insertions(+), 13 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 265a4a99..eda42dee 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -9,6 +9,7 @@
0.4.0
-----
+ * Fix restore job failing with "Keyspace does not exist" for quoted
mixed-case keyspace names (CASSSIDECAR-475)
* Split RestoreJobDiscoverer into a fast status-check loop and a slow
slice-discovery loop (CASSSIDECAR-462)
* Ability to load Cassandra connection secrets from filesystem
(CASSSIDECAR-407)
* Sidecar should wake up immediately on the instance receiving a phase signal
instead of waiting for the discovery loop (CASSSIDECAR-454)
diff --git
a/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/restore/RestoreJobDiscovererPhaseSignalIntTest.java
b/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/restore/RestoreJobDiscovererPhaseSignalIntTest.java
index c335bc68..25f2c2ab 100644
---
a/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/restore/RestoreJobDiscovererPhaseSignalIntTest.java
+++
b/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/restore/RestoreJobDiscovererPhaseSignalIntTest.java
@@ -59,7 +59,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
class RestoreJobDiscovererPhaseSignalIntTest extends
SharedClusterSidecarIntegrationTestBase
{
- private static final QualifiedName USER_KEYSPACE_TABLE = new
QualifiedName("restore_phase_signal_ks", "t");
+ private static final QualifiedName USER_KEYSPACE_TABLE = new
QualifiedName("Restore_phase_signal_ks", "T", true, true);
private static final QualifiedTableName SIDECAR_QUALIFIED_TABLE =
new QualifiedTableName(USER_KEYSPACE_TABLE.keyspace(),
USER_KEYSPACE_TABLE.table());
diff --git
a/server/src/main/java/org/apache/cassandra/sidecar/cluster/locator/CachedLocalTokenRanges.java
b/server/src/main/java/org/apache/cassandra/sidecar/cluster/locator/CachedLocalTokenRanges.java
index d22767c6..c8c21cb7 100644
---
a/server/src/main/java/org/apache/cassandra/sidecar/cluster/locator/CachedLocalTokenRanges.java
+++
b/server/src/main/java/org/apache/cassandra/sidecar/cluster/locator/CachedLocalTokenRanges.java
@@ -46,6 +46,7 @@ import
org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
import org.apache.cassandra.sidecar.common.server.cluster.locator.TokenRange;
import org.apache.cassandra.sidecar.common.server.dns.DnsResolver;
import org.apache.cassandra.sidecar.exceptions.CassandraUnavailableException;
+import org.apache.cassandra.sidecar.utils.MetadataUtils;
import org.jetbrains.annotations.NotNull;
/**
@@ -104,7 +105,7 @@ public class CachedLocalTokenRanges implements
LocalTokenRangesProvider
return Collections.emptyMap();
}
- if (metadata.getKeyspace(keyspace) == null)
+ if (MetadataUtils.keyspace(metadata, keyspace) == null)
{
throw new NoSuchElementException("Keyspace does not exist.
keyspace: " + keyspace);
}
@@ -147,7 +148,9 @@ public class CachedLocalTokenRanges implements
LocalTokenRangesProvider
public Set<TokenRange> tokenRangesOfHost(Metadata metadata, String
keyspace, Host host)
{
- return metadata.getTokenRanges(keyspace, host)
+ KeyspaceMetadata ks = MetadataUtils.keyspace(metadata, keyspace);
+ String resolvedName = ks != null ?
Metadata.quoteIfNecessary(ks.getName()) : keyspace;
+ return metadata.getTokenRanges(resolvedName, host)
.stream()
.flatMap(range -> TokenRange.from(range).stream())
.collect(Collectors.toSet());
diff --git
a/server/src/main/java/org/apache/cassandra/sidecar/coordination/CassandraClientTokenRingProvider.java
b/server/src/main/java/org/apache/cassandra/sidecar/coordination/CassandraClientTokenRingProvider.java
index 7c56ddc3..189f170b 100644
---
a/server/src/main/java/org/apache/cassandra/sidecar/coordination/CassandraClientTokenRingProvider.java
+++
b/server/src/main/java/org/apache/cassandra/sidecar/coordination/CassandraClientTokenRingProvider.java
@@ -55,6 +55,7 @@ import
org.apache.cassandra.sidecar.common.server.cluster.locator.Partitioners;
import org.apache.cassandra.sidecar.common.server.cluster.locator.TokenRange;
import org.apache.cassandra.sidecar.common.server.dns.DnsResolver;
import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
+import org.apache.cassandra.sidecar.utils.MetadataUtils;
/**
@@ -83,11 +84,12 @@ public class CassandraClientTokenRingProvider extends
TokenRingProvider implemen
{
checkAndReloadReloadCaches();
Metadata metadata =
instancesMetadata.instances().get(0).delegate().metadata();
- if (keyspace == null || metadata.getKeyspace(keyspace) == null)
+ KeyspaceMetadata ksMetadata = MetadataUtils.keyspace(metadata,
keyspace);
+ if (ksMetadata == null)
{
throw new NoSuchElementException("Keyspace does not exist.
keyspace: " + keyspace);
}
- return perKeySpaceTokenRangesOfAllInstances(metadata).get(keyspace)
+ return
perKeySpaceTokenRangesOfAllInstances(metadata).get(ksMetadata.getName())
.entrySet()
.stream()
.filter(entry ->
localHostsCache.containsKey(entry.getKey()))
@@ -252,7 +254,7 @@ public class CassandraClientTokenRingProvider extends
TokenRingProvider implemen
Map<Host, Set<TokenRange>> perHostTokenRanges = new HashMap<>();
for (Host host : metadata.getAllHosts())
{
- Set<TokenRange> tokenRanges =
metadata.getTokenRanges(ks.getName(), host)
+ Set<TokenRange> tokenRanges =
metadata.getTokenRanges(Metadata.quoteIfNecessary(ks.getName()), host)
.stream()
.flatMap(range ->
TokenRange.from(range).stream())
.collect(Collectors.toSet());
diff --git
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/validations/ValidateTableExistenceHandler.java
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/validations/ValidateTableExistenceHandler.java
index 136f2770..f18964ed 100644
---
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/validations/ValidateTableExistenceHandler.java
+++
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/validations/ValidateTableExistenceHandler.java
@@ -19,6 +19,7 @@
package org.apache.cassandra.sidecar.handlers.validations;
import com.datastax.driver.core.KeyspaceMetadata;
+import com.datastax.driver.core.Metadata;
import com.datastax.driver.core.TableMetadata;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -35,6 +36,7 @@ import org.apache.cassandra.sidecar.handlers.AbstractHandler;
import org.apache.cassandra.sidecar.routes.RoutingContextUtils;
import org.apache.cassandra.sidecar.utils.CassandraInputValidator;
import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
+import org.apache.cassandra.sidecar.utils.MetadataUtils;
import org.jetbrains.annotations.NotNull;
import static
org.apache.cassandra.sidecar.utils.HttpExceptions.wrapHttpException;
@@ -106,7 +108,7 @@ public class ValidateTableExistenceHandler extends
AbstractHandler<QualifiedTabl
return;
}
- TableMetadata tableMetadata = keyspaceMetadata.getTable(table);
+ TableMetadata tableMetadata =
MetadataUtils.table(keyspaceMetadata, table);
boolean tableExists = tableMetadata != null || unparseableSchema
!= null;
if (!tableExists)
{
@@ -128,10 +130,10 @@ public class ValidateTableExistenceHandler extends
AbstractHandler<QualifiedTabl
private Future<KeyspaceMetadata> getKeyspaceMetadata(String host, String
keyspace)
{
- return executorPools.service().executeBlocking(() ->
metadataFetcher.instance(host)
-
.delegate()
-
.metadata()
-
.getKeyspace(keyspace));
+ return executorPools.service().executeBlocking(() -> {
+ Metadata metadata =
metadataFetcher.instance(host).delegate().metadata();
+ return MetadataUtils.keyspace(metadata, keyspace);
+ });
}
private Future<String> unsupportedSchema(String keyspace, String table)
diff --git
a/server/src/main/java/org/apache/cassandra/sidecar/utils/MetadataUtils.java
b/server/src/main/java/org/apache/cassandra/sidecar/utils/MetadataUtils.java
index fa172368..40f88791 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/utils/MetadataUtils.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/utils/MetadataUtils.java
@@ -30,6 +30,10 @@ public class MetadataUtils
{
/**
* Returns the metadata of a keyspace given its name.
+ * Delegates to {@link #keyspace(Metadata, String)} using the unquoted
name. CQL quote context
+ * from the source {@link Name} is intentionally discarded: the two-step
lookup in
+ * {@link #keyspace(Metadata, String)} handles both unquoted (driver
case-folds) and CQL-quoted
+ * mixed-case keyspaces via its {@code quoteIfNecessary} fallback.
*
* @param metadata the metadata object.
* @param keyspace the name of the keyspace for which metadata should be
returned.
@@ -39,11 +43,35 @@ public class MetadataUtils
*/
public static KeyspaceMetadata keyspace(Metadata metadata, Name keyspace)
{
- return metadata.getKeyspace(keyspace.maybeQuotedName());
+ return keyspace(metadata, keyspace.name());
+ }
+
+ /**
+ * Returns the metadata of a keyspace given its name as stored in sidecar
(without CQL quote context).
+ * Tries a raw lookup first (driver folds to lowercase, handles unquoted
names), then falls back to a
+ * quoted lookup to handle mixed-case keyspaces created with CQL
double-quotes.
+ *
+ * @param metadata the metadata object.
+ * @param keyspace the keyspace name as stored in sidecar.
+ * @return the metadata of the requested keyspace or {@code null} if not
found.
+ */
+ public static KeyspaceMetadata keyspace(Metadata metadata, String keyspace)
+ {
+ if (keyspace == null)
+ return null;
+ KeyspaceMetadata ks = metadata.getKeyspace(keyspace);
+ if (ks == null)
+ {
+ ks = metadata.getKeyspace(Metadata.quoteIfNecessary(keyspace));
+ }
+ return ks;
}
/**
* Returns the metadata for a table contained in this keyspace.
+ * Delegates to {@link #table(KeyspaceMetadata, String)} using the
unquoted name so that mixed-case
+ * tables stored without CQL double-quotes (e.g. from URL path parameters)
are found via the
+ * quoted fallback in that overload.
*
* @param metadata the metadata object.
* @param table the name of table to retrieve
@@ -52,6 +80,27 @@ public class MetadataUtils
*/
public static TableMetadata table(KeyspaceMetadata metadata, Name table)
{
- return metadata.getTable(table.maybeQuotedName());
+ return table(metadata, table.name());
+ }
+
+ /**
+ * Returns the metadata for a table contained in this keyspace given its
name as stored in sidecar.
+ * Tries a raw lookup first, then falls back to a quoted lookup to handle
mixed-case tables created
+ * with CQL double-quotes.
+ *
+ * @param metadata the metadata object.
+ * @param table the table name as stored in sidecar.
+ * @return the metadata for the table or {@code null} if not found.
+ */
+ public static TableMetadata table(KeyspaceMetadata metadata, String table)
+ {
+ if (table == null)
+ return null;
+ TableMetadata tm = metadata.getTable(table);
+ if (tm == null)
+ {
+ tm = metadata.getTable(Metadata.quoteIfNecessary(table));
+ }
+ return tm;
}
}
diff --git
a/server/src/test/java/org/apache/cassandra/sidecar/cluster/CassandraClientTokenRingProviderTest.java
b/server/src/test/java/org/apache/cassandra/sidecar/cluster/CassandraClientTokenRingProviderTest.java
index 789bd7a1..9c540b02 100644
---
a/server/src/test/java/org/apache/cassandra/sidecar/cluster/CassandraClientTokenRingProviderTest.java
+++
b/server/src/test/java/org/apache/cassandra/sidecar/cluster/CassandraClientTokenRingProviderTest.java
@@ -25,10 +25,12 @@ import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.Collection;
+import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
@@ -40,6 +42,7 @@ import org.junit.jupiter.api.Test;
import com.datastax.driver.core.DataType;
import com.datastax.driver.core.Host;
+import com.datastax.driver.core.KeyspaceMetadata;
import com.datastax.driver.core.Metadata;
import com.datastax.driver.core.ProtocolVersion;
import com.datastax.driver.core.Token;
@@ -58,11 +61,15 @@ import org.jetbrains.annotations.NotNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -346,6 +353,98 @@ public class CassandraClientTokenRingProviderTest
return instanceMetadata;
}
+ @Test
+ void testLocalTokenRanges_succeedsForMixedCaseKeyspace()
+ {
+ String keyspace = "MyKeyspace";
+ Metadata metadata = getMetadataWithKeyspace(keyspace);
+ CassandraClientTokenRingProvider provider = new
CassandraClientTokenRingProvider(
+ mockInstancesMetadataWithMetadata(metadata),
+ mockInstanceMetadataFetcherWithMetadata(metadata),
+ mockDnsResolver());
+
+ Map<Integer, Set<TokenRange>> result =
provider.localTokenRanges(keyspace);
+
+ assertNotNull(result);
+ verify(metadata).getKeyspace(Metadata.quoteIfNecessary(keyspace));
+ }
+
+ @Test
+ void testLocalTokenRanges_throwsForNonExistentKeyspace()
+ {
+ Metadata metadata = getMetadata();
+
when(metadata.getKeyspace(Metadata.quoteIfNecessary("nonexistent"))).thenReturn(null);
+ CassandraClientTokenRingProvider provider = new
CassandraClientTokenRingProvider(
+ mockInstancesMetadataWithMetadata(metadata),
+ mockInstanceMetadataFetcherWithMetadata(metadata),
+ mockDnsResolver());
+
+ assertThatThrownBy(() -> provider.localTokenRanges("nonexistent"))
+ .isInstanceOf(NoSuchElementException.class)
+ .hasMessageContaining("Keyspace does not exist. keyspace:
nonexistent");
+ }
+
+ @Test
+ void testLocalTokenRanges_succeedsForUnquotedMixedCaseKeyspace()
+ {
+ // Regression: MyKeyspace created without CQL quotes (Cassandra
internal name: mykeyspace).
+ // Sidecar stores MyKeyspace. The raw lookup simulates the driver's
case-folding and returns
+ // the keyspace; the quoteIfNecessary fallback must not be reached,
because quoteIfNecessary
+ // would look for case-sensitive MyKeyspace (not mykeyspace) and
return null.
+ String storedKeyspace = "MyKeyspace";
+ Metadata metadata = getMetadataWithUnquotedKeyspace(storedKeyspace,
"mykeyspace");
+ CassandraClientTokenRingProvider provider = new
CassandraClientTokenRingProvider(
+ mockInstancesMetadataWithMetadata(metadata),
+ mockInstanceMetadataFetcherWithMetadata(metadata),
+ mockDnsResolver());
+
+ Map<Integer, Set<TokenRange>> result =
provider.localTokenRanges(storedKeyspace);
+
+ assertNotNull(result);
+ verify(metadata,
never()).getKeyspace(Metadata.quoteIfNecessary(storedKeyspace));
+ }
+
+ private static Metadata getMetadataWithKeyspace(String keyspace)
+ {
+ Metadata metadata = getMetadata();
+ KeyspaceMetadata keyspaceMetadata = mock(KeyspaceMetadata.class);
+ when(keyspaceMetadata.getName()).thenReturn(keyspace);
+
when(metadata.getKeyspace(Metadata.quoteIfNecessary(keyspace))).thenReturn(keyspaceMetadata);
+ when(metadata.getKeyspaces()).thenReturn(List.of(keyspaceMetadata));
+ when(metadata.getTokenRanges(any(),
any())).thenReturn(Collections.emptySet());
+ return metadata;
+ }
+
+ // storedName: what sidecar has stored (e.g. "MyKeyspace")
+ // internalName: Cassandra's internal name after case-folding (e.g.
"mykeyspace")
+ private static Metadata getMetadataWithUnquotedKeyspace(String storedName,
String internalName)
+ {
+ Metadata metadata = getMetadata();
+ KeyspaceMetadata keyspaceMetadata = mock(KeyspaceMetadata.class);
+ when(keyspaceMetadata.getName()).thenReturn(internalName);
+ when(metadata.getKeyspace(storedName)).thenReturn(keyspaceMetadata);
+ when(metadata.getKeyspaces()).thenReturn(List.of(keyspaceMetadata));
+ when(metadata.getTokenRanges(any(),
any())).thenReturn(Collections.emptySet());
+ return metadata;
+ }
+
+ private InstancesMetadata mockInstancesMetadataWithMetadata(Metadata
metadata)
+ {
+ InstancesMetadata instancesMetadata = mock(InstancesMetadata.class);
+ InstanceMetadata instance1 = getMockInstanceMetaData(101000101,
"localhost", metadata);
+ InstanceMetadata instance2 = getMockInstanceMetaData(101000201,
"localhost2", metadata);
+ InstanceMetadata instance3 = getMockInstanceMetaData(101000301,
"localhost3", metadata);
+ when(instancesMetadata.instances()).thenReturn(List.of(instance1,
instance2, instance3));
+ return instancesMetadata;
+ }
+
+ private InstanceMetadataFetcher
mockInstanceMetadataFetcherWithMetadata(Metadata metadata)
+ {
+ InstanceMetadataFetcher fetcher = mock(InstanceMetadataFetcher.class);
+ when(fetcher.callOnFirstAvailableInstance(any())).thenReturn(metadata);
+ return fetcher;
+ }
+
private InstanceMetadataFetcher mockInstanceMetadataFetcher()
{
InstanceMetadataFetcher fetcher = mock(InstanceMetadataFetcher.class);
diff --git
a/server/src/test/java/org/apache/cassandra/sidecar/cluster/locator/CachedLocalTokenRangesTest.java
b/server/src/test/java/org/apache/cassandra/sidecar/cluster/locator/CachedLocalTokenRangesTest.java
new file mode 100644
index 00000000..2c617fbc
--- /dev/null
+++
b/server/src/test/java/org/apache/cassandra/sidecar/cluster/locator/CachedLocalTokenRangesTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.cassandra.sidecar.cluster.locator;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.datastax.driver.core.KeyspaceMetadata;
+import com.datastax.driver.core.Metadata;
+
+import org.apache.cassandra.sidecar.cluster.InstancesMetadata;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.common.server.cluster.locator.TokenRange;
+import org.apache.cassandra.sidecar.common.server.dns.DnsResolver;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for {@link CachedLocalTokenRanges}
+ */
+public class CachedLocalTokenRangesTest
+{
+ private Metadata metadata;
+ private CachedLocalTokenRanges cachedLocalTokenRanges;
+
+ @BeforeEach
+ void setup()
+ {
+ metadata = mock(Metadata.class);
+ InstancesMetadata instancesMetadata = mock(InstancesMetadata.class);
+ DnsResolver dnsResolver = mock(DnsResolver.class);
+
+ InstanceMetadata instance = mock(InstanceMetadata.class,
RETURNS_DEEP_STUBS);
+ when(instance.delegate().metadata()).thenReturn(metadata);
+ when(instancesMetadata.instances()).thenReturn(List.of(instance));
+
+ cachedLocalTokenRanges = new CachedLocalTokenRanges(instancesMetadata,
dnsResolver);
+ }
+
+ @Test
+ void testLocalTokenRanges_succeedsForMixedCaseKeyspace()
+ {
+ // Keyspaces created as quoted CQL identifiers (e.g. CREATE KEYSPACE
"MyKeyspace")
+ // must be looked up with Metadata.quoteIfNecessary() to trigger a
case-sensitive
+ // lookup in the DataStax Java Driver. Without it, the driver
normalizes the name
+ // to lowercase and fails to find the keyspace.
+ String keyspace = "MyKeyspace";
+ KeyspaceMetadata keyspaceMetadata = mock(KeyspaceMetadata.class);
+ when(keyspaceMetadata.getName()).thenReturn(keyspace);
+
when(metadata.getKeyspace(Metadata.quoteIfNecessary(keyspace))).thenReturn(keyspaceMetadata);
+ when(metadata.getAllHosts()).thenReturn(Collections.emptySet());
+ when(metadata.getKeyspaces()).thenReturn(Collections.emptyList());
+
+ Map<Integer, Set<TokenRange>> result =
cachedLocalTokenRanges.localTokenRanges(keyspace);
+
+ assertNotNull(result);
+ // Verify the driver was called with the quoted form, not the raw
mixed-case string
+ verify(metadata).getKeyspace(Metadata.quoteIfNecessary(keyspace));
+ }
+
+ @Test
+ void testLocalTokenRanges_succeedsForUnquotedMixedCaseKeyspace()
+ {
+ // Regression: MyKeyspace created without CQL quotes (Cassandra
internal name: mykeyspace).
+ // Sidecar stores MyKeyspace. The raw lookup simulates the driver's
case-folding and returns
+ // the keyspace; the quoteIfNecessary fallback must not be reached,
because quoteIfNecessary
+ // would look for case-sensitive MyKeyspace (not mykeyspace) and
return null.
+ String keyspace = "MyKeyspace";
+ KeyspaceMetadata keyspaceMetadata = mock(KeyspaceMetadata.class);
+ when(keyspaceMetadata.getName()).thenReturn("mykeyspace");
+ when(metadata.getKeyspace(keyspace)).thenReturn(keyspaceMetadata);
+ when(metadata.getAllHosts()).thenReturn(Collections.emptySet());
+ when(metadata.getKeyspaces()).thenReturn(Collections.emptyList());
+
+ Map<Integer, Set<TokenRange>> result =
cachedLocalTokenRanges.localTokenRanges(keyspace);
+
+ assertNotNull(result);
+ verify(metadata,
never()).getKeyspace(Metadata.quoteIfNecessary(keyspace));
+ }
+
+ @Test
+ void testLocalTokenRanges_throwsWhenKeyspaceDoesNotExist()
+ {
+
when(metadata.getKeyspace(Metadata.quoteIfNecessary("nonexistent"))).thenReturn(null);
+ when(metadata.getAllHosts()).thenReturn(Collections.emptySet());
+
+ assertThatThrownBy(() ->
cachedLocalTokenRanges.localTokenRanges("nonexistent"))
+ .isInstanceOf(NoSuchElementException.class)
+ .hasMessageContaining("Keyspace does not exist. keyspace:
nonexistent");
+ }
+}
diff --git
a/server/src/test/java/org/apache/cassandra/sidecar/utils/MetadataUtilsTest.java
b/server/src/test/java/org/apache/cassandra/sidecar/utils/MetadataUtilsTest.java
new file mode 100644
index 00000000..c5f3805d
--- /dev/null
+++
b/server/src/test/java/org/apache/cassandra/sidecar/utils/MetadataUtilsTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.cassandra.sidecar.utils;
+
+import org.junit.jupiter.api.Test;
+
+import com.datastax.driver.core.KeyspaceMetadata;
+import com.datastax.driver.core.Metadata;
+import com.datastax.driver.core.TableMetadata;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for {@link MetadataUtils#keyspace(Metadata, String)} and {@link
MetadataUtils#table(KeyspaceMetadata, String)}
+ */
+public class MetadataUtilsTest
+{
+ @Test
+ void keyspace_quotedMixedCase_findsViaQuotedFallback()
+ {
+ // "MyKeyspace" created with CQL quotes: Cassandra internal name is
MyKeyspace (case-preserved).
+ // Sidecar stores MyKeyspace (no quote chars). Raw lookup folds to
mykeyspace → not found.
+ // Quoted fallback finds the case-sensitive entry.
+ Metadata metadata = mock(Metadata.class);
+ KeyspaceMetadata keyspaceMetadata = mock(KeyspaceMetadata.class);
+ String keyspace = "MyKeyspace";
+ when(metadata.getKeyspace(keyspace)).thenReturn(null);
+
when(metadata.getKeyspace(Metadata.quoteIfNecessary(keyspace))).thenReturn(keyspaceMetadata);
+
+ assertThat(MetadataUtils.keyspace(metadata,
keyspace)).isSameAs(keyspaceMetadata);
+ verify(metadata).getKeyspace(Metadata.quoteIfNecessary(keyspace));
+ }
+
+ @Test
+ void keyspace_unquotedMixedCase_findsViaRawLookupWithoutFallback()
+ {
+ // MyKeyspace created without CQL quotes: Cassandra folds to
mykeyspace internally.
+ // Sidecar stores MyKeyspace. The raw lookup succeeds (driver handles
case-folding);
+ // the quoted fallback must NOT be reached — that would look for
case-sensitive MyKeyspace
+ // and return null, which was the regression introduced by
quoteIfNecessary.
+ Metadata metadata = mock(Metadata.class);
+ KeyspaceMetadata keyspaceMetadata = mock(KeyspaceMetadata.class);
+ when(keyspaceMetadata.getName()).thenReturn("mykeyspace");
+ String keyspace = "MyKeyspace";
+ when(metadata.getKeyspace(keyspace)).thenReturn(keyspaceMetadata);
+
+ assertThat(MetadataUtils.keyspace(metadata,
keyspace)).isSameAs(keyspaceMetadata);
+ verify(metadata,
never()).getKeyspace(Metadata.quoteIfNecessary(keyspace));
+ }
+
+ @Test
+ void keyspace_lowercase_findsViaRawLookup()
+ {
+ Metadata metadata = mock(Metadata.class);
+ KeyspaceMetadata keyspaceMetadata = mock(KeyspaceMetadata.class);
+ when(metadata.getKeyspace("myks")).thenReturn(keyspaceMetadata);
+
+ assertThat(MetadataUtils.keyspace(metadata,
"myks")).isSameAs(keyspaceMetadata);
+ }
+
+ @Test
+ void keyspace_nonExistent_returnsNull()
+ {
+ Metadata metadata = mock(Metadata.class);
+ when(metadata.getKeyspace("nonexistent")).thenReturn(null);
+
when(metadata.getKeyspace(Metadata.quoteIfNecessary("nonexistent"))).thenReturn(null);
+
+ assertThat(MetadataUtils.keyspace(metadata, "nonexistent")).isNull();
+ }
+
+ @Test
+ void table_quotedMixedCase_findsViaQuotedFallback()
+ {
+ // "MyTable" created with CQL quotes: Cassandra internal name is
MyTable (case-preserved).
+ // Sidecar stores MyTable (no quote chars). Raw lookup folds to
mytable → not found.
+ // Quoted fallback finds the case-sensitive entry.
+ KeyspaceMetadata keyspaceMetadata = mock(KeyspaceMetadata.class);
+ TableMetadata tableMetadata = mock(TableMetadata.class);
+ String table = "MyTable";
+ when(keyspaceMetadata.getTable(table)).thenReturn(null);
+
when(keyspaceMetadata.getTable(Metadata.quoteIfNecessary(table))).thenReturn(tableMetadata);
+
+ assertThat(MetadataUtils.table(keyspaceMetadata,
table)).isSameAs(tableMetadata);
+ verify(keyspaceMetadata).getTable(Metadata.quoteIfNecessary(table));
+ }
+
+ @Test
+ void table_unquotedMixedCase_findsViaRawLookupWithoutFallback()
+ {
+ // MyTable created without CQL quotes: Cassandra folds to mytable
internally.
+ // Sidecar stores MyTable. The raw lookup succeeds (driver handles
case-folding);
+ // the quoted fallback must NOT be reached.
+ KeyspaceMetadata keyspaceMetadata = mock(KeyspaceMetadata.class);
+ TableMetadata tableMetadata = mock(TableMetadata.class);
+ String table = "MyTable";
+ when(keyspaceMetadata.getTable(table)).thenReturn(tableMetadata);
+
+ assertThat(MetadataUtils.table(keyspaceMetadata,
table)).isSameAs(tableMetadata);
+ verify(keyspaceMetadata,
never()).getTable(Metadata.quoteIfNecessary(table));
+ }
+
+ @Test
+ void table_lowercase_findsViaRawLookup()
+ {
+ KeyspaceMetadata keyspaceMetadata = mock(KeyspaceMetadata.class);
+ TableMetadata tableMetadata = mock(TableMetadata.class);
+ when(keyspaceMetadata.getTable("mytable")).thenReturn(tableMetadata);
+
+ assertThat(MetadataUtils.table(keyspaceMetadata,
"mytable")).isSameAs(tableMetadata);
+ }
+
+ @Test
+ void table_nonExistent_returnsNull()
+ {
+ KeyspaceMetadata keyspaceMetadata = mock(KeyspaceMetadata.class);
+ when(keyspaceMetadata.getTable("nonexistent")).thenReturn(null);
+
when(keyspaceMetadata.getTable(Metadata.quoteIfNecessary("nonexistent"))).thenReturn(null);
+
+ assertThat(MetadataUtils.table(keyspaceMetadata,
"nonexistent")).isNull();
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]