This is an automated email from the ASF dual-hosted git repository.

sarankk 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 99476afc4 CASSSIDECAR-505: Use describe statement to fetch schema 
instead of Java driver (#390)
99476afc4 is described below

commit 99476afc4dd3bec0f20d264febbed00e5b155f27
Author: Saranya Krishnakumar <[email protected]>
AuthorDate: Mon Oct 26 15:16:03 2026 -0700

    CASSSIDECAR-505: Use describe statement to fetch schema instead of Java 
driver (#390)
    
    Patch by Saranya Krishnakumar; Reviewed by Mansi Khara, Jyothsna Konisa, 
Yifan Cai for CASSSIDECAR-505
---
 CHANGES.txt                                        |   1 +
 docs/src/user.adoc                                 |   2 +
 .../CassandraSchemaRouteIntegrationTest.java       |  55 +++
 .../sidecar/config/DriverConfiguration.java        |  10 +-
 .../config/yaml/DriverConfigurationImpl.java       |  40 ++-
 .../sidecar/db/DriverUnsupportedSchemaCache.java   | 293 ----------------
 .../apache/cassandra/sidecar/db/SchemaCache.java   | 379 +++++++++++++++++++++
 .../sidecar/handlers/KeyspaceSchemaHandler.java    | 120 +++----
 .../cassandra/sidecar/handlers/SchemaHandler.java  |   8 +-
 .../sstableuploads/SSTableUploadHandler.java       |  12 +-
 .../validations/ValidateTableExistenceHandler.java |  14 +-
 .../sidecar/modules/CassandraOperationsModule.java |  12 +-
 .../cassandra/sidecar/modules/CdcModule.java       |   6 +-
 .../sidecar/modules/ConfigurationModule.java       |  14 +-
 .../modules/multibindings/PeriodicTaskMapKeys.java |   2 +-
 .../tasks/CassandraClusterSchemaMonitor.java       |  11 +-
 .../sidecar/testing/IntegrationTestModule.java     |   2 +-
 .../org/apache/cassandra/sidecar/TestModule.java   |   9 +-
 .../sidecar/db/CQLSchemaAccessorTest.java          |  58 +++-
 .../db/DriverUnsupportedSchemaCacheTest.java       | 128 -------
 .../cassandra/sidecar/db/SchemaCacheTest.java      | 361 ++++++++++++++++++++
 .../sidecar/handlers/SchemaHandlerTest.java        |  57 +++-
 .../tasks/CassandraClusterSchemaMonitorTest.java   |  23 +-
 23 files changed, 1051 insertions(+), 566 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index bebe79cd7..9987d8b5f 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,6 @@
 0.5.0
 -----
+ * Use describe statement to fetch schema instead of Java driver 
(CASSSIDECAR-505)
  * Make the CDC CassandraBridge injectable (CASSSIDECAR-511)
  * Introduce server-side configuration to enforce allowed credential modes for 
restore jobs (CASSSIDECAR-501)
  * Sanitize/Nullify Cloud Credentials on RestoreJob Completion 
(CASSSIDECAR-500)
diff --git a/docs/src/user.adoc b/docs/src/user.adoc
index f90f6d0e7..50b095631 100644
--- a/docs/src/user.adoc
+++ b/docs/src/user.adoc
@@ -353,6 +353,8 @@ The `driver_parameters` section of the `sidecar.yaml` file 
is used to configure
 * `ssl`: This defines how the Cassandra driver can connect to Cassandra over 
SSL. See the above <<ssl, SSL/TLS Configuration>> section for more information.
 * `num_connections`: This defines the number of connections that the Cassandra 
driver will open to each Cassandra instance. By default, 2.
 * `local_dc`: This configures the local data center of the Cassandra driver. 
Ensure that this is the same value as the data center of the Cassandra nodes 
which this Cassandra Sidecar manages.
+* `schema_refresh_time`: This defines how frequently Cassandra Sidecar 
refreshes its cache of the schema read from Cassandra. Defaults to 5 minutes 
(`5m`).
+* `unsupported_table_schema_refresh_time`: Deprecated in favor of 
`schema_refresh_time`, which falls back to this property when it is the only 
one configured.
 
 [[healthcheck]]
 ### healthcheck
diff --git 
a/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/routes/CassandraSchemaRouteIntegrationTest.java
 
b/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/routes/CassandraSchemaRouteIntegrationTest.java
index 56cb507a3..af2082417 100644
--- 
a/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/routes/CassandraSchemaRouteIntegrationTest.java
+++ 
b/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/routes/CassandraSchemaRouteIntegrationTest.java
@@ -18,10 +18,15 @@
 
 package org.apache.cassandra.sidecar.routes;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 
 import org.junit.jupiter.api.Test;
 
+import com.datastax.driver.core.Cluster;
+import com.datastax.driver.core.Row;
+import com.datastax.driver.core.Session;
 import io.vertx.core.http.HttpResponseExpectation;
 import org.apache.cassandra.sidecar.common.response.SchemaResponse;
 import 
org.apache.cassandra.sidecar.testing.SharedClusterSidecarIntegrationTestBase;
@@ -104,4 +109,54 @@ class CassandraSchemaRouteIntegrationTest extends 
SharedClusterSidecarIntegratio
         assertThat(response.keyspace()).isEqualTo("keyspace");
         assertThat(response.schema()).isNotNull();
     }
+
+    @Test
+    void testSchemaResponseForEachKeyspace()
+    {
+        for (String keyspace : List.of("testkeyspace", "\"Cycling\"", 
"\"keyspace\""))
+        {
+            String testRoute = "/api/v1/schema/keyspaces/" + keyspace;
+            SchemaResponse response = 
getBlocking(trustedClient().get(serverWrapper.serverPort, "localhost", 
testRoute)
+                                                                 .send()
+                                                                 
.expecting(HttpResponseExpectation.SC_OK))
+                                      .bodyAsJson(SchemaResponse.class);
+            assertThat(response.schema())
+            .describedAs("Schema of %s must be the schema rendered by 
Cassandra", keyspace)
+            .isEqualTo(convertToUpperCase(describeKeyspace(keyspace)));
+        }
+    }
+
+    @Test
+    void testAllKeyspacesPresentInFullSchema()
+    {
+        String testRoute = "/api/v1/schema/keyspaces";
+        SchemaResponse response = 
getBlocking(trustedClient().get(serverWrapper.serverPort, "localhost", 
testRoute)
+                                                             .send()
+                                                             
.expecting(HttpResponseExpectation.SC_OK))
+                                  .bodyAsJson(SchemaResponse.class);
+        
assertThat(response.schema()).contains(describeKeyspace("testkeyspace"));
+        
assertThat(response.schema()).contains(describeKeyspace("\"Cycling\""));
+        
assertThat(response.schema()).contains(describeKeyspace("\"keyspace\""));
+        assertThat(response.schema()).contains("CREATE KEYSPACE 
system_traces");
+    }
+
+    String describeKeyspace(String maybeQuotedKeyspace)
+    {
+        try (Cluster driverCluster = createDriverCluster(cluster.delegate());
+             Session session = driverCluster.connect())
+        {
+            List<String> statements = new ArrayList<>();
+            for (Row row : session.execute("DESCRIBE KEYSPACE " + 
maybeQuotedKeyspace).all())
+            {
+                statements.add(row.getString("create_statement"));
+            }
+            assertThat(statements).isNotEmpty();
+            return String.join("\n\n", statements);
+        }
+    }
+
+    String convertToUpperCase(String keyspaceSchema)
+    {
+        return keyspaceSchema.replaceFirst("WITH replication", "WITH 
REPLICATION");
+    }
 }
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/config/DriverConfiguration.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/config/DriverConfiguration.java
index 0ffea4cde..3097f5411 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/config/DriverConfiguration.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/config/DriverConfiguration.java
@@ -76,8 +76,16 @@ public interface DriverConfiguration
     SslConfiguration sslConfiguration();
 
     /**
+     * @deprecated use {@link #schemaRefreshTime()} instead
      * @return Refresh interval of table schemas not supported by Java 
driver's metadata
-     * (not parseable, e.g. including vector type).
+     * (not parseable, e.g. including vector type), {@code null} when it is 
not configured.
      */
+    @Deprecated
     SecondBoundConfiguration unsupportedTableSchemaRefreshTime();
+
+    /**
+     * @return Refresh interval of schema cache. When it is not configured, 
the deprecated
+     * {@link #unsupportedTableSchemaRefreshTime()} is used instead, if it is 
configured.
+     */
+    SecondBoundConfiguration schemaRefreshTime();
 }
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/DriverConfigurationImpl.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/DriverConfigurationImpl.java
index fdd1f0dd2..77fe1bbe6 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/DriverConfigurationImpl.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/DriverConfigurationImpl.java
@@ -35,7 +35,7 @@ import org.apache.cassandra.sidecar.config.SslConfiguration;
 public class DriverConfigurationImpl implements DriverConfiguration
 {
     private static final int DEFAULT_NUM_CONNECTIONS = 1000;
-    private static final SecondBoundConfiguration 
DEFAULT_UNSUPPORTED_TABLE_SCHEMA_REFRESH_TIME = new SecondBoundConfiguration(5, 
TimeUnit.MINUTES);
+    private static final SecondBoundConfiguration DEFAULT_SCHEMA_REFRESH_TIME 
= new SecondBoundConfiguration(5, TimeUnit.MINUTES);
 
     @JsonProperty("contact_points")
     private final List<InetSocketAddress> contactPoints;
@@ -58,9 +58,13 @@ public class DriverConfigurationImpl implements 
DriverConfiguration
     @JsonProperty("ssl")
     private final SslConfiguration sslConfiguration;
 
+    @Deprecated
     @JsonProperty("unsupported_table_schema_refresh_time")
     private final SecondBoundConfiguration unsupportedTableSchemaRefreshTime;
 
+    @JsonProperty("schema_refresh_time")
+    private final SecondBoundConfiguration schemaRefreshTime;
+
     public DriverConfigurationImpl()
     {
         this(builder());
@@ -76,6 +80,7 @@ public class DriverConfigurationImpl implements 
DriverConfiguration
         authProvider = builder.authProvider;
         sslConfiguration = builder.sslConfiguration;
         unsupportedTableSchemaRefreshTime = 
builder.unsupportedTableSchemaRefreshTime;
+        schemaRefreshTime = builder.schemaRefreshTime;
     }
 
     /**
@@ -153,6 +158,7 @@ public class DriverConfigurationImpl implements 
DriverConfiguration
     /**
      * {@inheritDoc}
      */
+    @Deprecated
     @Override
     @JsonProperty("unsupported_table_schema_refresh_time")
     public SecondBoundConfiguration unsupportedTableSchemaRefreshTime()
@@ -160,6 +166,21 @@ public class DriverConfigurationImpl implements 
DriverConfiguration
         return unsupportedTableSchemaRefreshTime;
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    @JsonProperty("schema_refresh_time")
+    public SecondBoundConfiguration schemaRefreshTime()
+    {
+        if (schemaRefreshTime != null)
+        {
+            return schemaRefreshTime;
+        }
+        return unsupportedTableSchemaRefreshTime != null ? 
unsupportedTableSchemaRefreshTime
+                                                         : 
DEFAULT_SCHEMA_REFRESH_TIME;
+    }
+
     public static Builder builder()
     {
         return new Builder();
@@ -177,7 +198,8 @@ public class DriverConfigurationImpl implements 
DriverConfiguration
         private String password;
         private ParameterizedClassConfiguration authProvider;
         private SslConfiguration sslConfiguration;
-        private SecondBoundConfiguration unsupportedTableSchemaRefreshTime = 
DEFAULT_UNSUPPORTED_TABLE_SCHEMA_REFRESH_TIME;
+        private SecondBoundConfiguration unsupportedTableSchemaRefreshTime;
+        private SecondBoundConfiguration schemaRefreshTime;
 
         private Builder()
         {
@@ -291,12 +313,26 @@ public class DriverConfigurationImpl implements 
DriverConfiguration
          *
          * @param unsupportedTableSchemaRefreshTime the {@code 
unsupportedTableSchemaRefreshTime} to set
          * @return a reference to this Builder
+         * @deprecated use {@link 
#schemaRefreshTime(SecondBoundConfiguration)} instead
          */
+        @Deprecated
         public Builder 
unsupportedTableSchemaRefreshTime(SecondBoundConfiguration 
unsupportedTableSchemaRefreshTime)
         {
             return update(b -> b.unsupportedTableSchemaRefreshTime = 
unsupportedTableSchemaRefreshTime);
         }
 
+        /**
+         * Sets the {@code schemaRefreshTime} and returns a reference to this 
Builder enabling
+         * method chaining.
+         *
+         * @param schemaRefreshTime the {@code schemaRefreshTime} to set
+         * @return a reference to this Builder
+         */
+        public Builder schemaRefreshTime(SecondBoundConfiguration 
schemaRefreshTime)
+        {
+            return update(b -> b.schemaRefreshTime = schemaRefreshTime);
+        }
+
         /**
          * Returns a {@code DriverConfigurationImpl} built from the parameters 
previously set.
          *
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/db/DriverUnsupportedSchemaCache.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/db/DriverUnsupportedSchemaCache.java
deleted file mode 100644
index 2250147ee..000000000
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/db/DriverUnsupportedSchemaCache.java
+++ /dev/null
@@ -1,293 +0,0 @@
-/*
- * 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.db;
-
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.SortedMap;
-import java.util.TreeMap;
-import java.util.function.Predicate;
-import java.util.stream.Collectors;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.datastax.driver.core.Cluster;
-import com.datastax.driver.core.KeyspaceMetadata;
-import com.datastax.driver.core.PreparedStatement;
-import com.datastax.driver.core.Row;
-import com.datastax.driver.core.Session;
-import com.datastax.driver.core.TableMetadata;
-import com.google.inject.Singleton;
-import io.vertx.core.Promise;
-import org.apache.cassandra.sidecar.common.server.CQLSessionProvider;
-import org.apache.cassandra.sidecar.common.server.data.Name;
-import org.apache.cassandra.sidecar.common.server.data.QualifiedTableName;
-import org.apache.cassandra.sidecar.common.server.utils.DurationSpec;
-import org.apache.cassandra.sidecar.common.utils.StringUtils;
-import org.apache.cassandra.sidecar.config.SidecarConfiguration;
-import org.apache.cassandra.sidecar.exceptions.CassandraUnavailableException;
-import org.apache.cassandra.sidecar.tasks.PeriodicTask;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import org.jetbrains.annotations.VisibleForTesting;
-
-/**
- * The {@link DriverUnsupportedSchemaCache} class maintains cache of CQL 
schema for tables whose definition is not
- * supported by driver natively. Java driver 3.x does not return {@link 
TableMetadata} for tables which schema
- * could not be parsed. Since it does not currently support vector type, any 
table containing vector would not
- * be visible. Cache is refreshed for the first time as soon as CQL connection 
is established or upon first lookup.
- * Later, it is periodically refreshed according to configured schedule.
- * TODO: Remove after upgrade to Java driver 4.x (CASSSIDECAR-421).
- */
-@Singleton
-public class DriverUnsupportedSchemaCache implements PeriodicTask
-{
-    private static final Logger LOGGER = 
LoggerFactory.getLogger(DriverUnsupportedSchemaCache.class);
-    private static final String STATEMENT_DELIMITER = "\n\n";
-
-    private final SidecarConfiguration sidecarConfiguration;
-    private final CQLSessionProvider sessionProvider;
-    private final CQLSchemaAccessor schemaAccessor;
-
-    // cache contains only schemas unparseable by Java driver
-    // during every cache refresh, reference is being reassigned to achieve 
atomic swap
-    private volatile SortedMap<QualifiedTableName, String> schemaCache;
-    private volatile boolean initialized; // flag indicating whether cache has 
been populated at least once
-
-    private PreparedStatement tableListStatement;
-
-    public DriverUnsupportedSchemaCache(SidecarConfiguration 
sidecarConfiguration,
-                                        CQLSessionProvider sessionProvider)
-    {
-        this.sidecarConfiguration = sidecarConfiguration;
-        this.sessionProvider = sessionProvider;
-        this.schemaAccessor = new CQLSchemaAccessor(sessionProvider);
-        this.schemaCache = createCache();
-        this.initialized = false;
-    }
-
-    /**
-     * @return Schema for all tables across all keyspaces (only those not 
supported by Java driver).
-     */
-    @NotNull
-    public String getFullSchema()
-    {
-        // we cannot know whether schema was updated after last
-        // periodic refresh, so results may be stale
-        refreshIfUninitialized();
-        return getUnsupportedSchema(table -> true);
-    }
-
-    /**
-     * @return Schema for all tables within given keyspaces (only those not 
supported by Java driver).
-     */
-    @NotNull
-    public String getKeyspaceSchema(@NotNull Name keyspace)
-    {
-        // we cannot know whether schema was updated after last
-        // periodic refresh, so results may be stale
-        refreshIfUninitialized();
-        return getUnsupportedSchema(table -> 
keyspace.equals(table.getKeyspace()));
-    }
-
-    /**
-     * @return Schema for table not supported by Java driver's metadata, 
{@code null} otherwise.
-     */
-    @Nullable
-    public String getTableSchema(@NotNull Name keyspace, @NotNull Name table)
-    {
-        return getTableSchema(keyspace, table, true);
-    }
-
-    /**
-     * @param allowRefresh Flag indicating whether lookup of table schema via 
CQL query
-     *                     is allowed, when data not found in cache.
-     * @return Schema for table not supported by Java driver's metadata, 
{@code null} otherwise.
-     */
-    @Nullable
-    public String getTableSchema(@NotNull Name keyspace, @NotNull Name table, 
boolean allowRefresh)
-    {
-        refreshIfUninitialized();
-        QualifiedTableName name = new QualifiedTableName(keyspace, table);
-        String schema = schemaCache.get(name);
-        if (schema == null && allowRefresh)
-        {
-            // proactively fetch table schema, not to provide
-            // false-negative when table is not cached yet
-            // attention, method can block
-            schema = populateSchemaCache(schemaCache, name);
-        }
-        return schema;
-    }
-
-    @Override
-    public DurationSpec delay()
-    {
-        return 
sidecarConfiguration.driverConfiguration().unsupportedTableSchemaRefreshTime();
-    }
-
-    @Override
-    public void execute(Promise<Void> promise)
-    {
-        try
-        {
-            refresh(false);
-            promise.tryComplete();
-        }
-        catch (Throwable t)
-        {
-            promise.fail(t);
-        }
-    }
-
-    private String getUnsupportedSchema(Predicate<QualifiedTableName> 
condition)
-    {
-        StringBuilder result = new StringBuilder();
-        for (Map.Entry<QualifiedTableName, String> entry : 
schemaCache.entrySet())
-        {
-            if (condition.test(entry.getKey()))
-            {
-                result.append(entry.getValue()).append(STATEMENT_DELIMITER);
-            }
-        }
-        return result.toString().trim();
-    }
-
-    private void refreshIfUninitialized()
-    {
-        if (!initialized)
-        {
-            refresh(true);
-        }
-    }
-
-    public synchronized void refresh(boolean initializeOnly)
-    {
-        if (initialized && initializeOnly)
-        {
-            // cache has been already initialized, early exit
-            return;
-        }
-        try
-        {
-            Session session = sessionProvider.get();
-            prepareStatements(session);
-
-            Set<QualifiedTableName> tables = queryAllTables(session);
-            Set<QualifiedTableName> driverKnownTables = 
driverKnownTables(session);
-
-            tables.removeAll(driverKnownTables);
-
-            SortedMap<QualifiedTableName, String> newCache = createCache();
-            if (!tables.isEmpty())
-            {
-                LOGGER.debug("Tables not supported by Java driver metadata: 
{}", tables);
-                tables.forEach(table -> populateSchemaCache(newCache, table));
-            }
-            // replacing cache, because some tables might have been removed in 
the meanwhile
-            schemaCache = newCache;
-
-            initialized = true;
-        }
-        catch (CassandraUnavailableException ignored)
-        {
-            LOGGER.debug("Not yet connected to Cassandra cluster");
-        }
-    }
-
-    private String populateSchemaCache(Map<QualifiedTableName, String> cache, 
QualifiedTableName table)
-    {
-        Name keyspaceName = table.getKeyspace();
-        Name tableName = table.table();
-        if (keyspaceName == null || tableName == null)
-        {
-            throw new IllegalArgumentException("Invalid table name: " + table);
-        }
-        List<String> cqlSchema = schemaAccessor.getTableSchema(keyspaceName, 
tableName);
-        if (cqlSchema != null)
-        {
-            String schema = String.join(STATEMENT_DELIMITER, cqlSchema);
-            cache.put(table, schema);
-            return schema;
-        }
-        return null;
-    }
-
-    private Set<QualifiedTableName> queryAllTables(Session session)
-    {
-        List<Row> rows = session.execute(tableListStatement.bind()).all();
-        return rows.stream()
-                   .map(r -> new 
QualifiedTableName(r.getString("keyspace_name"),
-                                                    r.getString("table_name")))
-                   .collect(Collectors.toSet());
-    }
-
-    private Set<QualifiedTableName> driverKnownTables(Session session)
-    {
-        Set<QualifiedTableName> result = new HashSet<>();
-        Cluster cluster = session.getCluster();
-        for (KeyspaceMetadata keyspace : cluster.getMetadata().getKeyspaces())
-        {
-            for (TableMetadata table : keyspace.getTables())
-            {
-                result.add(new QualifiedTableName(keyspace.getName(), 
table.getName()));
-            }
-        }
-        return result;
-    }
-
-    private void prepareStatements(Session session)
-    {
-        if (tableListStatement == null)
-        {
-            tableListStatement = session.prepare("SELECT keyspace_name, 
table_name FROM system_schema.tables");
-        }
-    }
-
-    private SortedMap<QualifiedTableName, String> createCache()
-    {
-        // use sorted map for repeatable results when retrieving full schema
-        // synchronized, because different threads may indirectly add items to 
the map using getTableSchema() method
-        return Collections.synchronizedSortedMap(new 
TreeMap<>(Comparator.comparing(QualifiedTableName::toString)));
-    }
-
-    @VisibleForTesting
-    void setInitialized(boolean initialized)
-    {
-        this.initialized = initialized;
-    }
-
-    public static String concatSchemas(String ... schemas)
-    {
-        StringBuilder result = new StringBuilder();
-        for (String schema : schemas)
-        {
-            if (StringUtils.isNotEmpty(schema))
-            {
-                result.append(schema.trim()).append(STATEMENT_DELIMITER);
-            }
-        }
-        return result.toString().trim();
-    }
-}
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/db/SchemaCache.java 
b/server/src/main/java/org/apache/cassandra/sidecar/db/SchemaCache.java
new file mode 100644
index 000000000..d9395af6c
--- /dev/null
+++ b/server/src/main/java/org/apache/cassandra/sidecar/db/SchemaCache.java
@@ -0,0 +1,379 @@
+/*
+ * 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.db;
+
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentNavigableMap;
+import java.util.concurrent.ConcurrentSkipListMap;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.datastax.driver.core.Cluster;
+import com.datastax.driver.core.KeyspaceMetadata;
+import com.datastax.driver.core.Metadata;
+import com.datastax.driver.core.PreparedStatement;
+import com.datastax.driver.core.Row;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.TableMetadata;
+import com.google.inject.Singleton;
+import io.vertx.core.Promise;
+import org.apache.cassandra.sidecar.common.server.CQLSessionProvider;
+import org.apache.cassandra.sidecar.common.server.data.Name;
+import org.apache.cassandra.sidecar.common.server.data.QualifiedTableName;
+import org.apache.cassandra.sidecar.common.server.utils.DurationSpec;
+import org.apache.cassandra.sidecar.common.utils.StringUtils;
+import org.apache.cassandra.sidecar.config.SidecarConfiguration;
+import org.apache.cassandra.sidecar.exceptions.CassandraUnavailableException;
+import org.apache.cassandra.sidecar.tasks.PeriodicTask;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.VisibleForTesting;
+
+/**
+ * The {@link SchemaCache} class maintains cache of keyspace, table schemas 
read through {@code DESCRIBE} call. Schema
+ * provided by Java driver could be partial as it drops objects it cannot 
parse. It also stores driver unparseable
+ * table level schemas, used for table existence checks.
+ * <p>
+ * Cache is regularly refreshed, but schema changes applied between refreshes 
may not be immediately visible
+ */
+@Singleton
+public class SchemaCache implements PeriodicTask
+{
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(SchemaCache.class);
+    private static final String STATEMENT_DELIMITER = "\n\n";
+
+    private final SidecarConfiguration sidecarConfiguration;
+    private final CQLSessionProvider sessionProvider;
+    private final CQLSchemaAccessor schemaAccessor;
+
+    // Caches table schemas those that could not be parsed by Java driver
+    private volatile ConcurrentNavigableMap<QualifiedTableName, String> 
unsupportedTableSchemaCache;
+    private volatile ConcurrentNavigableMap<String, String> 
keyspaceSchemaCache;
+    // CQL schema of all keyspaces. It is invalidated whenever the schema of a 
keyspace is added to keyspaceSchemaCache.
+    private final AtomicReference<FullSchema> fullSchemaCache = new 
AtomicReference<>(new FullSchema(null));
+    private volatile boolean initialized;
+
+    private PreparedStatement tableListStatement;
+
+    public SchemaCache(SidecarConfiguration sidecarConfiguration, 
CQLSessionProvider sessionProvider)
+    {
+        this.sidecarConfiguration = sidecarConfiguration;
+        this.sessionProvider = sessionProvider;
+        this.schemaAccessor = new CQLSchemaAccessor(sessionProvider);
+        this.unsupportedTableSchemaCache = createTableSchemaCache();
+        this.keyspaceSchemaCache = createKeyspaceSchemaCache();
+        this.initialized = false;
+    }
+
+    /**
+     * @return CQL schema of all keyspaces. Keyspaces created after the last 
cache refresh are not included, unless
+     * their schema has been looked up individually in the meanwhile, so 
results may be stale.
+     */
+    @NotNull
+    public String getFullSchema()
+    {
+        refreshIfUninitialized();
+        FullSchema cached = fullSchemaCache.get();
+        if (cached.schema != null)
+        {
+            return cached.schema;
+        }
+        String fullSchema = assembleFullSchema(keyspaceSchemaCache);
+        // Cache only if nothing was added while assembling, the result would 
miss it otherwise.
+        // The assembled schema is returned either way
+        fullSchemaCache.compareAndSet(cached, new FullSchema(fullSchema));
+        return fullSchema;
+    }
+
+    /**
+     * @return CQL schema of the keyspace, {@code null} when the keyspace does 
not exist. If the keyspace was updated
+     * after last refresh, results maybe stale until next refresh.
+     */
+    @Nullable
+    public String getKeyspaceSchema(@NotNull String keyspace)
+    {
+        refreshIfUninitialized();
+        ConcurrentNavigableMap<String, String> cache = keyspaceSchemaCache;
+        String schema = cache.get(keyspace);
+        if (schema == null)
+        {
+            schema = populateKeyspaceSchemaCache(cache, keyspace);
+            if (schema != null)
+            {
+                // the keyspace was added to the live cache, so the cached 
full schema is outdated
+                setFullSchema(null);
+            }
+        }
+        return schema;
+    }
+
+    /**
+     * @return Schema for all tables across all keyspaces (only those not 
supported by Java driver). we cannot know
+     * whether schema was updated after last periodic refresh, so results may 
be stale.
+     */
+    @NotNull
+    public String getUnsupportedTableSchema()
+    {
+        refreshIfUninitialized();
+        StringBuilder result = new StringBuilder();
+        for (Map.Entry<QualifiedTableName, String> entry : 
unsupportedTableSchemaCache.entrySet())
+        {
+            result.append(entry.getValue()).append(STATEMENT_DELIMITER);
+        }
+        return result.toString().trim();
+    }
+
+    /**
+     * @return Schema for table not supported by Java driver's metadata, 
{@code null} otherwise.
+     */
+    @Nullable
+    public String getUnsupportedTableSchema(@NotNull Name keyspace, @NotNull 
Name table)
+    {
+        return getUnsupportedTableSchema(keyspace, table, true);
+    }
+
+    /**
+     * @param allowRefresh Flag indicating whether lookup of table schema via 
CQL query
+     *                     is allowed, when data not found in cache.
+     * @return Schema for table not supported by Java driver's metadata, 
{@code null} otherwise.
+     */
+    @Nullable
+    public String getUnsupportedTableSchema(@NotNull Name keyspace, @NotNull 
Name table, boolean allowRefresh)
+    {
+        refreshIfUninitialized();
+        // the cache is keyed by the names as stored by Cassandra
+        QualifiedTableName name = new 
QualifiedTableName(foldIfUnquoted(keyspace), foldIfUnquoted(table));
+        String schema = unsupportedTableSchemaCache.get(name);
+        if (schema == null && allowRefresh)
+        {
+            // proactively fetch table schema, not to provide
+            // false-negative when table is not cached yet
+            // attention, method can block
+            schema = populateTableSchemaCache(unsupportedTableSchemaCache, 
name);
+        }
+        return schema;
+    }
+
+    @Override
+    public DurationSpec delay()
+    {
+        return sidecarConfiguration.driverConfiguration().schemaRefreshTime();
+    }
+
+    @Override
+    public void execute(Promise<Void> promise)
+    {
+        try
+        {
+            refresh(false);
+            promise.tryComplete();
+        }
+        catch (Throwable t)
+        {
+            promise.fail(t);
+        }
+    }
+
+    private void refreshIfUninitialized()
+    {
+        if (!initialized)
+        {
+            refresh(true);
+        }
+    }
+
+    public synchronized void refresh(boolean initializeOnly)
+    {
+        if (initialized && initializeOnly)
+        {
+            return;
+        }
+        try
+        {
+            Session session = sessionProvider.get();
+            prepareStatements(session);
+            Set<QualifiedTableName> tables = queryAllTables(session);
+            Set<QualifiedTableName> driverKnownTables = 
driverKnownTables(session);
+            tables.removeAll(driverKnownTables);
+
+            ConcurrentNavigableMap<QualifiedTableName, String> 
newTableSchemaCache = createTableSchemaCache();
+            if (!tables.isEmpty())
+            {
+                LOGGER.debug("Tables not supported by Java driver metadata: 
{}", tables);
+                tables.forEach(table -> 
populateTableSchemaCache(newTableSchemaCache, table));
+            }
+            ConcurrentNavigableMap<String, String> newKeyspaceSchemaCache = 
createKeyspaceSchemaCache();
+            for (Name keyspace : schemaAccessor.getKeyspaces())
+            {
+                populateKeyspaceSchemaCache(newKeyspaceSchemaCache, 
keyspace.name());
+            }
+            unsupportedTableSchemaCache = newTableSchemaCache;
+            keyspaceSchemaCache = newKeyspaceSchemaCache;
+            setFullSchema(assembleFullSchema(newKeyspaceSchemaCache));
+            initialized = true;
+        }
+        catch (CassandraUnavailableException ignored)
+        {
+            LOGGER.debug("Error refreshing SchemaCache, not yet connected to 
Cassandra");
+        }
+    }
+
+    @Nullable
+    private String populateKeyspaceSchemaCache(Map<String, String> cache, 
String keyspace)
+    {
+        List<String> cqlSchema = schemaAccessor.getKeyspaceSchema(new 
Name(Metadata.quoteIfNecessary(keyspace)));
+        if (cqlSchema == null || cqlSchema.isEmpty())
+        {
+            return null;
+        }
+        String schema = String.join(STATEMENT_DELIMITER, cqlSchema);
+        cache.put(keyspace, schema);
+        return schema;
+    }
+
+    private void setFullSchema(@Nullable String fullSchema)
+    {
+        fullSchemaCache.set(new FullSchema(fullSchema));
+    }
+
+    private static String assembleFullSchema(Map<String, String> 
keyspaceSchemas)
+    {
+        StringBuilder result = new StringBuilder();
+        for (String keyspaceSchema : keyspaceSchemas.values())
+        {
+            result.append(keyspaceSchema).append(STATEMENT_DELIMITER);
+        }
+        return result.toString().trim();
+    }
+
+    private String populateTableSchemaCache(Map<QualifiedTableName, String> 
cache, QualifiedTableName table)
+    {
+        Name keyspaceName = table.getKeyspace();
+        Name tableName = table.table();
+        if (keyspaceName == null || tableName == null)
+        {
+            throw new IllegalArgumentException("Invalid table name: " + table);
+        }
+        // Cassandra stores the names unquoted, while DESCRIBE requires them 
to be quoted when the name is case
+        // sensitive or a reserved keyword
+        List<String> cqlSchema = schemaAccessor.getTableSchema(new 
Name(Metadata.quoteIfNecessary(keyspaceName.name())),
+                                                               new 
Name(Metadata.quoteIfNecessary(tableName.name())));
+        if (cqlSchema != null)
+        {
+            String schema = String.join(STATEMENT_DELIMITER, cqlSchema);
+            cache.put(table, schema);
+            return schema;
+        }
+        return null;
+    }
+
+    private Set<QualifiedTableName> queryAllTables(Session session)
+    {
+        List<Row> rows = session.execute(tableListStatement.bind()).all();
+        return rows.stream()
+                   .map(r -> new 
QualifiedTableName(r.getString("keyspace_name"),
+                                                    r.getString("table_name")))
+                   .collect(Collectors.toSet());
+    }
+
+    private Set<QualifiedTableName> driverKnownTables(Session session)
+    {
+        Set<QualifiedTableName> result = new HashSet<>();
+        Cluster cluster = session.getCluster();
+        for (KeyspaceMetadata keyspace : cluster.getMetadata().getKeyspaces())
+        {
+            for (TableMetadata table : keyspace.getTables())
+            {
+                result.add(new QualifiedTableName(keyspace.getName(), 
table.getName()));
+            }
+        }
+        return result;
+    }
+
+    private void prepareStatements(Session session)
+    {
+        if (tableListStatement == null)
+        {
+            tableListStatement = session.prepare("SELECT keyspace_name, 
table_name FROM system_schema.tables");
+        }
+    }
+
+    /**
+     * Normalizes a name obtained from a request into the form Cassandra 
stores it in: CQL folds unquoted
+     * names to lowercase, while quoted names keep their case.
+     */
+    private static String foldIfUnquoted(Name name)
+    {
+        return name.isSourceQuoted() ? name.name() : 
name.name().toLowerCase(Locale.ROOT);
+    }
+
+    private ConcurrentNavigableMap<QualifiedTableName, String> 
createTableSchemaCache()
+    {
+        // uses ConcurrentSkipListMap to have assembled full schema or table 
schemas consistent across calls
+        return new 
ConcurrentSkipListMap<>(Comparator.comparing(QualifiedTableName::toString));
+    }
+
+    private ConcurrentNavigableMap<String, String> createKeyspaceSchemaCache()
+    {
+        // uses ConcurrentSkipListMap to have assembled full schema or table 
schemas consistent across calls
+        return new ConcurrentSkipListMap<>();
+    }
+
+    @VisibleForTesting
+    void setInitialized(boolean initialized)
+    {
+        this.initialized = initialized;
+    }
+
+    public static String concatSchemas(String ... schemas)
+    {
+        StringBuilder result = new StringBuilder();
+        for (String schema : schemas)
+        {
+            if (StringUtils.isNotEmpty(schema))
+            {
+                result.append(schema.trim()).append(STATEMENT_DELIMITER);
+            }
+        }
+        return result.toString().trim();
+    }
+
+    /**
+     * Holder of the cached full schema. Compared by identity, so every update 
is distinguishable from the previous
+     * one even when the cached value is the same
+     */
+    private static final class FullSchema
+    {
+        @Nullable
+        final String schema;
+
+        FullSchema(@Nullable String schema)
+        {
+            this.schema = schema;
+        }
+    }
+}
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/KeyspaceSchemaHandler.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/KeyspaceSchemaHandler.java
index 3a43976bf..2fd3c91ec 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/KeyspaceSchemaHandler.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/KeyspaceSchemaHandler.java
@@ -25,17 +25,17 @@ import com.datastax.driver.core.Metadata;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import io.netty.handler.codec.http.HttpResponseStatus;
-import io.vertx.core.CompositeFuture;
 import io.vertx.core.Future;
 import io.vertx.core.http.HttpServerRequest;
 import io.vertx.core.net.SocketAddress;
 import io.vertx.ext.auth.authorization.Authorization;
 import io.vertx.ext.web.RoutingContext;
+import io.vertx.ext.web.handler.HttpException;
 import org.apache.cassandra.sidecar.acl.authorization.BasicPermissions;
 import org.apache.cassandra.sidecar.common.response.SchemaResponse;
 import org.apache.cassandra.sidecar.common.server.data.Name;
 import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
-import org.apache.cassandra.sidecar.db.DriverUnsupportedSchemaCache;
+import org.apache.cassandra.sidecar.db.SchemaCache;
 import org.apache.cassandra.sidecar.utils.CassandraInputValidator;
 import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
 import org.apache.cassandra.sidecar.utils.MetadataUtils;
@@ -49,7 +49,7 @@ import static 
org.apache.cassandra.sidecar.utils.HttpExceptions.wrapHttpExceptio
 @Singleton
 public class KeyspaceSchemaHandler extends AbstractHandler<Name> implements 
AccessProtected
 {
-    private final DriverUnsupportedSchemaCache driverUnsupportedSchemaCache;
+    private final SchemaCache schemaCache;
 
     /**
      * Constructs a handler with the provided {@code metadataFetcher}
@@ -57,16 +57,16 @@ public class KeyspaceSchemaHandler extends 
AbstractHandler<Name> implements Acce
      * @param metadataFetcher the interface to retrieve metadata
      * @param executorPools   executor pools for blocking executions
      * @param validator       a validator instance to validate 
Cassandra-specific input
-     * @param driverUnsupportedSchemaCache cache of unparseable table schemas 
by Java driver
+     * @param schemaCache cache of CQL schema read from Cassandra
      */
     @Inject
     protected KeyspaceSchemaHandler(InstanceMetadataFetcher metadataFetcher,
                                     ExecutorPools executorPools,
                                     CassandraInputValidator validator,
-                                    DriverUnsupportedSchemaCache 
driverUnsupportedSchemaCache)
+                                    SchemaCache schemaCache)
     {
         super(metadataFetcher, executorPools, validator);
-        this.driverUnsupportedSchemaCache = driverUnsupportedSchemaCache;
+        this.schemaCache = schemaCache;
     }
 
     @Override
@@ -85,84 +85,68 @@ public class KeyspaceSchemaHandler extends 
AbstractHandler<Name> implements Acce
                                SocketAddress remoteAddress,
                                Name keyspace)
     {
-        Future.all(metadata(host), unsupportedSchema(keyspace))
-              .onFailure(cause -> processFailure(cause, context, host, 
remoteAddress, keyspace))
-              .onSuccess(future -> handleWithMetadata(context, keyspace, 
future));
+        describeSchema(host, keyspace)
+        .onSuccess(context::json)
+        .onFailure(cause -> processFailure(cause, context, host, 
remoteAddress, keyspace));
+    }
+
+    private Future<SchemaResponse> describeSchema(String host, Name keyspace)
+    {
+        return executorPools.service()
+                            .executeBlocking(() -> {
+                                if (keyspace == null)
+                                {
+                                    String schema = 
schemaCache.getFullSchema();
+                                    if (schema.isEmpty())
+                                    {
+                                        throw schemaNotAvailable();
+                                    }
+                                    return new SchemaResponse(schema);
+                                }
+
+                                String keyspaceSchema = 
schemaCache.getKeyspaceSchema(resolveKeyspace(host, keyspace));
+                                if (keyspaceSchema == null)
+                                {
+                                    throw keyspaceDoesNotExist(keyspace);
+                                }
+                                return new SchemaResponse(keyspace.name(), 
upperCaseReplication(keyspaceSchema));
+                            });
     }
 
     /**
-     * Handles the request with the Cassandra {@link Metadata metadata}.
-     *
-     * @param context  the event to handle
-     * @param keyspace the keyspace parsed from the request
-     * @param future   composite future containing result of:
-     *                 - the metadata on the connected cluster, including 
known nodes and schema definitions
-     *                 - additional schema which could not be parsed by Java 
driver
+     * {@code DESCRIBE KEYSPACE} emits the replication in lower case, whereas 
the schema previously returned
+     * by this endpoint, generated by Java driver, used upper case. Keywords 
are upper cased to keep the
+     * response compatible with existing clients.
      */
-    private void handleWithMetadata(RoutingContext context, Name keyspace, 
CompositeFuture future)
+    private String upperCaseReplication(String schema)
     {
-        Metadata metadata = future.resultAt(0);
-        String unparseableSchema = future.resultAt(1);
-        if (keyspace == null)
-        {
-            String fullSchema = 
DriverUnsupportedSchemaCache.concatSchemas(metadata.exportSchemaAsString(),
-                                                                           
unparseableSchema);
-            SchemaResponse schemaResponse = new SchemaResponse(fullSchema);
-            context.json(schemaResponse);
-            return;
-        }
+        return schema.replaceFirst("WITH replication", "WITH REPLICATION");
+    }
 
-        // retrieve keyspace metadata
+    /**
+     * Checks keyspace exists and resolves to String stored in
+     */
+    private String resolveKeyspace(String host, Name keyspace)
+    {
+        Metadata metadata = metadataFetcher.delegate(host).metadata();
         KeyspaceMetadata ksMetadata = MetadataUtils.keyspace(metadata, 
keyspace);
-
         if (ksMetadata == null)
         {
-            // set request as failed and return
-            // keyspace does not exist
-            String errorMessage = String.format("Keyspace '%s' does not 
exist.", keyspace);
-            context.fail(wrapHttpException(HttpResponseStatus.NOT_FOUND, 
errorMessage));
-            return;
+            throw keyspaceDoesNotExist(keyspace);
         }
-
-        String keyspaceSchema = 
DriverUnsupportedSchemaCache.concatSchemas(ksMetadata.exportAsString(),
-                                                                           
unparseableSchema);
-        SchemaResponse schemaResponse = new SchemaResponse(keyspace.name(), 
keyspaceSchema);
-        context.json(schemaResponse);
+        return ksMetadata.getName();
     }
 
-    /**
-     * Gets cluster metadata asynchronously.
-     *
-     * @param host the Cassandra instance host
-     * @return {@link Future} containing {@link Metadata}
-     */
-    private Future<Metadata> metadata(String host)
+    private HttpException keyspaceDoesNotExist(Name keyspace)
     {
-        return executorPools.service().executeBlocking(() -> {
-            // metadata can block so we need to run in a blocking thread
-            return metadataFetcher.delegate(host).metadata();
-        });
+        String errorMessage = String.format("Keyspace '%s' does not exist.", 
keyspace.name());
+        return wrapHttpException(HttpResponseStatus.NOT_FOUND, errorMessage);
     }
 
-    /**
-     * Get CQL schema not parseable by Java driver (and therefore not present 
in {@link Metadata}).
-     *
-     * @param keyspace optional keyspace name, {@code null} includes schema 
for all keyspaces
-     * @return {@link Future} containing CQL schema
-     */
-    private Future<String> unsupportedSchema(Name keyspace)
+    private HttpException schemaNotAvailable()
     {
-        return executorPools.service().executeBlocking(() -> {
-            // schema cache can block if it was not successfully initialized 
at least once
-            if (keyspace == null)
-            {
-                return driverUnsupportedSchemaCache.getFullSchema();
-            }
-            else
-            {
-                return 
driverUnsupportedSchemaCache.getKeyspaceSchema(keyspace);
-            }
-        });
+        return wrapHttpException(HttpResponseStatus.SERVICE_UNAVAILABLE,
+                                 "Schema is not available yet, Cassandra could 
not be reached.");
     }
 
     /**
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/SchemaHandler.java 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/SchemaHandler.java
index 351115b80..4ba144ea6 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/SchemaHandler.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/SchemaHandler.java
@@ -27,7 +27,7 @@ import io.vertx.ext.web.RoutingContext;
 import org.apache.cassandra.sidecar.acl.authorization.BasicPermissions;
 import org.apache.cassandra.sidecar.common.server.data.Name;
 import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
-import org.apache.cassandra.sidecar.db.DriverUnsupportedSchemaCache;
+import org.apache.cassandra.sidecar.db.SchemaCache;
 import org.apache.cassandra.sidecar.utils.CassandraInputValidator;
 import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
 
@@ -43,15 +43,15 @@ public class SchemaHandler extends KeyspaceSchemaHandler
      * @param metadataFetcher the interface to retrieve metadata
      * @param executorPools   executor pools for blocking executions
      * @param validator       a validator instance to validate 
Cassandra-specific input
-     * @param driverUnsupportedSchemaCache cache of unparseable table schemas 
by Java driver
+     * @param schemaCache     cache of schemas retrieved by describe stmt call
      */
     @Inject
     protected SchemaHandler(InstanceMetadataFetcher metadataFetcher,
                             ExecutorPools executorPools,
                             CassandraInputValidator validator,
-                            DriverUnsupportedSchemaCache 
driverUnsupportedSchemaCache)
+                            SchemaCache schemaCache)
     {
-        super(metadataFetcher, executorPools, validator, 
driverUnsupportedSchemaCache);
+        super(metadataFetcher, executorPools, validator, schemaCache);
     }
 
     @Override
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/sstableuploads/SSTableUploadHandler.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/sstableuploads/SSTableUploadHandler.java
index 3a9a55551..670b697f1 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/sstableuploads/SSTableUploadHandler.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/sstableuploads/SSTableUploadHandler.java
@@ -42,7 +42,7 @@ import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
 import org.apache.cassandra.sidecar.concurrent.TaskExecutorPool;
 import org.apache.cassandra.sidecar.config.SSTableUploadConfiguration;
 import org.apache.cassandra.sidecar.config.ServiceConfiguration;
-import org.apache.cassandra.sidecar.db.DriverUnsupportedSchemaCache;
+import org.apache.cassandra.sidecar.db.SchemaCache;
 import org.apache.cassandra.sidecar.handlers.AbstractHandler;
 import org.apache.cassandra.sidecar.handlers.AccessProtected;
 import org.apache.cassandra.sidecar.handlers.data.SSTableUploadRequestParam;
@@ -73,7 +73,7 @@ public class SSTableUploadHandler extends 
AbstractHandler<SSTableUploadRequestPa
     private final SSTableUploadsPathBuilder uploadPathBuilder;
     private final ConcurrencyLimiter limiter;
     private final DigestVerifierFactory digestVerifierFactory;
-    private final DriverUnsupportedSchemaCache driverUnsupportedSchemaCache;
+    private final SchemaCache schemaCache;
 
     /**
      * Constructs a handler with the provided params.
@@ -86,7 +86,7 @@ public class SSTableUploadHandler extends 
AbstractHandler<SSTableUploadRequestPa
      * @param executorPools                executor pools for blocking 
executions
      * @param validator                    a validator instance to validate 
Cassandra-specific input
      * @param digestVerifierFactory        a factory of checksum verifiers
-     * @param driverUnsupportedSchemaCache cache of unparseable table schemas 
by Java driver
+     * @param schemaCache                  cache containing unparseable table 
schemas by Java driver
      */
     @Inject
     protected SSTableUploadHandler(Vertx vertx,
@@ -97,7 +97,7 @@ public class SSTableUploadHandler extends 
AbstractHandler<SSTableUploadRequestPa
                                    ExecutorPools executorPools,
                                    CassandraInputValidator validator,
                                    DigestVerifierFactory digestVerifierFactory,
-                                   DriverUnsupportedSchemaCache 
driverUnsupportedSchemaCache)
+                                   SchemaCache schemaCache)
     {
         super(metadataFetcher, executorPools, validator);
         this.fs = vertx.fileSystem();
@@ -106,7 +106,7 @@ public class SSTableUploadHandler extends 
AbstractHandler<SSTableUploadRequestPa
         this.uploadPathBuilder = uploadPathBuilder;
         this.limiter = new 
ConcurrencyLimiter(configuration::concurrentUploadsLimit);
         this.digestVerifierFactory = digestVerifierFactory;
-        this.driverUnsupportedSchemaCache = driverUnsupportedSchemaCache;
+        this.schemaCache = schemaCache;
     }
 
     @Override
@@ -209,7 +209,7 @@ public class SSTableUploadHandler extends 
AbstractHandler<SSTableUploadRequestPa
     {
         TaskExecutorPool pool = executorPools.service();
         return Future.all(pool.executeBlocking(() -> 
metadataFetcher.delegate(host).metadata()),
-                          pool.executeBlocking(() -> 
driverUnsupportedSchemaCache.getTableSchema(request.keyspace(), 
request.table(), false)))
+                          pool.executeBlocking(() -> 
schemaCache.getUnsupportedTableSchema(request.keyspace(), request.table(), 
false)))
                      .compose(compositeFuture -> {
                          Metadata metadata = compositeFuture.resultAt(0);
                          String unparseableSchema = 
compositeFuture.resultAt(1);
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 f18964edf..8b729744d 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
@@ -31,7 +31,7 @@ import io.vertx.ext.web.RoutingContext;
 import org.apache.cassandra.sidecar.common.server.data.Name;
 import org.apache.cassandra.sidecar.common.server.data.QualifiedTableName;
 import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
-import org.apache.cassandra.sidecar.db.DriverUnsupportedSchemaCache;
+import org.apache.cassandra.sidecar.db.SchemaCache;
 import org.apache.cassandra.sidecar.handlers.AbstractHandler;
 import org.apache.cassandra.sidecar.routes.RoutingContextUtils;
 import org.apache.cassandra.sidecar.utils.CassandraInputValidator;
@@ -50,16 +50,16 @@ import static 
org.apache.cassandra.sidecar.utils.HttpExceptions.wrapHttpExceptio
 @Singleton
 public class ValidateTableExistenceHandler extends 
AbstractHandler<QualifiedTableName>
 {
-    private final DriverUnsupportedSchemaCache driverUnsupportedSchemaCache;
+    private final SchemaCache schemaCache;
 
     @Inject
     public ValidateTableExistenceHandler(InstanceMetadataFetcher 
metadataFetcher,
                                          ExecutorPools executorPools,
                                          CassandraInputValidator validator,
-                                         DriverUnsupportedSchemaCache 
driverUnsupportedSchemaCache)
+                                         SchemaCache schemaCache)
     {
         super(metadataFetcher, executorPools, validator);
-        this.driverUnsupportedSchemaCache = driverUnsupportedSchemaCache;
+        this.schemaCache = schemaCache;
     }
 
     // It is a validator, and it does not assume values (keyspace and table) 
are present.
@@ -144,9 +144,9 @@ public class ValidateTableExistenceHandler extends 
AbstractHandler<QualifiedTabl
                 return null;
             }
             // schema cache can block if it was not successfully initialized 
at least once
-            return driverUnsupportedSchemaCache.getTableSchema(new 
Name(keyspace),
-                                                               new Name(table),
-                                                               false);
+            return schemaCache.getUnsupportedTableSchema(new Name(keyspace),
+                                                         new Name(table),
+                                                         false);
         });
     }
 }
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/modules/CassandraOperationsModule.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/modules/CassandraOperationsModule.java
index 49c1af468..789ab0a31 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/modules/CassandraOperationsModule.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/modules/CassandraOperationsModule.java
@@ -322,7 +322,8 @@ public class CassandraOperationsModule extends 
AbstractModule
     @GET
     @Path(ApiEndpointsV1.ALL_KEYSPACES_SCHEMA_ROUTE)
     @Operation(summary = "Get all keyspaces schema",
-               description = "Returns the schema information for all keyspaces 
in the Cassandra cluster")
+               description = "Returns the schema information for all keyspaces 
in the Cassandra cluster, fetches " +
+                             "schema using describe schema call")
     @APIResponse(description = "Schema information retrieved successfully",
                  responseCode = "200",
                  content = @Content(mediaType = "application/json",
@@ -339,7 +340,8 @@ public class CassandraOperationsModule extends 
AbstractModule
     @GET
     @Path(ApiEndpointsV1.DEPRECATED_ALL_KEYSPACES_SCHEMA_ROUTE)
     @Operation(summary = "Get all keyspaces schema (deprecated)",
-               description = "Returns the schema information for all keyspaces 
in the Cassandra cluster. This endpoint is deprecated.")
+               description = "Returns the schema information for all keyspaces 
in the Cassandra cluster, fetches " +
+                             "schema using describe schema call. This endpoint 
is deprecated.")
     @APIResponse(description = "Schema information retrieved successfully",
                  responseCode = "200",
                  content = @Content(mediaType = "application/json",
@@ -355,7 +357,8 @@ public class CassandraOperationsModule extends 
AbstractModule
     @GET
     @Path(ApiEndpointsV1.KEYSPACE_SCHEMA_ROUTE)
     @Operation(summary = "Get keyspace schema",
-               description = "Returns the schema information for a specific 
keyspace")
+               description = "Returns the schema information for a specific 
keyspace, fetches schema using describe " +
+                             "schema call")
     @APIResponse(description = "Keyspace schema retrieved successfully",
                  responseCode = "200",
                  content = @Content(mediaType = "application/json",
@@ -372,7 +375,8 @@ public class CassandraOperationsModule extends 
AbstractModule
     @GET
     @Path(ApiEndpointsV1.DEPRECATED_KEYSPACE_SCHEMA_ROUTE)
     @Operation(summary = "Get keyspace schema (deprecated)",
-               description = "Returns the schema information for a specific 
keyspace. This endpoint is deprecated.")
+               description = "Returns the schema information for a specific 
keyspace, fetches schema using describe " +
+                             "schema call. This endpoint is deprecated.")
     @APIResponse(description = "Keyspace schema retrieved successfully",
                  responseCode = "200",
                  content = @Content(mediaType = "application/json",
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/modules/CdcModule.java 
b/server/src/main/java/org/apache/cassandra/sidecar/modules/CdcModule.java
index 09c33b691..043e14eeb 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/modules/CdcModule.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/modules/CdcModule.java
@@ -83,8 +83,8 @@ import 
org.apache.cassandra.sidecar.coordination.TokenRingProvider;
 import org.apache.cassandra.sidecar.db.CdcConfigAccessor;
 import org.apache.cassandra.sidecar.db.CdcDatabaseAccessor;
 import org.apache.cassandra.sidecar.db.CdcSystemViewsDatabaseAccessor;
-import org.apache.cassandra.sidecar.db.DriverUnsupportedSchemaCache;
 import org.apache.cassandra.sidecar.db.KafkaConfigAccessor;
+import org.apache.cassandra.sidecar.db.SchemaCache;
 import org.apache.cassandra.sidecar.db.TokenSplitConfigAccessor;
 import org.apache.cassandra.sidecar.db.schema.CdcStatesSchema;
 import org.apache.cassandra.sidecar.db.schema.ConfigsSchema;
@@ -158,12 +158,12 @@ public class CdcModule extends AbstractModule
     @Singleton
     CassandraClusterSchemaMonitor 
cassandraClusterSchemaMonitorInstance(InstanceMetadataFetcher 
instanceMetadataFetcher,
                                                                         
CdcDatabaseAccessor databaseAccessor,
-                                                                        
DriverUnsupportedSchemaCache driverUnsupportedSchemaCache,
+                                                                        
SchemaCache schemaCache,
                                                                         
SidecarConfiguration configuration,
                                                                         
Provider<CassandraBridge> cassandraBridgeProvider)
     {
         return new CassandraClusterSchemaMonitor(instanceMetadataFetcher, 
databaseAccessor,
-                                                 driverUnsupportedSchemaCache, 
configuration,
+                                                 schemaCache, configuration,
                                                  cassandraBridgeProvider);
     }
 
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/modules/ConfigurationModule.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/modules/ConfigurationModule.java
index 2da63b21d..811d6d6c7 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/modules/ConfigurationModule.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/modules/ConfigurationModule.java
@@ -59,7 +59,7 @@ import org.apache.cassandra.sidecar.config.JmxConfiguration;
 import org.apache.cassandra.sidecar.config.ParameterizedClassConfiguration;
 import org.apache.cassandra.sidecar.config.ServiceConfiguration;
 import org.apache.cassandra.sidecar.config.SidecarConfiguration;
-import org.apache.cassandra.sidecar.db.DriverUnsupportedSchemaCache;
+import org.apache.cassandra.sidecar.db.SchemaCache;
 import org.apache.cassandra.sidecar.db.schema.TableSchemaFetcher;
 import org.apache.cassandra.sidecar.exceptions.ConfigurationException;
 import org.apache.cassandra.sidecar.metrics.MetricRegistryFactory;
@@ -172,11 +172,11 @@ public class ConfigurationModule extends AbstractModule
 
     @Provides
     @Singleton
-    DriverUnsupportedSchemaCache driverUnsupportedSchemaCache(Vertx vertx,
-                                                              
SidecarConfiguration sidecarConfiguration,
-                                                              
CQLSessionProvider cqlSessionProvider)
+    SchemaCache schemaCache(Vertx vertx,
+                            SidecarConfiguration sidecarConfiguration,
+                            CQLSessionProvider cqlSessionProvider)
     {
-        DriverUnsupportedSchemaCache schemaCache = new 
DriverUnsupportedSchemaCache(sidecarConfiguration, cqlSessionProvider);
+        SchemaCache schemaCache = new SchemaCache(sidecarConfiguration, 
cqlSessionProvider);
         // trigger immediate cache population once Sidecar connects to 
Cassandra node
         EventBusUtils.onceLocalConsumer(vertx.eventBus(),
                                         ON_CASSANDRA_CQL_READY.address(),
@@ -188,8 +188,8 @@ public class ConfigurationModule extends AbstractModule
     }
 
     @ProvidesIntoMap
-    @KeyClassMapKey(PeriodicTaskMapKeys.UnsupportedSchemaCacheTaskKey.class)
-    PeriodicTask 
driverUnsupportedSchemaCachePeriodicTask(DriverUnsupportedSchemaCache 
schemaCache)
+    @KeyClassMapKey(PeriodicTaskMapKeys.SchemaCacheTaskKey.class)
+    PeriodicTask schemaCachePeriodicTask(SchemaCache schemaCache)
     {
         return schemaCache;
     }
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/PeriodicTaskMapKeys.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/PeriodicTaskMapKeys.java
index fcf7055fa..688d740a8 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/PeriodicTaskMapKeys.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/PeriodicTaskMapKeys.java
@@ -37,5 +37,5 @@ public interface PeriodicTaskMapKeys
     interface CdcPublisherTaskKey extends ClassKey {}
     interface CdcConfigRefresherNotifierKey extends ClassKey {}
     interface CassandraClusterSchemaTaskKey extends ClassKey {}
-    interface UnsupportedSchemaCacheTaskKey extends ClassKey {}
+    interface SchemaCacheTaskKey extends ClassKey {}
 }
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/tasks/CassandraClusterSchemaMonitor.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/tasks/CassandraClusterSchemaMonitor.java
index e3bd0a649..b56b1aa86 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/tasks/CassandraClusterSchemaMonitor.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/tasks/CassandraClusterSchemaMonitor.java
@@ -42,7 +42,7 @@ import 
org.apache.cassandra.sidecar.common.response.NodeSettings;
 import org.apache.cassandra.sidecar.common.server.utils.DurationSpec;
 import org.apache.cassandra.sidecar.config.SidecarConfiguration;
 import org.apache.cassandra.sidecar.db.CdcDatabaseAccessor;
-import org.apache.cassandra.sidecar.db.DriverUnsupportedSchemaCache;
+import org.apache.cassandra.sidecar.db.SchemaCache;
 import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
 import org.apache.cassandra.spark.data.CqlTable;
 import org.apache.cassandra.spark.data.partitioner.Partitioner;
@@ -79,7 +79,7 @@ public class CassandraClusterSchemaMonitor implements 
PeriodicTask
     private final AtomicReference<Set<TableIdentifier>> lastRegisteredTables = 
new AtomicReference<>(Collections.emptySet());
     private final ConcurrentHashMap<TableIdentifier, UUID> tableIdCache = new 
ConcurrentHashMap<>();
     private final CdcDatabaseAccessor databaseAccessor;
-    private final DriverUnsupportedSchemaCache driverUnsupportedSchemaCache;
+    private final SchemaCache schemaCache;
     private final CopyOnWriteArrayList<Runnable> schemaChangeListeners = new 
CopyOnWriteArrayList<>();
     private final SidecarConfiguration sidecarConfiguration;
     private final InstanceMetadataFetcher instanceFetcher;
@@ -87,14 +87,14 @@ public class CassandraClusterSchemaMonitor implements 
PeriodicTask
 
     public CassandraClusterSchemaMonitor(InstanceMetadataFetcher 
instanceFetcher,
                                          CdcDatabaseAccessor databaseAccessor,
-                                         DriverUnsupportedSchemaCache 
driverUnsupportedSchemaCache,
+                                         SchemaCache schemaCache,
                                          SidecarConfiguration 
sidecarConfiguration,
                                          Provider<CassandraBridge> 
cassandraBridgeProvider)
     {
 
         this.instanceFetcher = instanceFetcher;
         this.databaseAccessor = databaseAccessor;
-        this.driverUnsupportedSchemaCache = driverUnsupportedSchemaCache;
+        this.schemaCache = schemaCache;
         this.sidecarConfiguration = sidecarConfiguration;
         this.cassandraBridgeProvider = cassandraBridgeProvider;
     }
@@ -113,8 +113,7 @@ public class CassandraClusterSchemaMonitor implements 
PeriodicTask
         try
         {
             LOGGER.debug("Checking for schema changes...");
-            String fullSchemaText = 
DriverUnsupportedSchemaCache.concatSchemas(databaseAccessor.fullSchema(),
-                                                                               
driverUnsupportedSchemaCache.getFullSchema());
+            String fullSchemaText = 
SchemaCache.concatSchemas(databaseAccessor.fullSchema(), 
schemaCache.getUnsupportedTableSchema());
             if (!fullSchemaText.equals(currSchemaText.get()))
             {
                 LOGGER.info("Schema change detected, refreshing CDC tables");
diff --git 
a/server/src/test/integration/org/apache/cassandra/sidecar/testing/IntegrationTestModule.java
 
b/server/src/test/integration/org/apache/cassandra/sidecar/testing/IntegrationTestModule.java
index ce6c4590b..816018364 100644
--- 
a/server/src/test/integration/org/apache/cassandra/sidecar/testing/IntegrationTestModule.java
+++ 
b/server/src/test/integration/org/apache/cassandra/sidecar/testing/IntegrationTestModule.java
@@ -135,7 +135,7 @@ public class IntegrationTestModule extends AbstractModule
         DriverConfiguration driverConfiguration = 
DriverConfigurationImpl.builder()
                                                                          
.localDc("dc1")
                                                                          
.numConnections(1)
-                                                                         
.unsupportedTableSchemaRefreshTime(new SecondBoundConfiguration(3, 
TimeUnit.SECONDS))
+                                                                         
.schemaRefreshTime(new SecondBoundConfiguration(3, TimeUnit.SECONDS))
                                                                          
.authProvider(authProvider)
                                                                          
.build();
 
diff --git a/server/src/test/java/org/apache/cassandra/sidecar/TestModule.java 
b/server/src/test/java/org/apache/cassandra/sidecar/TestModule.java
index f2629483e..d67d58e3f 100644
--- a/server/src/test/java/org/apache/cassandra/sidecar/TestModule.java
+++ b/server/src/test/java/org/apache/cassandra/sidecar/TestModule.java
@@ -65,7 +65,7 @@ import 
org.apache.cassandra.sidecar.config.yaml.SchemaKeyspaceConfigurationImpl;
 import org.apache.cassandra.sidecar.config.yaml.SidecarConfigurationImpl;
 import org.apache.cassandra.sidecar.config.yaml.TestServiceConfiguration;
 import org.apache.cassandra.sidecar.config.yaml.ThrottleConfigurationImpl;
-import org.apache.cassandra.sidecar.db.DriverUnsupportedSchemaCache;
+import org.apache.cassandra.sidecar.db.SchemaCache;
 import org.apache.cassandra.sidecar.utils.CassandraVersionProvider;
 
 import static org.apache.cassandra.sidecar.utils.TestMetricUtils.registry;
@@ -268,11 +268,12 @@ public class TestModule extends AbstractModule
 
     @Provides
     @Singleton
-    public DriverUnsupportedSchemaCache driverUnsupportedSchemaCache()
+    public SchemaCache schemaCache()
     {
-        DriverUnsupportedSchemaCache mock = 
mock(DriverUnsupportedSchemaCache.class);
-        when(mock.getFullSchema()).thenReturn("");
+        SchemaCache mock = mock(SchemaCache.class);
+        when(mock.getUnsupportedTableSchema()).thenReturn("");
         when(mock.getKeyspaceSchema(any())).thenReturn("");
+        when(mock.getFullSchema()).thenReturn("");
         return mock;
     }
 }
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/db/CQLSchemaAccessorTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/db/CQLSchemaAccessorTest.java
index e9c6ad626..b8635af10 100644
--- 
a/server/src/test/java/org/apache/cassandra/sidecar/db/CQLSchemaAccessorTest.java
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/db/CQLSchemaAccessorTest.java
@@ -35,6 +35,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.startsWith;
 import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -42,14 +43,15 @@ class CQLSchemaAccessorTest
 {
     static final Name KEYSPACE = new Name("keyspace1");
     static final Name TABLE = new Name("table1");
-    static final String SCHEMA = "CREATE TABLE keyspace1.table1 (a int, b int, 
PRIMARY KEY (a));";
+    static final String VIRTUAL_KEYSPACE = "system_views";
+    static final String TABLE_SCHEMA = "CREATE TABLE keyspace1.table1 (a int, 
b int, PRIMARY KEY (a));";
 
     CQLSchemaAccessor schemaAccessor;
 
     @BeforeEach
     void setUp()
     {
-        CQLSessionProvider sessionProvider = 
mockCQLSessionProvider(KEYSPACE.name(), TABLE.name(), SCHEMA);
+        CQLSessionProvider sessionProvider = 
mockCQLSessionProvider(KEYSPACE.name(), TABLE.name(), TABLE_SCHEMA);
         schemaAccessor = new CQLSchemaAccessor(sessionProvider);
     }
 
@@ -62,7 +64,7 @@ class CQLSchemaAccessorTest
     @Test
     void testGetExistingTable()
     {
-        assertThat(schemaAccessor.getTableSchema(KEYSPACE, 
TABLE)).containsExactly(SCHEMA);
+        assertThat(schemaAccessor.getTableSchema(KEYSPACE, 
TABLE)).containsExactly(TABLE_SCHEMA);
     }
 
     @Test
@@ -71,7 +73,26 @@ class CQLSchemaAccessorTest
         assertThat(schemaAccessor.getTableSchema(KEYSPACE, new 
Name("unknown"))).isNull();
     }
 
-    static CQLSessionProvider mockCQLSessionProvider(String keyspace, String 
table, String schema)
+    @Test
+    void testGetExistingKeyspace()
+    {
+        assertThat(schemaAccessor.getKeyspaceSchema(KEYSPACE))
+        .containsExactly(createKeyspaceStatement(KEYSPACE.name()), 
TABLE_SCHEMA);
+    }
+
+    @Test
+    void testGetNotExistingKeyspace()
+    {
+        assertThat(schemaAccessor.getKeyspaceSchema(new 
Name("unknown"))).isNull();
+    }
+
+    @Test
+    void testGetKeyspaceWithoutSchema()
+    {
+        assertThat(schemaAccessor.getKeyspaceSchema(new 
Name(VIRTUAL_KEYSPACE))).isEmpty();
+    }
+
+    static CQLSessionProvider mockCQLSessionProvider(String keyspace, String 
table, String tableSchema)
     {
         Session session = mock(Session.class, RETURNS_DEEP_STUBS);
 
@@ -87,17 +108,44 @@ class CQLSchemaAccessorTest
         String describeTable = String.format("DESCRIBE TABLE %s.%s", keyspace, 
table);
         when(session.execute(eq(describeTable))).then(invocation -> {
             ResultSet resultSet = mock(ResultSet.class);
-            Row row = mockRow(Map.of("create_statement", schema));
+            Row row = mockRow(Map.of("create_statement", tableSchema));
             when(resultSet.all()).thenReturn(List.of(row));
             return resultSet;
         });
 
+        when(session.execute(startsWith("DESCRIBE KEYSPACE "))).thenThrow(new 
InvalidQueryException("Unknown keyspace"));
+
+        String describeKeyspace = String.format("DESCRIBE KEYSPACE %s", 
keyspace);
+        when(session.execute(eq(describeKeyspace))).then(invocation -> {
+            ResultSet resultSet = mock(ResultSet.class);
+            doReturn(List.of(mockRow(Map.of("create_statement", 
createKeyspaceStatement(keyspace))),
+                             mockRow(Map.of("create_statement", 
tableSchema)))).when(resultSet).all();
+            return resultSet;
+        });
+
+        when(session.execute(eq(String.format("DESCRIBE KEYSPACE %s", 
VIRTUAL_KEYSPACE)))).then(invocation -> {
+            ResultSet resultSet = mock(ResultSet.class);
+            when(resultSet.all()).thenReturn(List.of());
+            return resultSet;
+        });
+
         CQLSessionProvider cqlSession = mock(CQLSessionProvider.class);
         when(cqlSession.get()).thenReturn(session);
 
         return cqlSession;
     }
 
+    static String createKeyspaceStatement(String keyspace)
+    {
+        return String.format("CREATE KEYSPACE %s WITH replication = {'class': 
'SimpleStrategy',"
+                             + " 'replication_factor': '1'}  AND 
durable_writes = true;", keyspace);
+    }
+
+    static String keyspaceSchema(String keyspace, String schema)
+    {
+        return createKeyspaceStatement(keyspace) + "\n\n" + schema;
+    }
+
     static Row mockRow(Map<String, String> values)
     {
         Row row = mock(Row.class);
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/db/DriverUnsupportedSchemaCacheTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/db/DriverUnsupportedSchemaCacheTest.java
deleted file mode 100644
index 0c9de1ebe..000000000
--- 
a/server/src/test/java/org/apache/cassandra/sidecar/db/DriverUnsupportedSchemaCacheTest.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * 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.db;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.TimeUnit;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import com.datastax.driver.core.BoundStatement;
-import com.datastax.driver.core.PreparedStatement;
-import com.datastax.driver.core.ResultSet;
-import com.datastax.driver.core.Row;
-import com.datastax.driver.core.Session;
-import io.vertx.core.Future;
-import io.vertx.core.Promise;
-import org.apache.cassandra.sidecar.common.server.CQLSessionProvider;
-import org.apache.cassandra.sidecar.common.server.data.Name;
-import 
org.apache.cassandra.sidecar.common.server.utils.SecondBoundConfiguration;
-import org.apache.cassandra.sidecar.config.DriverConfiguration;
-import org.apache.cassandra.sidecar.config.SidecarConfiguration;
-import org.apache.cassandra.sidecar.exceptions.CassandraUnavailableException;
-
-import static org.apache.cassandra.sidecar.db.CQLSchemaAccessorTest.mockRow;
-import static 
org.apache.cassandra.sidecar.exceptions.CassandraUnavailableException.Service.CQL;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-class DriverUnsupportedSchemaCacheTest
-{
-    static final Name KEYSPACE = new Name("keyspace1");
-    static final Name TABLE = new Name("table1");
-    static final String SCHEMA = "CREATE TABLE keyspace1.table1 (a int, b 
vector<float, 3>, PRIMARY KEY (a));";
-
-    CQLSessionProvider sessionProvider;
-    Session session;
-    DriverUnsupportedSchemaCache schemaCache;
-
-    @BeforeEach
-    void setUp()
-    {
-        sessionProvider = 
CQLSchemaAccessorTest.mockCQLSessionProvider(KEYSPACE.name(), TABLE.name(), 
SCHEMA);
-        session = sessionProvider.get();
-        mockPreparedQuery(session,
-                          "SELECT keyspace_name, table_name FROM 
system_schema.tables",
-                          List.of(Map.of("keyspace_name", KEYSPACE.name(), 
"table_name", TABLE.name())));
-        SidecarConfiguration sidecarConfiguration = mockSidecarConfiguration();
-        schemaCache = new DriverUnsupportedSchemaCache(sidecarConfiguration, 
sessionProvider);
-    }
-
-    @Test
-    void testImmediateSchemaLookup()
-    {
-        assertThat(schemaCache.getFullSchema()).isEqualTo(SCHEMA);
-        assertThat(schemaCache.getKeyspaceSchema(KEYSPACE)).isEqualTo(SCHEMA);
-        assertThat(schemaCache.getTableSchema(KEYSPACE, 
TABLE)).isEqualTo(SCHEMA);
-
-        assertThat(schemaCache.getTableSchema(new Name("unknown"), 
TABLE)).isNull();
-        assertThat(schemaCache.getTableSchema(KEYSPACE, new 
Name("unknown"))).isNull();
-    }
-
-    @Test
-    void testSchemaLookupAfterRefresh()
-    {
-        // mark cache as initialized, but effectively empty
-        schemaCache.setInitialized(true);
-
-        assertThat(schemaCache.getFullSchema()).isEqualTo("");
-        assertThat(schemaCache.getKeyspaceSchema(KEYSPACE)).isEqualTo("");
-
-        Promise<Void> p = Promise.promise();
-        schemaCache.execute(p);
-        Future<Void> future = p.future();
-        assertThat(future.succeeded()).isTrue();
-
-        // simulate Cassandra unavailability to verify data is taken from cache
-        when(sessionProvider.get()).thenThrow(new 
CassandraUnavailableException(CQL, "CQL unavailable"));
-
-        assertThat(schemaCache.getFullSchema()).isEqualTo(SCHEMA);
-        assertThat(schemaCache.getKeyspaceSchema(KEYSPACE)).isEqualTo(SCHEMA);
-        assertThat(schemaCache.getTableSchema(KEYSPACE, 
TABLE)).isEqualTo(SCHEMA);
-    }
-
-    static SidecarConfiguration mockSidecarConfiguration()
-    {
-        SidecarConfiguration sidecarConfiguration = 
mock(SidecarConfiguration.class);
-        DriverConfiguration driverConfiguration = 
mock(DriverConfiguration.class);
-        
when(driverConfiguration.unsupportedTableSchemaRefreshTime()).thenReturn(new 
SecondBoundConfiguration(5, TimeUnit.SECONDS));
-        
when(sidecarConfiguration.driverConfiguration()).thenReturn(driverConfiguration);
-        return sidecarConfiguration;
-    }
-
-    static void mockPreparedQuery(Session session, String statement, 
List<Map<String, String>> rows)
-    {
-        PreparedStatement preparedStatement = mock(PreparedStatement.class);
-        BoundStatement boundStatement = mock(BoundStatement.class);
-        when(session.prepare(eq(statement))).thenReturn(preparedStatement);
-        when(preparedStatement.bind()).thenReturn(boundStatement);
-        when(session.execute(eq(boundStatement))).then(invocation -> {
-            ResultSet resultSet = mock(ResultSet.class);
-            List<Row> mockRows = new ArrayList<>();
-            rows.forEach(row -> mockRows.add(mockRow(row)));
-            when(resultSet.all()).thenReturn(mockRows);
-            return resultSet;
-        });
-    }
-}
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/db/SchemaCacheTest.java 
b/server/src/test/java/org/apache/cassandra/sidecar/db/SchemaCacheTest.java
new file mode 100644
index 000000000..fbed45d5d
--- /dev/null
+++ b/server/src/test/java/org/apache/cassandra/sidecar/db/SchemaCacheTest.java
@@ -0,0 +1,361 @@
+/*
+ * 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.db;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.datastax.driver.core.BoundStatement;
+import com.datastax.driver.core.Cluster;
+import com.datastax.driver.core.KeyspaceMetadata;
+import com.datastax.driver.core.Metadata;
+import com.datastax.driver.core.PreparedStatement;
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.Row;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.TableMetadata;
+import io.vertx.core.Future;
+import io.vertx.core.Promise;
+import org.apache.cassandra.sidecar.common.server.CQLSessionProvider;
+import org.apache.cassandra.sidecar.common.server.data.Name;
+import 
org.apache.cassandra.sidecar.common.server.utils.SecondBoundConfiguration;
+import org.apache.cassandra.sidecar.config.DriverConfiguration;
+import org.apache.cassandra.sidecar.config.SidecarConfiguration;
+import org.apache.cassandra.sidecar.exceptions.CassandraUnavailableException;
+
+import static 
org.apache.cassandra.sidecar.db.CQLSchemaAccessorTest.VIRTUAL_KEYSPACE;
+import static 
org.apache.cassandra.sidecar.db.CQLSchemaAccessorTest.createKeyspaceStatement;
+import static 
org.apache.cassandra.sidecar.db.CQLSchemaAccessorTest.keyspaceSchema;
+import static org.apache.cassandra.sidecar.db.CQLSchemaAccessorTest.mockRow;
+import static 
org.apache.cassandra.sidecar.exceptions.CassandraUnavailableException.Service.CQL;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class SchemaCacheTest
+{
+    static final Name KEYSPACE = new Name("keyspace1");
+    static final Name TABLE = new Name("table1");
+    static final String TABLE_LIST_QUERY = "SELECT keyspace_name, table_name 
FROM system_schema.tables";
+    // a table the Java driver cannot parse, because of the vector data type
+    static final String SCHEMA = "CREATE TABLE keyspace1.table1 (a int, b 
vector<float, 3>, PRIMARY KEY (a));";
+    static final String KEYSPACE_SCHEMA = keyspaceSchema(KEYSPACE.name(), 
SCHEMA);
+
+    CQLSessionProvider sessionProvider;
+    Session session;
+    SchemaCache schemaCache;
+    List<Map<String, String>> tableRows;
+    List<Map<String, String>> keyspaceRows;
+
+    @BeforeEach
+    void setUp()
+    {
+        sessionProvider = 
CQLSchemaAccessorTest.mockCQLSessionProvider(KEYSPACE.name(), TABLE.name(), 
SCHEMA);
+        session = sessionProvider.get();
+        tableRows = new ArrayList<>();
+        tableRows.add(Map.of("keyspace_name", KEYSPACE.name(), "table_name", 
TABLE.name()));
+        mockPreparedQuery(session, TABLE_LIST_QUERY, tableRows);
+        keyspaceRows = new ArrayList<>();
+        keyspaceRows.add(Map.of("keyspace_name", KEYSPACE.name()));
+        mockQuery(session, "DESCRIBE KEYSPACES", keyspaceRows);
+        SidecarConfiguration sidecarConfiguration = mockSidecarConfiguration();
+        schemaCache = new SchemaCache(sidecarConfiguration, sessionProvider);
+    }
+
+    @Test
+    void testImmediateUnsupportedTableSchemaLookup()
+    {
+        assertThat(schemaCache.getUnsupportedTableSchema()).isEqualTo(SCHEMA);
+        assertThat(schemaCache.getUnsupportedTableSchema(KEYSPACE, 
TABLE)).isEqualTo(SCHEMA);
+
+        assertThat(schemaCache.getUnsupportedTableSchema(new Name("unknown"), 
TABLE)).isNull();
+        assertThat(schemaCache.getUnsupportedTableSchema(KEYSPACE, new 
Name("unknown"))).isNull();
+    }
+
+    @Test
+    void testUnsupportedTableSchemaLookupWithoutRefresh()
+    {
+        assertThat(schemaCache.getUnsupportedTableSchema(KEYSPACE, TABLE, 
false)).isEqualTo(SCHEMA);
+
+        // for a table created after the last refresh
+        Name table = new Name("table2");
+        assertThat(schemaCache.getUnsupportedTableSchema(KEYSPACE, table, 
false)).isNull();
+        verify(session, never()).execute("DESCRIBE TABLE keyspace1.table2");
+
+        mockDescribeTable(session, KEYSPACE.name(), table.name(), "CREATE 
TABLE keyspace1.table2 (a int PRIMARY KEY);");
+        assertThat(schemaCache.getUnsupportedTableSchema(KEYSPACE, table, 
true))
+        .isEqualTo("CREATE TABLE keyspace1.table2 (a int PRIMARY KEY);");
+        verify(session, times(1)).execute("DESCRIBE TABLE keyspace1.table2");
+    }
+
+    @Test
+    void testUnsupportedTableSchemaLookupForUnquotedNames()
+    {
+        assertThat(schemaCache.getUnsupportedTableSchema(new Name("Keyspace1"),
+                                                         new Name("TABLE1"),
+                                                         false))
+        .isEqualTo(SCHEMA);
+
+        assertThat(schemaCache.getUnsupportedTableSchema(new 
Name("\"Keyspace1\""),
+                                                         new 
Name("\"Table1\""),
+                                                         false))
+        .isNull();
+
+        assertThat(schemaCache.getUnsupportedTableSchema(new 
Name("\"keyspace1\""),
+                                                         new 
Name("\"table1\""),
+                                                         false))
+        .isEqualTo(SCHEMA);
+    }
+
+    @Test
+    void testUnsupportedTableSchemaLookupAfterRefresh()
+    {
+        schemaCache.setInitialized(true);
+
+        assertThat(schemaCache.getUnsupportedTableSchema()).isEqualTo("");
+
+        Promise<Void> p = Promise.promise();
+        schemaCache.execute(p);
+        Future<Void> future = p.future();
+        assertThat(future.succeeded()).isTrue();
+
+        when(sessionProvider.get()).thenThrow(new 
CassandraUnavailableException(CQL, "CQL unavailable"));
+
+        assertThat(schemaCache.getUnsupportedTableSchema()).isEqualTo(SCHEMA);
+        assertThat(schemaCache.getUnsupportedTableSchema(KEYSPACE, 
TABLE)).isEqualTo(SCHEMA);
+    }
+
+    @Test
+    void testUnsupportedTableSchemaIsAssembledInStableOrder()
+    {
+        String otherSchema = "CREATE TABLE keyspace1.table0 (a int, b 
vector<float, 3>, PRIMARY KEY (a));";
+        mockDescribeTable(session, KEYSPACE.name(), "table0", otherSchema);
+        tableRows.add(Map.of("keyspace_name", KEYSPACE.name(), "table_name", 
"table0"));
+
+        
assertThat(schemaCache.getUnsupportedTableSchema()).isEqualTo(otherSchema + 
"\n\n" + SCHEMA);
+        schemaCache.refresh(false);
+        
assertThat(schemaCache.getUnsupportedTableSchema()).isEqualTo(otherSchema + 
"\n\n" + SCHEMA);
+    }
+
+    @Test
+    void testDroppedTableIsRemovedOnRefresh()
+    {
+        assertThat(schemaCache.getUnsupportedTableSchema()).isEqualTo(SCHEMA);
+        tableRows.clear();
+        schemaCache.refresh(false);
+        assertThat(schemaCache.getUnsupportedTableSchema()).isEqualTo("");
+        assertThat(schemaCache.getUnsupportedTableSchema(KEYSPACE, TABLE, 
false)).isNull();
+    }
+
+    @Test
+    void testTableKnownToDriverIsNotCached()
+    {
+        Cluster mockCluster = mock(Cluster.class);
+        when(session.getCluster()).thenReturn(mockCluster);
+        Metadata mockMetadata = mock(Metadata.class);
+        when(mockCluster.getMetadata()).thenReturn(mockMetadata);
+        doReturn(List.of(mockKeyspaceMetadata(KEYSPACE.name(), 
TABLE.name()))).when(mockMetadata).getKeyspaces();
+
+        assertThat(schemaCache.getUnsupportedTableSchema()).isEqualTo("");
+        assertThat(schemaCache.getUnsupportedTableSchema(KEYSPACE, TABLE, 
false)).isNull();
+    }
+
+    @Test
+    void testImmediateDescribeSchemaLookup()
+    {
+        assertThat(schemaCache.getFullSchema()).isEqualTo(KEYSPACE_SCHEMA);
+        
assertThat(schemaCache.getKeyspaceSchema(KEYSPACE.name())).isEqualTo(KEYSPACE_SCHEMA);
+        assertThat(schemaCache.getKeyspaceSchema("unknown")).isNull();
+
+        verify(session, times(1)).execute("DESCRIBE KEYSPACE " + 
KEYSPACE.name());
+    }
+
+    @Test
+    void testDescribeSchemaLookupOnDemand()
+    {
+        schemaCache.setInitialized(true);
+        assertThat(schemaCache.getFullSchema()).isEqualTo("");
+
+        
assertThat(schemaCache.getKeyspaceSchema(KEYSPACE.name())).isEqualTo(KEYSPACE_SCHEMA);
+        
assertThat(schemaCache.getKeyspaceSchema(KEYSPACE.name())).isEqualTo(KEYSPACE_SCHEMA);
+        verify(session, times(1)).execute("DESCRIBE KEYSPACE " + 
KEYSPACE.name());
+
+        when(sessionProvider.get()).thenThrow(new 
CassandraUnavailableException(CQL, "CQL unavailable"));
+
+        assertThat(schemaCache.getFullSchema()).isEqualTo(KEYSPACE_SCHEMA);
+        
assertThat(schemaCache.getKeyspaceSchema(KEYSPACE.name())).isEqualTo(KEYSPACE_SCHEMA);
+    }
+
+    @Test
+    void testDescribeSchemaOfAllKeyspacesIsInvalidatedOnDemandLookup()
+    {
+        assertThat(schemaCache.getFullSchema()).isEqualTo(KEYSPACE_SCHEMA);
+
+        String otherSchema = "CREATE TABLE keyspace2.table1 (a int PRIMARY 
KEY);";
+        mockDescribeKeyspace(session, "keyspace2", otherSchema);
+
+        
assertThat(schemaCache.getKeyspaceSchema("keyspace2")).isEqualTo(keyspaceSchema("keyspace2",
 otherSchema));
+        assertThat(schemaCache.getFullSchema())
+        .isEqualTo(KEYSPACE_SCHEMA + "\n\n" + keyspaceSchema("keyspace2", 
otherSchema));
+    }
+
+    @Test
+    void testDescribeSchemaIsAssembledInStableOrder()
+    {
+        String otherSchema = "CREATE TABLE aaa_keyspace.table1 (a int PRIMARY 
KEY);";
+        mockDescribeKeyspace(session, "aaa_keyspace", otherSchema);
+        keyspaceRows.add(Map.of("keyspace_name", "aaa_keyspace"));
+
+        assertThat(schemaCache.getFullSchema())
+        .isEqualTo(keyspaceSchema("aaa_keyspace", otherSchema) + "\n\n" + 
KEYSPACE_SCHEMA);
+        schemaCache.refresh(false);
+        assertThat(schemaCache.getFullSchema())
+        .isEqualTo(keyspaceSchema("aaa_keyspace", otherSchema) + "\n\n" + 
KEYSPACE_SCHEMA);
+    }
+
+    @Test
+    void testDroppedKeyspaceIsRemovedOnRefresh()
+    {
+        assertThat(schemaCache.getFullSchema()).isEqualTo(KEYSPACE_SCHEMA);
+
+        keyspaceRows.clear();
+        schemaCache.refresh(false);
+
+        assertThat(schemaCache.getFullSchema()).isEqualTo("");
+    }
+
+    @Test
+    void testKeyspaceWithoutSchemaIsNotCached()
+    {
+        keyspaceRows.add(Map.of("keyspace_name", VIRTUAL_KEYSPACE));
+
+        assertThat(schemaCache.getFullSchema()).isEqualTo(KEYSPACE_SCHEMA);
+        assertThat(schemaCache.getKeyspaceSchema(VIRTUAL_KEYSPACE)).isNull();
+    }
+
+    @Test
+    void testLookupWhileCassandraIsUnavailable()
+    {
+        CQLSessionProvider unavailable = mock(CQLSessionProvider.class);
+        when(unavailable.get()).thenThrow(new 
CassandraUnavailableException(CQL, "CQL unavailable"));
+        SchemaCache cache = new SchemaCache(mockSidecarConfiguration(), 
unavailable);
+
+        assertThat(cache.getFullSchema()).isEqualTo("");
+        assertThat(cache.getUnsupportedTableSchema()).isEqualTo("");
+
+        assertThatThrownBy(() -> cache.getKeyspaceSchema(KEYSPACE.name()))
+        .isInstanceOf(CassandraUnavailableException.class);
+        assertThatThrownBy(() -> cache.getUnsupportedTableSchema(KEYSPACE, 
TABLE))
+        .isInstanceOf(CassandraUnavailableException.class);
+
+        assertThat(cache.getUnsupportedTableSchema(KEYSPACE, TABLE, 
false)).isNull();
+    }
+
+    @Test
+    void testExecuteFailsWhenRefreshFails()
+    {
+        CQLSessionProvider failing = mock(CQLSessionProvider.class);
+        when(failing.get()).thenThrow(new RuntimeException("cannot get 
session"));
+        SchemaCache cache = new SchemaCache(mockSidecarConfiguration(), 
failing);
+
+        Promise<Void> p = Promise.promise();
+        cache.execute(p);
+        Future<Void> future = p.future();
+        assertThat(future.failed()).isTrue();
+        
assertThat(future.cause()).isInstanceOf(RuntimeException.class).hasMessage("cannot
 get session");
+    }
+
+    @Test
+    void testConcatSchemas()
+    {
+        assertThat(SchemaCache.concatSchemas()).isEqualTo("");
+        assertThat(SchemaCache.concatSchemas("", null)).isEqualTo("");
+        assertThat(SchemaCache.concatSchemas(SCHEMA)).isEqualTo(SCHEMA);
+        assertThat(SchemaCache.concatSchemas(KEYSPACE_SCHEMA, null, "", "\n" + 
SCHEMA + "\n"))
+        .isEqualTo(KEYSPACE_SCHEMA + "\n\n" + SCHEMA);
+    }
+
+    private SidecarConfiguration mockSidecarConfiguration()
+    {
+        SidecarConfiguration sidecarConfiguration = 
mock(SidecarConfiguration.class);
+        DriverConfiguration driverConfiguration = 
mock(DriverConfiguration.class);
+        when(driverConfiguration.schemaRefreshTime()).thenReturn(new 
SecondBoundConfiguration(5, TimeUnit.SECONDS));
+        
when(sidecarConfiguration.driverConfiguration()).thenReturn(driverConfiguration);
+        return sidecarConfiguration;
+    }
+
+    private void mockPreparedQuery(Session session, String statement, 
List<Map<String, String>> rows)
+    {
+        PreparedStatement preparedStatement = mock(PreparedStatement.class);
+        BoundStatement boundStatement = mock(BoundStatement.class);
+        when(session.prepare(eq(statement))).thenReturn(preparedStatement);
+        when(preparedStatement.bind()).thenReturn(boundStatement);
+        when(session.execute(eq(boundStatement))).then(invocation -> 
mockResultSet(rows));
+    }
+
+    private void mockDescribeKeyspace(Session session, String keyspace, String 
schema)
+    {
+        mockQuery(session,
+                  "DESCRIBE KEYSPACE " + keyspace,
+                  List.of(Map.of("create_statement", 
createKeyspaceStatement(keyspace)),
+                          Map.of("create_statement", schema)));
+    }
+
+    private void mockDescribeTable(Session session, String keyspace, String 
table, String schema)
+    {
+        mockQuery(session,
+                  String.format("DESCRIBE TABLE %s.%s", keyspace, table),
+                  List.of(Map.of("create_statement", schema)));
+    }
+
+    private void mockQuery(Session session, String statement, List<Map<String, 
String>> rows)
+    {
+        when(session.execute(eq(statement))).then(invocation -> 
mockResultSet(rows));
+    }
+
+    private KeyspaceMetadata mockKeyspaceMetadata(String keyspace, String 
table)
+    {
+        TableMetadata tableMetadata = mock(TableMetadata.class);
+        when(tableMetadata.getName()).thenReturn(table);
+        KeyspaceMetadata keyspaceMetadata = mock(KeyspaceMetadata.class);
+        when(keyspaceMetadata.getName()).thenReturn(keyspace);
+        when(keyspaceMetadata.getTables()).thenReturn(List.of(tableMetadata));
+        return keyspaceMetadata;
+    }
+
+    private ResultSet mockResultSet(List<Map<String, String>> rows)
+    {
+        ResultSet resultSet = mock(ResultSet.class);
+        List<Row> mockRows = new ArrayList<>();
+        rows.forEach(row -> mockRows.add(mockRow(row)));
+        when(resultSet.all()).thenReturn(mockRows);
+        return resultSet;
+    }
+}
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/handlers/SchemaHandlerTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/handlers/SchemaHandlerTest.java
index 37ebd3e50..b4a3115f6 100644
--- 
a/server/src/test/java/org/apache/cassandra/sidecar/handlers/SchemaHandlerTest.java
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/handlers/SchemaHandlerTest.java
@@ -53,14 +53,14 @@ import 
org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.cluster.InstancesMetadata;
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
 import org.apache.cassandra.sidecar.common.server.utils.IOUtils;
-import org.apache.cassandra.sidecar.db.DriverUnsupportedSchemaCache;
+import org.apache.cassandra.sidecar.db.SchemaCache;
 import org.apache.cassandra.sidecar.modules.SidecarModules;
 import org.apache.cassandra.sidecar.server.Server;
 
 import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
 import static io.netty.handler.codec.http.HttpResponseStatus.OK;
+import static 
io.netty.handler.codec.http.HttpResponseStatus.SERVICE_UNAVAILABLE;
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -71,17 +71,22 @@ import static org.mockito.Mockito.when;
 class SchemaHandlerTest
 {
     static final Logger LOGGER = 
LoggerFactory.getLogger(SchemaHandlerTest.class);
+    static final String TEST_KEYSPACE = "testKeyspace";
     Vertx vertx;
     Server server;
     @TempDir
     File instanceDir;
     String testKeyspaceSchema;
+    SchemaCache mockSchemaCache;
 
     @BeforeEach
     void before() throws InterruptedException, IOException
     {
         ClassLoader cl = getClass().getClassLoader();
         testKeyspaceSchema = 
IOUtils.readFully(cl.getResourceAsStream("schema/test_keyspace_schema.cql"));
+        mockSchemaCache = mock(SchemaCache.class);
+        when(mockSchemaCache.getFullSchema()).thenReturn("MOCK SCHEMA");
+        
when(mockSchemaCache.getKeyspaceSchema(TEST_KEYSPACE)).thenReturn(testKeyspaceSchema);
 
         Injector injector;
         injector = Guice.createInjector(Modules.override(SidecarModules.all())
@@ -119,7 +124,22 @@ class SchemaHandlerTest
                   JsonObject jsonObject = response.bodyAsJsonObject();
                   assertThat(jsonObject.getString("keyspace")).isNull();
                   assertThat(jsonObject.getString("schema"))
-                  .isEqualTo("FULL SCHEMA");
+                  .isEqualTo("MOCK SCHEMA");
+                  context.completeNow();
+              })));
+    }
+
+    @Test
+    void testSchemaNotAvailable(VertxTestContext context)
+    {
+        when(mockSchemaCache.getFullSchema()).thenReturn("");
+
+        WebClient client = WebClient.create(vertx);
+        String testRoute = "/api/v1/schema/keyspaces";
+        client.get(server.actualPort(), "127.0.0.1", testRoute)
+              .expect(ResponsePredicate.SC_SERVICE_UNAVAILABLE)
+              .send(context.succeeding(response -> context.verify(() -> {
+                  
assertThat(response.statusCode()).isEqualTo(SERVICE_UNAVAILABLE.code());
                   context.completeNow();
               })));
     }
@@ -134,9 +154,24 @@ class SchemaHandlerTest
               .send(context.succeeding(response -> context.verify(() -> {
                   assertThat(response.statusCode()).isEqualTo(OK.code());
                   JsonObject jsonObject = response.bodyAsJsonObject();
-                  
assertThat(jsonObject.getString("keyspace")).isEqualTo("testKeyspace");
+                  
assertThat(jsonObject.getString("keyspace")).isEqualTo(TEST_KEYSPACE);
                   assertThat(jsonObject.getString("schema"))
-                  .isEqualTo(testKeyspaceSchema.trim());
+                  .isEqualTo(testKeyspaceSchema);
+                  context.completeNow();
+              })));
+    }
+
+    @Test
+    void testKeyspaceSchemaNotAvailable(VertxTestContext context)
+    {
+        
when(mockSchemaCache.getKeyspaceSchema(TEST_KEYSPACE)).thenReturn(null);
+
+        WebClient client = WebClient.create(vertx);
+        String testRoute = "/api/v1/schema/keyspaces/testKeyspace";
+        client.get(server.actualPort(), "127.0.0.1", testRoute)
+              .expect(ResponsePredicate.SC_NOT_FOUND)
+              .send(context.succeeding(response -> context.verify(() -> {
+                  
assertThat(response.statusCode()).isEqualTo(NOT_FOUND.code());
                   context.completeNow();
               })));
     }
@@ -175,9 +210,8 @@ class SchemaHandlerTest
             
when(instanceMetadata.delegate()).thenReturn(mockCassandraAdapterDelegate);
             Metadata mockMetadata = mock(Metadata.class);
             KeyspaceMetadata mockKeyspaceMetadata = 
mock(KeyspaceMetadata.class);
-            when(mockMetadata.exportSchemaAsString()).thenReturn("FULL 
SCHEMA");
-            
when(mockMetadata.getKeyspace("testKeyspace")).thenReturn(mockKeyspaceMetadata);
-            
when(mockKeyspaceMetadata.exportAsString()).thenReturn(testKeyspaceSchema);
+            
when(mockMetadata.getKeyspace(TEST_KEYSPACE)).thenReturn(mockKeyspaceMetadata);
+            when(mockKeyspaceMetadata.getName()).thenReturn(TEST_KEYSPACE);
 
             
when(mockCassandraAdapterDelegate.metadata()).thenReturn(mockMetadata);
 
@@ -191,12 +225,9 @@ class SchemaHandlerTest
 
         @Provides
         @Singleton
-        public DriverUnsupportedSchemaCache driverUnsupportedSchemaCache()
+        public SchemaCache schemaCache()
         {
-            DriverUnsupportedSchemaCache schemaAccessor = 
mock(DriverUnsupportedSchemaCache.class);
-            when(schemaAccessor.getFullSchema()).thenReturn("");
-            when(schemaAccessor.getKeyspaceSchema(any())).thenReturn("");
-            return schemaAccessor;
+            return mockSchemaCache;
         }
     }
 }
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/tasks/CassandraClusterSchemaMonitorTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/tasks/CassandraClusterSchemaMonitorTest.java
index e225efc6d..242641e1f 100644
--- 
a/server/src/test/java/org/apache/cassandra/sidecar/tasks/CassandraClusterSchemaMonitorTest.java
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/tasks/CassandraClusterSchemaMonitorTest.java
@@ -41,7 +41,7 @@ import 
org.apache.cassandra.sidecar.config.SchemaKeyspaceConfiguration;
 import org.apache.cassandra.sidecar.config.ServiceConfiguration;
 import org.apache.cassandra.sidecar.config.SidecarConfiguration;
 import org.apache.cassandra.sidecar.db.CdcDatabaseAccessor;
-import org.apache.cassandra.sidecar.db.DriverUnsupportedSchemaCache;
+import org.apache.cassandra.sidecar.db.SchemaCache;
 import org.apache.cassandra.sidecar.utils.CdcUtil;
 import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
 import org.apache.cassandra.spark.data.CqlTable;
@@ -72,7 +72,7 @@ class CassandraClusterSchemaMonitorTest
     private CassandraClusterSchemaMonitor clusterSchema;
     private InstanceMetadataFetcher mockInstanceFetcher;
     private CdcDatabaseAccessor mockDatabaseAccessor;
-    private DriverUnsupportedSchemaCache mockDriverUnsupportedSchemaCache;
+    private SchemaCache mockSchemaCache;
     private SidecarConfiguration mockSidecarConfiguration;
     private ServiceConfiguration mockServiceConfiguration;
     private CdcConfiguration mockCdcConfiguration;
@@ -102,7 +102,7 @@ class CassandraClusterSchemaMonitorTest
     {
         mockInstanceFetcher = mock(InstanceMetadataFetcher.class);
         mockDatabaseAccessor = mock(CdcDatabaseAccessor.class);
-        mockDriverUnsupportedSchemaCache = 
mock(DriverUnsupportedSchemaCache.class);
+        mockSchemaCache = mock(SchemaCache.class);
         mockSidecarConfiguration = mock(SidecarConfiguration.class);
         mockServiceConfiguration = mock(ServiceConfiguration.class);
         mockCdcConfiguration = mock(CdcConfiguration.class);
@@ -131,7 +131,7 @@ class CassandraClusterSchemaMonitorTest
 
         // Setup database accessor
         when(mockDatabaseAccessor.fullSchema()).thenReturn(INITIAL_SCHEMA);
-        when(mockDriverUnsupportedSchemaCache.getFullSchema()).thenReturn("");
+        when(mockSchemaCache.getUnsupportedTableSchema()).thenReturn("");
         
when(mockDatabaseAccessor.partitioner()).thenReturn(Partitioner.Murmur3Partitioner);
         
when(mockDatabaseAccessor.getTableId(any(TableIdentifier.class))).thenReturn(UUID.randomUUID());
         mockCdcBridgeFactory.when(() -> 
CdcBridgeFactory.getCdcBridge(any(CassandraBridge.class))).thenReturn(mockCdcBridge);
@@ -139,7 +139,7 @@ class CassandraClusterSchemaMonitorTest
         clusterSchema = new CassandraClusterSchemaMonitor(
         mockInstanceFetcher,
         mockDatabaseAccessor,
-        mockDriverUnsupportedSchemaCache,
+        mockSchemaCache,
         mockSidecarConfiguration,
         () -> mockCassandraBridge
         );
@@ -238,7 +238,6 @@ class CassandraClusterSchemaMonitorTest
 
             // Verify initial schema processing
             verify(mockDatabaseAccessor, times(1)).fullSchema();
-            verify(mockDriverUnsupportedSchemaCache, times(1)).getFullSchema();
             verify(mockCdcBridge, 
times(1)).updateCdcSchema(tablesCaptor.capture(), 
eq(Partitioner.Murmur3Partitioner), any());
             assertThat(tablesCaptor.getValue()).hasSize(1);
 
@@ -247,7 +246,6 @@ class CassandraClusterSchemaMonitorTest
 
             // Verify schema change detection and update
             verify(mockDatabaseAccessor, times(2)).fullSchema();
-            verify(mockDriverUnsupportedSchemaCache, times(2)).getFullSchema();
             verify(mockCdcBridge, 
times(2)).updateCdcSchema(tablesCaptor.capture(), 
eq(Partitioner.Murmur3Partitioner), any());
             assertThat(tablesCaptor.getValue()).hasSize(2);
         }
@@ -322,7 +320,7 @@ class CassandraClusterSchemaMonitorTest
             // Same table set every refresh — nothing ever becomes stale.
             when(mockDatabaseAccessor.fullSchema())
             .thenReturn(INITIAL_SCHEMA)
-            .thenReturn(INITIAL_SCHEMA + " ");  // trailing space forces a 
"schema changed" refresh without changing the table set
+            .thenReturn(INITIAL_SCHEMA + " -- changed");  // forces a "schema 
changed" refresh without changing the table set
 
             clusterSchema.refresh();
             clusterSchema.refresh();
@@ -346,10 +344,9 @@ class CassandraClusterSchemaMonitorTest
             String updatedCdcTableStmt = "CREATE TABLE test.cdc_table (id text 
PRIMARY KEY, data text) WITH cdc = true;";
             String updatedMismatchedSchema = updatedCdcTableStmt + 
nonCdcTableStmt;
             // Third refresh: schema "changes" again but the table set is 
identical — appending
-            // a non-whitespace comment-like suffix (whitespace alone would be 
stripped by
-            // DriverUnsupportedSchemaCache.concatSchemas()'s trim(), never 
triggering a
-            // "schema changed" refresh at all) is enough to force refresh() 
to re-run its
-            // schema-changed branch and retry unregistration.
+            // a non-whitespace comment-like suffix (whitespace alone is 
trimmed when SchemaCache
+            // assembles the full schema, never triggering a "schema changed" 
refresh at all) is
+            // enough to force refresh() to re-run its schema-changed branch 
and retry unregistration.
             String thirdRefreshSchema = updatedMismatchedSchema + " -- 
refresh3";
 
             Map<TableIdentifier, CdcUtil.TableSchema> initialTables = Map.of(
@@ -474,7 +471,7 @@ class CassandraClusterSchemaMonitorTest
     void testRefreshHandlesGenericExceptionGracefully()
     {
 
-        when(mockDatabaseAccessor.fullSchema()).thenThrow(new 
RuntimeException("Unexpected error"));
+        when(mockDatabaseAccessor.fullSchema()).thenThrow(new 
IllegalStateException("Unexpected error"));
 
         // Should now throw the exception instead of handling it gracefully
         try


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to