diff --git a/.python-version b/.python-version new file mode 100644 index 00000000000..2419ad5b0a3 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11.9 diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml index 5f3a935be3c..b2851d9021c 100644 --- a/conf/cassandra.yaml +++ b/conf/cassandra.yaml @@ -2531,6 +2531,10 @@ drop_compact_storage_enabled: false # This would also apply to system keyspaces. # maximum_replication_factor_warn_threshold: -1 # maximum_replication_factor_fail_threshold: -1 +# +# When true, allows the usage of misprepared statements, only warns in the logs. +# When false, misprepared statements will result in an error to the client. Default is true. +# misprepared_statements_enabled: true #role_name_policy: # # Implementation class of a validator. When not in form of FQCN, the diff --git a/src/java/org/apache/cassandra/config/Config.java b/src/java/org/apache/cassandra/config/Config.java index 2f290d8182e..4ef0208a93d 100644 --- a/src/java/org/apache/cassandra/config/Config.java +++ b/src/java/org/apache/cassandra/config/Config.java @@ -937,6 +937,7 @@ public static void setClientMode(boolean clientMode) public volatile boolean user_timestamps_enabled = true; public volatile boolean alter_table_enabled = true; public volatile boolean group_by_enabled = true; + public volatile boolean misprepared_statements_enabled = true; public volatile boolean bulk_load_enabled = true; public volatile boolean drop_truncate_table_enabled = true; public volatile boolean drop_keyspace_enabled = true; diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java index 809a44fdcc6..5b01d716376 100644 --- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java +++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java @@ -5483,6 +5483,15 @@ public static void setUseStatementsEnabled(boolean enabled) } } + public static boolean getMispreparedStatementsEnabled() + { + return conf.misprepared_statements_enabled; + } + + public static void setMispreparedStatementsEnabled(boolean enabled) + { + conf.misprepared_statements_enabled = enabled; + } public static AccordSpec getAccord() { diff --git a/src/java/org/apache/cassandra/config/GuardrailsOptions.java b/src/java/org/apache/cassandra/config/GuardrailsOptions.java index 7443565d89e..bf9c9348052 100644 --- a/src/java/org/apache/cassandra/config/GuardrailsOptions.java +++ b/src/java/org/apache/cassandra/config/GuardrailsOptions.java @@ -1329,6 +1329,21 @@ public boolean getVectorTypeEnabled() return config.vector_type_enabled; } + @Override + public boolean getMispreparedStatementsEnabled() + { + return config.misprepared_statements_enabled; + } + + + public void setMispreparedStatementsEnabled(boolean enabled) + { + updatePropertyWithLogging("misprepare_statements_enabled", + enabled, + () -> config.misprepared_statements_enabled, + x -> config.misprepared_statements_enabled = x); + } + @Override public void setVectorTypeEnabled(boolean enabled) { diff --git a/src/java/org/apache/cassandra/cql3/QueryProcessor.java b/src/java/org/apache/cassandra/cql3/QueryProcessor.java index 606da473056..1af40d0d487 100644 --- a/src/java/org/apache/cassandra/cql3/QueryProcessor.java +++ b/src/java/org/apache/cassandra/cql3/QueryProcessor.java @@ -130,6 +130,7 @@ public class QueryProcessor implements QueryHandler // counters. Callers of processStatement are responsible for correctly notifying metrics public static final CQLMetrics metrics = new CQLMetrics(); + // Paging size to use when preloading prepared statements. public static final int PRELOAD_PREPARED_STATEMENTS_FETCH_SIZE = 5000; @@ -494,7 +495,6 @@ public static Prepared parseAndPrepare(String query, ClientState clientState, bo // Note: if 2 threads prepare the same query, we'll live so don't bother synchronizing CQLStatement statement = raw.prepare(clientState); statement.validate(clientState); - // Set CQL string for AlterSchemaStatement as this is used to serialize the transformation // in the cluster metadata log if (statement instanceof AlterSchemaStatement) diff --git a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java index 118f2c1fa44..c08e861e8ee 100644 --- a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java @@ -409,7 +409,21 @@ public void validate(ClientState state) throws InvalidRequestException checkFalse(isVirtual() && hasConditions(), "Conditional updates are not supported by virtual tables"); if (attrs.isTimestampSet()) + { Guardrails.userTimestampsEnabled.ensureEnabled(state); + } + // Misprepare guardrail shouldn't block system keyspaces + if (SchemaConstants.isSystemKeyspace(metadata.keyspace)) + return; + + boolean hasRestriction = restrictions != null && + restrictions.hasPartitionKeyRestrictions() || + restrictions.hasClusteringColumnsRestrictions() || + restrictions.hasNonPrimaryKeyRestrictions(); + if (getBindVariables().isEmpty() && hasRestriction) + { + Guardrails.onMisprepared(state); + } } public void validateDiskUsage(QueryOptions options, ClientState state) diff --git a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java index 1752fe0bc45..562831dddf3 100644 --- a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java @@ -339,8 +339,19 @@ public void authorize(ClientState state) throws InvalidRequestException, Unautho public void validate(ClientState state) throws InvalidRequestException { - if (parameters.allowFiltering && !SchemaConstants.isSystemKeyspace(table.keyspace)) + if (SchemaConstants.isSystemKeyspace(table.keyspace)) + return; + if (parameters.allowFiltering) Guardrails.allowFilteringEnabled.ensureEnabled(state); + boolean hasRestrictions = restrictions != null && + (restrictions.hasPartitionKeyRestrictions() || + restrictions.hasClusteringColumnsRestrictions() || + restrictions.hasNonPrimaryKeyRestrictions()); + if (getBindVariables().isEmpty() + && hasRestrictions) + { + Guardrails.onMisprepared(state); + } } @Override diff --git a/src/java/org/apache/cassandra/db/guardrails/Guardrails.java b/src/java/org/apache/cassandra/db/guardrails/Guardrails.java index 6738d9a7d95..f2424b84874 100644 --- a/src/java/org/apache/cassandra/db/guardrails/Guardrails.java +++ b/src/java/org/apache/cassandra/db/guardrails/Guardrails.java @@ -54,6 +54,9 @@ public final class Guardrails implements GuardrailsMBean public static final String MBEAN_NAME = "org.apache.cassandra.db:type=Guardrails"; public static final GuardrailsConfigProvider CONFIG_PROVIDER = GuardrailsConfigProvider.instance; + + public static final String MISPREPARED_STATEMENT_WARN_MESSAGE = "Performance Tip: This query contains literal values in the WHERE clause. " + "Using '?' placeholders (bind markers) is much more efficient. " + "It allows the server to cache this query once and reuse it for different values, " + "reducing memory usage and improving speed."; + private static final GuardrailsOptions DEFAULT_CONFIG = DatabaseDescriptor.getGuardrailsConfig(); @VisibleForTesting @@ -613,6 +616,19 @@ public final class Guardrails implements GuardrailsMBean format("The keyspace %s has a replication factor of %s, above the %s threshold of %s.", what, value, isWarning ? "warning" : "failure", threshold)); + /** + * Prevents heap exhaustion caused by the anti-pattern of preparing queries with + * hardcoded literals instead of bind markers. This prevents filling the statement cache with non-reusable entries. + * + * @see CASSANDRA-21139 + */ + + public static final EnableFlag mispreparedStatementsEnabled = + new EnableFlag("misprepared_statements_enabled", + "Mis-prepared statements cause server-side memory exhaustion and high GC pressure by flooding the statement cache with non-reusable query entries", + state -> CONFIG_PROVIDER.getOrCreate(state).getMispreparedStatementsEnabled(), + "Mis-prepared statements"); + public static final MaxThreshold maximumAllowableTimestamp = new MaxThreshold("maximum_timestamp", "Timestamps too far in the future can lead to data that can't be easily overwritten", @@ -705,6 +721,18 @@ public void setKeyspacesThreshold(int warn, int fail) DEFAULT_CONFIG.setKeyspacesThreshold(warn, fail); } + @Override + public boolean getMispreparedStatementsEnabled() + { + return DEFAULT_CONFIG.getMispreparedStatementsEnabled(); + } + + @Override + public void setMispreparedStatementsEnabled(boolean enabled) + { + DEFAULT_CONFIG.setMispreparedStatementsEnabled(enabled); + } + @Override public int getTablesWarnThreshold() { @@ -1886,4 +1914,15 @@ private static long minimumTimestampAsRelativeMicros(@Nullable DurationSpec.Long ? Long.MIN_VALUE : (ClientState.getLastTimestampMicros() - duration.toMicroseconds()); } + + public static void onMisprepared(ClientState state) + { + mispreparedStatementsEnabled.ensureEnabled(state); + + // only warn if user ins't super user or a external call + if (!state.isInternal && !state.isSuper()) + { + mispreparedStatementsEnabled.warn(MISPREPARED_STATEMENT_WARN_MESSAGE); + } + } } diff --git a/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java b/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java index 8e0d0202597..e91442959f6 100644 --- a/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java +++ b/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java @@ -406,6 +406,40 @@ public interface GuardrailsConfig */ boolean getVectorTypeEnabled(); + /** + *

+ * A statement is considered "mis-prepared" if it contains hardcoded literal values + * instead of bind markers. This is a performance anti-pattern because it prevents + * query plan reuse and floods the server-side Prepared Statement Cache with + * unique entries, leading to heap exhaustion and high GC pressure. + *

+ * LWT and Batch Considerations: + * This check applies to both the {@code WHERE} clause and the {@code IF} conditions + * in Lightweight Transactions (LWT). For example, the following is considered + * mis-prepared because of the hardcoded literals: + *

{@code
+     * UPDATE users SET description = 'v2' WHERE id = 1 AND name = 'v1' IF description = 'v0'
+     * }
+ * To be compliant, it should use bind markers ({@code ?}): + *
{@code
+     * UPDATE users SET description = ? WHERE id = ? AND name = ? IF description = ?
+     * }
+ * For {@code BATCH} statements, if any individual statement within the batch is + * identified as mis-prepared, the entire batch triggers this guardrail. + + * + * @return true if the usage of mis-prepared statements is enabled, false otherwise. Returns true by default. + * {@code false} if mis-prepared statements should be strictly rejected, causing the query to fail. + * @see CASSANDRA-21139 + */ + + boolean getMispreparedStatementsEnabled(); + + /** + * sets whether misprepared statements is enabled + */ + void setMispreparedStatementsEnabled(boolean enabled); + /** * Sets whether new columns can be created with vector type * @@ -509,6 +543,8 @@ public interface GuardrailsConfig void setIntersectFilteringQueryEnabled(boolean value); + + /** * @return A timestamp that if a user supplied timestamp is after will trigger a warning */ diff --git a/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java b/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java index e2e9cd74ddb..31fb8d865de 100644 --- a/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java +++ b/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java @@ -54,6 +54,13 @@ public interface GuardrailsMBean */ void setKeyspacesThreshold(int warn, int fail); + /** + * @return the use of misprepared statement is enabled + */ + boolean getMispreparedStatementsEnabled(); + + void setMispreparedStatementsEnabled(boolean enabled); + /** * @return The threshold to warn when creating more tables than threshold. * -1 means disabled. diff --git a/src/java/org/apache/cassandra/service/StorageService.java b/src/java/org/apache/cassandra/service/StorageService.java index dc9d9e3f680..d53836f626e 100644 --- a/src/java/org/apache/cassandra/service/StorageService.java +++ b/src/java/org/apache/cassandra/service/StorageService.java @@ -4836,6 +4836,19 @@ public void setColumnIndexCacheSize(int cacheSizeInKB) logger.info("Updated column_index_cache_size to {}", cacheSizeInKB); } + @Override + public boolean getMispreparedStatementsEnabled() + { + return DatabaseDescriptor.getMispreparedStatementsEnabled(); + } + + @Override + public void setMispreparedStatementsEnabled(boolean enabled) + { + DatabaseDescriptor.setMispreparedStatementsEnabled(enabled); + logger.info("Updated misprepared_statements_enabled to {}", enabled); + } + /* * In CASSANDRA-17668, JMX setters that did not throw standard exceptions were deprecated in favor of ones that do. * For consistency purposes, the respective getter "getColumnIndexCacheSize" was also deprecated and replaced by diff --git a/src/java/org/apache/cassandra/service/StorageServiceMBean.java b/src/java/org/apache/cassandra/service/StorageServiceMBean.java index 9c4ae481ec1..09db793ad84 100644 --- a/src/java/org/apache/cassandra/service/StorageServiceMBean.java +++ b/src/java/org/apache/cassandra/service/StorageServiceMBean.java @@ -1097,6 +1097,16 @@ default int upgradeSSTables(String keyspaceName, boolean excludeCurrentVersion, @Deprecated(since = "5.0") public void setColumnIndexCacheSize(int cacheSizeInKB); + /** + * Returns whether use of misprepared statements is enabled + */ + public boolean getMispreparedStatementsEnabled(); + + /** + * Sets whether use of misprepared statements is enabled + */ + public void setMispreparedStatementsEnabled(boolean enabled); + /** Returns the threshold for skipping the column index when caching partition info **/ public int getColumnIndexCacheSizeInKiB(); diff --git a/test/unit/org/apache/cassandra/config/DatabaseDescriptorTest.java b/test/unit/org/apache/cassandra/config/DatabaseDescriptorTest.java index 678f6746f1d..88d583d57f5 100644 --- a/test/unit/org/apache/cassandra/config/DatabaseDescriptorTest.java +++ b/test/unit/org/apache/cassandra/config/DatabaseDescriptorTest.java @@ -781,6 +781,27 @@ public void testRowIndexSizeWarnEqAbort() DatabaseDescriptor.applyReadThresholdsValidations(conf); } + @Test + public void testMispreparedStatementsEnabled() { + boolean originalValue = DatabaseDescriptor.getMispreparedStatementsEnabled(); + try + { + DatabaseDescriptor.setMispreparedStatementsEnabled(true); + Assert.assertTrue("Value should be true after setting to true", + DatabaseDescriptor.getMispreparedStatementsEnabled()); + + DatabaseDescriptor.setMispreparedStatementsEnabled(false); + Assert.assertFalse("Value should be false after setting to false", + DatabaseDescriptor.getMispreparedStatementsEnabled()); + + Assert.assertTrue("Default value of misprepared_statement_enabled must be true", originalValue); + } + finally + { + DatabaseDescriptor.setMispreparedStatementsEnabled(originalValue); + } + } + @Test public void testRowIndexSizeWarnEnabledAbortDisabled() { diff --git a/test/unit/org/apache/cassandra/cql3/MispreparedStatementsTest.java b/test/unit/org/apache/cassandra/cql3/MispreparedStatementsTest.java new file mode 100644 index 00000000000..2cb437e5dca --- /dev/null +++ b/test/unit/org/apache/cassandra/cql3/MispreparedStatementsTest.java @@ -0,0 +1,391 @@ +/* + * 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.cql3; + +import java.net.InetSocketAddress; +import java.util.List; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.auth.AuthenticatedUser; +import org.apache.cassandra.auth.CassandraAuthorizer; +import org.apache.cassandra.auth.PasswordAuthenticator; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.guardrails.GuardrailViolatedException; +import org.apache.cassandra.db.guardrails.Guardrails; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.service.ClientWarn; + +import static org.junit.Assert.assertTrue; + +public class MispreparedStatementsTest extends CQLTester +{ + private static final ClientState state = ClientState.forExternalCalls(new InetSocketAddress("127.0.0.1", 9042)); + + @BeforeClass + public static void setupGlobalConfig() + { + DatabaseDescriptor.setAuthenticator(new PasswordAuthenticator()); + DatabaseDescriptor.setAuthorizer(new CassandraAuthorizer()); + DatabaseDescriptor.setMispreparedStatementsEnabled(false); + } + + @Before + public void setUp() + { + AuthenticatedUser nonSuperUser = new AuthenticatedUser("regular-user") + { + @Override + public boolean isSuper() + { + return false; + } + + @Override + public boolean isSystem() + { + return false; + } + + @Override + public boolean isAnonymous() + { + return false; + } + + @Override + public boolean canLogin() + { + return true; + } + }; + + state.login(nonSuperUser); + state.setKeyspace(KEYSPACE); + ClientWarn.instance.captureWarnings(); + createTable("CREATE TABLE %s (id int, description text, age int, name text, PRIMARY KEY (id, name, age))"); + } + + @After + public void tearDown() + { + DatabaseDescriptor.setMispreparedStatementsEnabled(false); + } + + @Test + public void testSelectWithPartitionKey() + { + assertGuardrailViolated(String.format("SELECT * FROM %s WHERE id = 1", currentTable())); + assertNoWarnings(); + } + + @Test + public void testSelectWithClusteringKey() + { + assertGuardrailViolated(String.format("SELECT * FROM %s WHERE name = 'v1' ALLOW FILTERING", currentTable())); + assertNoWarnings(); + } + + @Test + public void testSelectWithFullPrimaryKey() + { + assertGuardrailViolated(String.format("SELECT * FROM %s WHERE id = 1 AND name = 'v1'", currentTable())); + assertNoWarnings(); + } + + @Test + public void testSelectWithWhereNonPrimaryKeyColumn() + { + assertGuardrailViolated(String.format("SELECT * FROM %s WHERE description = 'foo' ALLOW FILTERING", currentTable())); + assertNoWarnings(); + } + + + @Test + public void testSelectWithCompositeRestriction() + { + assertGuardrailViolated(String.format("SELECT sum(id) from %s WHERE (name, age) = ('a', 1) ALLOW FILTERING", currentTable())); + } + + @Test + public void testSelectWithFunction() + { + assertGuardrailViolated(String.format("SELECT sum(id) from %s where name = 'v1' ALLOW FILTERING", currentTable())); + } + + @Test + public void testSelectWithRangeRestriction() + { + assertGuardrailViolated(String.format("SELECT * FROM %s WHERE id = 1 AND name > 'a'", currentTable())); + assertNoWarnings(); + } + + @Test + public void testSelectInRestrictionOnPartitionKey() + { + assertGuardrailViolated(String.format("SELECT * FROM %s WHERE id IN (1, 2, 3)", currentTable())); + assertNoWarnings(); + } + + @Test + public void testSelectInRestrictionOnClusteringKey() + { + assertGuardrailViolated(String.format("SELECT * FROM %s WHERE name IN ('a', 'b') ALLOW FILTERING", currentTable())); + assertNoWarnings(); + } + + @Test + public void testSelectInRestrictionOnFullPrimaryKey() + { + assertGuardrailViolated(String.format("SELECT * FROM %s WHERE id IN (1, 2, 3) AND name in ('a', 'b')", currentTable())); + assertNoWarnings(); + } + + @Test + public void testDDLStatementsBypass() + { + assertGuardrailPassed("CREATE TABLE IF NOT EXISTS test_table (id INT PRIMARY KEY)"); + assertNoWarnings(); + } + + @Test + public void testWhereLiteralWithLike() + { + assertGuardrailViolated(String.format("SELECT * FROM %s WHERE name LIKE 'prefix%%' ALLOW FILTERING", currentTable())); + assertNoWarnings(); + } + + @Test + public void testInsertJsonGuardrail() + { + assertGuardrailViolated(String.format("INSERT INTO %s JSON '{\"id\": 1, \"name\": \"v1\"}'", currentTable())); + assertNoWarnings(); + } + + @Test + public void testUpdateWithPartitionKey() + { + assertGuardrailViolated(String.format("UPDATE %s SET description = 'new' WHERE id = 1 AND name = 'name' AND age = 1", KEYSPACE + '.' + currentTable())); + assertNoWarnings(); + } + + @Test + public void testUpdateWithIfCondition() + { + assertGuardrailViolated(String.format("UPDATE %s SET description = 'v2' WHERE id = 1 AND name = 'v1' AND age = 1 IF description = 'v0'", currentTable())); + assertNoWarnings(); + } + + @Test + public void testDeleteWithFullPrimaryKey() + { + assertGuardrailViolated(String.format("DELETE FROM %s WHERE id = 1 AND name = 'v1'", currentTable())); + assertNoWarnings(); + } + + @Test + public void testMultiTableBatchGuardrailAllLiteral() + { + String table1 = currentTable(); + String table2 = createTable("CREATE TABLE %s (id int PRIMARY KEY, val text)"); + String query = String.format("BEGIN BATCH " + + "UPDATE %s.%s SET description = 'v1' WHERE id = 1 AND name = 'n1' AND age = 1; " + + "INSERT INTO %s.%s (id, val) VALUES (2, 'v2'); " + + "APPLY BATCH", + KEYSPACE, table1, KEYSPACE, table2); + + assertGuardrailViolated(query); + assertNoWarnings(); + } + + @Test + public void testProperlyPreparedWithBindMarkers() + { + assertGuardrailPassed(String.format("SELECT * FROM %s WHERE id = ? AND name = ?", currentTable())); + assertNoWarnings(); + } + + @Test + public void testSelectAllPasses() + { + assertGuardrailPassed(String.format("SELECT * FROM %s", currentTable())); + assertNoWarnings(); + } + + @Test + public void testLiteralInProjectionIsAllowed() + { + assertGuardrailPassed(String.format("SELECT id, (text)'const_val' FROM %s WHERE id = ?", currentTable())); + assertNoWarnings(); + } + + @Test + public void testInternalBypass() + { + assertGuardrailPassed("SELECT * FROM " + KEYSPACE + '.' + currentTable() + " WHERE id = 1", ClientState.forInternalCalls()); + assertNoWarnings(); + } + + @Test + public void testSuperUserBypass() + { + AuthenticatedUser superUser = new AuthenticatedUser("super-user") + { + @Override + public boolean isSuper() + { + return true; + } + + @Override + public boolean isSystem() + { + return false; + } + + @Override + public boolean isAnonymous() + { + return false; + } + + @Override + public boolean canLogin() + { + return true; + } + }; + ClientState superUserState = ClientState.forExternalCalls(new InetSocketAddress("127.0.0.1", 9042)); + superUserState.login(superUser); + assertGuardrailPassed("SELECT * FROM " + KEYSPACE + '.' + currentTable() + " WHERE id = 1", superUserState); + assertNoWarnings(); + } + + @Test + public void testSystemKeyspaceBypassForRegularUser() + { + assertGuardrailPassed("SELECT * FROM system.local WHERE key = 'local'"); + assertNoWarnings(); + } + + @Test + public void testGuardrailDisabledAllowsLiterals() + { + DatabaseDescriptor.setMispreparedStatementsEnabled(true); + assertGuardrailPassed(String.format("SELECT * FROM %s WHERE id = 1", currentTable())); + assertWarnings(); + } + + @Test + public void testWarningIsIssuedWhenGuardrailIsAllowed() + { + DatabaseDescriptor.setMispreparedStatementsEnabled(true); + ClientWarn.instance.captureWarnings(); + assertGuardrailPassed(String.format("SELECT * FROM %s WHERE id = 1", currentTable())); + assertWarnings(); + + } + + @Test + public void testGuardrailDisabledAllowsBatchLiterals() + { + DatabaseDescriptor.setMispreparedStatementsEnabled(true); + String tableName = currentTable(); + assertGuardrailPassed(String.format("BEGIN BATCH " + + "INSERT INTO %s.%s (id, description, name, age) VALUES (1, 'v1', 'v1', 1); " + + "UPDATE %s.%s SET description = 'v2' WHERE id = 2 AND name = 'v1' AND age = 1; " + + "APPLY BATCH", KEYSPACE, tableName, KEYSPACE, tableName)); + assertWarnings(); + } + + @Test + public void testBlockedStatementsDoNotEnterCache() + { + QueryProcessor.clearPreparedStatementsCache(); + int initialCacheSize = QueryProcessor.preparedStatementsCount(); + for (int i = 0; i < 10; i++) + { + String query = String.format("SELECT * FROM %s WHERE id = %d", currentTable(), i); + try + { + QueryProcessor.instance.prepare(query, state); + } + catch (GuardrailViolatedException e) + { + // Expected: Guardrail throws exception + } + } + Assert.assertEquals("Blocked misprepared statements should not be added to the cache", initialCacheSize, QueryProcessor.preparedStatementsCount()); + } + + private void assertGuardrailViolated(String query) + { + try + { + QueryProcessor.instance.prepare(query, state); + Assert.fail("Expected GuardrailViolatedException for query: " + query); + } + catch (GuardrailViolatedException e) + { + assertTrue(e.getMessage().contains("Mis-prepared statements is not allowed")); + } + catch (Exception e) + { + Assert.fail(e.getMessage()); + } + } + + private void assertNoWarnings() + { + List warnings = ClientWarn.instance.getWarnings(); + if (warnings != null) + { + assertTrue("Expected performance tip warning was found", + warnings.stream().noneMatch(w -> w.contains(Guardrails.MISPREPARED_STATEMENT_WARN_MESSAGE))); + } + } + + private void assertWarnings() + { + List warnings = ClientWarn.instance.getWarnings(); + assertTrue("Expected performance tip warning was not found", + warnings.stream().anyMatch(w -> w.contains(Guardrails.MISPREPARED_STATEMENT_WARN_MESSAGE))); + } + + private void assertGuardrailPassed(String query) + { + assertGuardrailPassed(query, state); + } + + private void assertGuardrailPassed(String query, ClientState clientState) + { + try + { + QueryProcessor.instance.prepare(query, clientState); + } + catch (Exception e) + { + Assert.fail("Expected guardrail to pass, but got: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/test/unit/org/apache/cassandra/tools/nodetool/GuardrailsConfigCommandsTest.java b/test/unit/org/apache/cassandra/tools/nodetool/GuardrailsConfigCommandsTest.java index 37958178b7b..1aade56a000 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/GuardrailsConfigCommandsTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/GuardrailsConfigCommandsTest.java @@ -208,6 +208,7 @@ private Set getConfigFieldNames() "group_by_enabled true\n" + "intersect_filtering_query_enabled true\n" + "intersect_filtering_query_warned true\n" + + "misprepared_statements_enabled true\n" + "non_partition_restricted_index_query_enabled true\n" + "read_before_write_list_operations_enabled true\n" + "secondary_indexes_enabled true\n" +