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]

Reply via email to