Skip to content

Commit bff43df

Browse files
committed
Merge branch 'cassandra-4.0' into cassandra-4.1
* cassandra-4.0: Prevent too long table names not fitting file names
2 parents 04d56d4 + 6c29686 commit bff43df

File tree

12 files changed

+223
-91
lines changed

12 files changed

+223
-91
lines changed

CHANGES.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* IntrusiveStack.accumulate is not accumulating correctly (CASSANDRA-20670)
1010
* Add nodetool get/setguardrailsconfig commands (CASSANDRA-19552)
1111
Merged from 4.0:
12+
* Prevent too long table names not fitting file names (CASSANDRA-20389)
1213
* Fix IndexOutOfBoundsException in sstablemetadata tool when a range tombstone is a max clustering value (CASSANDRA-20855)
1314
* Update Jackson to 2.19.2 (CASSANDRA-20848)
1415
* Update commons-lang3 to 3.18.0 (CASSANDRA-20849)
@@ -7841,5 +7842,3 @@ Full list of issues resolved in 0.4 is at https://issues.apache.org/jira/secure/
78417842
* Combined blocking and non-blocking versions of insert APIs
78427843
* Added FlushPeriodInMinutes configuration parameter to force
78437844
flushing of infrequently-updated ColumnFamilies
7844-
7845-

src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
import org.apache.cassandra.transport.Event.SchemaChange;
3737
import org.apache.cassandra.transport.messages.ResultMessage;
3838

39+
import static org.apache.cassandra.schema.KeyspaceMetadata.validateKeyspaceName;
40+
3941
abstract public class AlterSchemaStatement implements CQLStatement.SingleKeyspaceCqlStatement, SchemaTransformation
4042
{
4143
protected final String keyspaceName; // name of the keyspace affected by the statement
@@ -109,7 +111,7 @@ public ResultMessage execute(QueryState state, boolean locally)
109111
if (null != keyspace && keyspace.isVirtual())
110112
throw ire("Virtual keyspace '%s' is not user-modifiable", keyspaceName);
111113

112-
validateKeyspaceName();
114+
validateKeyspaceName(keyspaceName, AlterSchemaStatement::ire);
113115

114116
SchemaTransformationResult result = Schema.instance.transform(this, locally);
115117

@@ -132,16 +134,6 @@ public ResultMessage execute(QueryState state, boolean locally)
132134
return new ResultMessage.SchemaChange(schemaChangeEvent(result.diff));
133135
}
134136

135-
private void validateKeyspaceName()
136-
{
137-
if (!SchemaConstants.isValidName(keyspaceName))
138-
{
139-
throw ire("Keyspace name must not be empty, more than %d characters long, " +
140-
"or contain non-alphanumeric-underscore characters (got '%s')",
141-
SchemaConstants.NAME_LENGTH, keyspaceName);
142-
}
143-
}
144-
145137
private void grantPermissionsOnResource(IResource resource, AuthenticatedUser user)
146138
{
147139
try

src/java/org/apache/cassandra/db/Directories.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ public class Directories
102102
public static final String SNAPSHOT_SUBDIR = "snapshots";
103103
public static final String TMP_SUBDIR = "tmp";
104104
public static final String SECONDARY_INDEX_NAME_SEPARATOR = ".";
105+
public static final String TABLE_DIRECTORY_NAME_SEPARATOR = "-";
105106

106107
/**
107108
* The directories used to store keyspaces data.
@@ -209,14 +210,11 @@ public Directories(final TableMetadata metadata, DataDirectory[] paths)
209210
this.metadata = metadata;
210211
this.paths = paths;
211212
ImmutableMap.Builder<Path, DataDirectory> canonicalPathsBuilder = ImmutableMap.builder();
212-
String tableId = metadata.id.toHexString();
213-
int idx = metadata.name.indexOf(SECONDARY_INDEX_NAME_SEPARATOR);
214-
String cfName = idx >= 0 ? metadata.name.substring(0, idx) : metadata.name;
215-
String indexNameWithDot = idx >= 0 ? metadata.name.substring(idx) : null;
213+
String indexNameWithDot = metadata.getIndexNameWithDot();
216214

217215
this.dataPaths = new File[paths.length];
218216
// If upgraded from version less than 2.1, use existing directories
219-
String oldSSTableRelativePath = join(metadata.keyspace, cfName);
217+
String oldSSTableRelativePath = join(metadata.keyspace, metadata.getTableName());
220218
for (int i = 0; i < paths.length; ++i)
221219
{
222220
// check if old SSTable directory exists
@@ -229,7 +227,7 @@ public Directories(final TableMetadata metadata, DataDirectory[] paths)
229227
{
230228
canonicalPathsBuilder = ImmutableMap.builder();
231229
// use 2.1+ style
232-
String newSSTableRelativePath = join(metadata.keyspace, cfName + '-' + tableId);
230+
String newSSTableRelativePath = join(metadata.keyspace, metadata.getTableDirectoryName());
233231
for (int i = 0; i < paths.length; ++i)
234232
{
235233
File dataPath = new File(paths[i].location, newSSTableRelativePath);

src/java/org/apache/cassandra/schema/IndexMetadata.java

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import java.io.IOException;
2222
import java.lang.reflect.InvocationTargetException;
2323
import java.util.*;
24-
import java.util.regex.Pattern;
2524
import java.util.stream.Collectors;
2625

2726
import com.google.common.base.Objects;
@@ -42,17 +41,16 @@
4241
import org.apache.cassandra.utils.FBUtilities;
4342
import org.apache.cassandra.utils.UUIDSerializer;
4443

44+
import static org.apache.cassandra.schema.SchemaConstants.PATTERN_NON_WORD_CHAR;
45+
import static org.apache.cassandra.schema.SchemaConstants.isValidCharsName;
46+
4547
/**
4648
* An immutable representation of secondary index metadata.
4749
*/
4850
public final class IndexMetadata
4951
{
5052
private static final Logger logger = LoggerFactory.getLogger(IndexMetadata.class);
5153

52-
private static final Pattern PATTERN_NON_WORD_CHAR = Pattern.compile("\\W");
53-
private static final Pattern PATTERN_WORD_CHARS = Pattern.compile("\\w+");
54-
55-
5654
public static final Serializer serializer = new Serializer();
5755

5856
public enum Kind
@@ -94,11 +92,6 @@ public static IndexMetadata fromIndexTargets(List<IndexTarget> targets,
9492
return new IndexMetadata(name, newOptions, kind);
9593
}
9694

97-
public static boolean isNameValid(String name)
98-
{
99-
return name != null && !name.isEmpty() && PATTERN_WORD_CHARS.matcher(name).matches();
100-
}
101-
10295
public static String generateDefaultIndexName(String table, ColumnIdentifier column)
10396
{
10497
return PATTERN_NON_WORD_CHAR.matcher(table + "_" + column.toString() + "_idx").replaceAll("");
@@ -111,7 +104,8 @@ public static String generateDefaultIndexName(String table)
111104

112105
public void validate(TableMetadata table)
113106
{
114-
if (!isNameValid(name))
107+
// TODO: address validating the length by CASSANDRA-20445
108+
if (!isValidCharsName(name))
115109
throw new ConfigurationException("Illegal index name " + name);
116110

117111
if (kind == null)

src/java/org/apache/cassandra/schema/KeyspaceMetadata.java

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.apache.cassandra.cql3.functions.UDFunction;
3535
import org.apache.cassandra.db.marshal.UserType;
3636
import org.apache.cassandra.exceptions.ConfigurationException;
37+
import org.apache.cassandra.exceptions.RequestValidationException;
3738
import org.apache.cassandra.locator.AbstractReplicationStrategy;
3839
import org.apache.cassandra.schema.Functions.FunctionsDiff;
3940
import org.apache.cassandra.schema.Tables.TablesDiff;
@@ -44,12 +45,30 @@
4445
import static java.lang.String.format;
4546

4647
import static com.google.common.collect.Iterables.any;
48+
import static org.apache.cassandra.schema.SchemaConstants.TABLE_NAME_LENGTH;
4749

4850
/**
4951
* An immutable representation of keyspace metadata (name, params, tables, types, and functions).
5052
*/
5153
public final class KeyspaceMetadata implements SchemaElement
5254
{
55+
/**
56+
* Validates the keyspace name for valid characters and correct length.
57+
* Throws an exception if it's invalid.
58+
*
59+
* @param keyspaceName The name of the keyspace to validate
60+
* @param exceptionBuilder The exception constructor to throw if validation fails
61+
*/
62+
public static <T extends RequestValidationException> void validateKeyspaceName(String keyspaceName, java.util.function.Function<String, T> exceptionBuilder)
63+
{
64+
if (!SchemaConstants.isValidCharsName(keyspaceName))
65+
throw exceptionBuilder.apply(format("Keyspace name must not be empty and must contain alphanumeric or underscore characters only (got \"%s\")",
66+
keyspaceName));
67+
if (keyspaceName.length() > SchemaConstants.NAME_LENGTH)
68+
throw exceptionBuilder.apply(format("Keyspace name must not be more than %d characters long (got %d characters for \"%s\")",
69+
TABLE_NAME_LENGTH, keyspaceName.length(), keyspaceName));
70+
}
71+
5372
public enum Kind
5473
{
5574
REGULAR, VIRTUAL
@@ -306,16 +325,8 @@ public String toCqlString(boolean withInternals, boolean ifNotExists)
306325

307326
public void validate()
308327
{
309-
if (!SchemaConstants.isValidName(name))
310-
{
311-
throw new ConfigurationException(format("Keyspace name must not be empty, more than %s characters long, "
312-
+ "or contain non-alphanumeric-underscore characters (got \"%s\")",
313-
SchemaConstants.NAME_LENGTH,
314-
name));
315-
}
316-
328+
validateKeyspaceName(name, ConfigurationException::new);
317329
params.validate(name, null);
318-
319330
tablesAndViews().forEach(TableMetadata::validate);
320331

321332
Set<String> indexNames = new HashSet<>();

src/java/org/apache/cassandra/schema/SchemaConstants.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,15 @@
2929

3030
import org.apache.cassandra.db.Digest;
3131

32+
import static org.apache.cassandra.db.Directories.TABLE_DIRECTORY_NAME_SEPARATOR;
33+
3234
/**
3335
* When adding new String keyspace names here, double check if it needs to be added to PartitionDenylist.canDenylistKeyspace
3436
*/
3537
public final class SchemaConstants
3638
{
3739
public static final Pattern PATTERN_WORD_CHARS = Pattern.compile("\\w+");
40+
public static final Pattern PATTERN_NON_WORD_CHAR = Pattern.compile("\\W");
3841

3942
public static final String SYSTEM_KEYSPACE_NAME = "system";
4043
public static final String SCHEMA_KEYSPACE_NAME = "system_schema";
@@ -66,14 +69,37 @@ public final class SchemaConstants
6669
*/
6770
public static final int NAME_LENGTH = 48;
6871

72+
/**
73+
* Longest acceptable file name. Longer names lead to too long file name error.
74+
*/
75+
public static final int FILENAME_LENGTH = 255;
76+
77+
/**
78+
* Length of a table uuid as a hex string.
79+
*/
80+
public static final int TABLE_UUID_AS_HEX_LENGTH = 32;
81+
82+
/**
83+
* Longest acceptable table name, so it can be used in a directory
84+
* name constructed with a suffix of a table id and a separator.
85+
*/
86+
public static final int TABLE_NAME_LENGTH = FILENAME_LENGTH - TABLE_UUID_AS_HEX_LENGTH - TABLE_DIRECTORY_NAME_SEPARATOR.length();
87+
6988
// 59adb24e-f3cd-3e02-97f0-5b395827453f
7089
public static final UUID emptyVersion;
7190

7291
public static final List<String> LEGACY_AUTH_TABLES = Arrays.asList("credentials", "users", "permissions");
7392

74-
public static boolean isValidName(String name)
93+
/**
94+
* Validates that a name is not empty and contains only alphanumeric characters or
95+
* underscore, so it can be used in file or directory names.
96+
*
97+
* @param name the name to check
98+
* @return whether the non-empty name contains only valid characters
99+
*/
100+
public static boolean isValidCharsName(String name)
75101
{
76-
return name != null && !name.isEmpty() && name.length() <= NAME_LENGTH && PATTERN_WORD_CHARS.matcher(name).matches();
102+
return name != null && !name.isEmpty() && PATTERN_WORD_CHARS.matcher(name).matches();
77103
}
78104

79105
static

src/java/org/apache/cassandra/schema/TableMetadata.java

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,12 @@
4949
import static java.lang.String.format;
5050
import static java.util.stream.Collectors.toList;
5151
import static java.util.stream.Collectors.toSet;
52-
import static org.apache.cassandra.schema.IndexMetadata.isNameValid;
52+
import static org.apache.cassandra.db.Directories.SECONDARY_INDEX_NAME_SEPARATOR;
53+
import static org.apache.cassandra.db.Directories.TABLE_DIRECTORY_NAME_SEPARATOR;
54+
import static org.apache.cassandra.schema.KeyspaceMetadata.validateKeyspaceName;
55+
import static org.apache.cassandra.schema.SchemaConstants.FILENAME_LENGTH;
56+
import static org.apache.cassandra.schema.SchemaConstants.TABLE_NAME_LENGTH;
57+
import static org.apache.cassandra.schema.SchemaConstants.isValidCharsName;
5358

5459
@Unmetered
5560
public class TableMetadata implements SchemaElement
@@ -416,11 +421,9 @@ public boolean hasStaticColumns()
416421

417422
public void validate()
418423
{
419-
if (!isNameValid(keyspace))
420-
except("Keyspace name must not be empty, more than %s characters long, or contain non-alphanumeric-underscore characters (got \"%s\")", SchemaConstants.NAME_LENGTH, keyspace);
424+
validateKeyspaceName(keyspace, this::prepareConfigurationException);
421425

422-
if (!isNameValid(name))
423-
except("Table name must not be empty, more than %s characters long, or contain non-alphanumeric-underscore characters (got \"%s\")", SchemaConstants.NAME_LENGTH, name);
426+
validateTableName();
424427

425428
params.validate();
426429

@@ -448,6 +451,17 @@ public void validate()
448451
indexes.validate(this);
449452
}
450453

454+
private void validateTableName()
455+
{
456+
if (!isValidCharsName(name))
457+
except("Table name must not be empty or not contain non-alphanumeric-underscore characters (got \"%s\")", name);
458+
459+
if (name.length() > TABLE_NAME_LENGTH)
460+
except("Table name must not be more than %d characters long (got %d characters for \"%s\")", TABLE_NAME_LENGTH, name.length(), name);
461+
462+
assert getTableDirectoryName().length() <= FILENAME_LENGTH : String.format("Generated directory name for a table of %d characters doesn't fit the max filename legnth of %s. This unexpectedly wasn't prevented by check of the table name length, %d, to fit %d characters (got table name \"%s\" and generated directory name \"%s\"", getTableDirectoryName().length(), FILENAME_LENGTH, name.length(), TABLE_NAME_LENGTH, name, getTableDirectoryName());
463+
}
464+
451465
/**
452466
* To support backward compatibility with thrift super columns in the C* 3.0+ storage engine, we encode said super
453467
* columns as a CQL {@code map<blob, blob>}. To ensure the name of this map did not conflict with any other user
@@ -544,7 +558,41 @@ public ClusteringComparator partitionKeyAsClusteringComparator()
544558
public String indexTableName(IndexMetadata info)
545559
{
546560
// TODO simplify this when info.index_name is guaranteed to be set
547-
return name + Directories.SECONDARY_INDEX_NAME_SEPARATOR + info.name;
561+
return name + SECONDARY_INDEX_NAME_SEPARATOR + info.name;
562+
}
563+
564+
/**
565+
* Returns the table part of the index table name or the entire table name
566+
* if not an index table.
567+
* @return table name part
568+
*/
569+
public String getTableName()
570+
{
571+
int idx = name.indexOf(SECONDARY_INDEX_NAME_SEPARATOR);
572+
return idx >= 0 ? name.substring(0, idx) : name;
573+
}
574+
575+
/**
576+
* Generates a directory name for the table by using table part of
577+
* the (index) table name and table id.
578+
* @return directory name
579+
*/
580+
public String getTableDirectoryName()
581+
{
582+
return getTableName() + TABLE_DIRECTORY_NAME_SEPARATOR + id.toHexString();
583+
}
584+
585+
/**
586+
* Returns the index name from the name of an index table
587+
* including the dot prefixing the index name, see {@link #indexTableName}.
588+
* If not an index table, returns null.
589+
* @return index name prefixed with dot prefix or null
590+
*/
591+
@Nullable
592+
public String getIndexNameWithDot()
593+
{
594+
int idx = name.indexOf(SECONDARY_INDEX_NAME_SEPARATOR);
595+
return idx >= 0 ? name.substring(idx) : null;
548596
}
549597

550598
/**
@@ -602,9 +650,14 @@ public TableMetadata withUpdatedUserType(UserType udt)
602650
return builder.build();
603651
}
604652

653+
private ConfigurationException prepareConfigurationException(String format, Object... args)
654+
{
655+
return new ConfigurationException(keyspace + '.' + name + ": " + format(format, args));
656+
}
657+
605658
protected void except(String format, Object... args)
606659
{
607-
throw new ConfigurationException(keyspace + "." + name + ": " + format(format, args));
660+
throw prepareConfigurationException(format, args);
608661
}
609662

610663
@Override

test/unit/org/apache/cassandra/cql3/CQLTester.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import com.google.common.collect.ImmutableSet;
4848
import com.google.common.collect.Iterables;
4949

50+
import org.apache.commons.lang3.ArrayUtils;
5051
import org.junit.*;
5152

5253
import org.slf4j.Logger;
@@ -663,6 +664,21 @@ public void flush(String keyspace)
663664
Util.flush(store);
664665
}
665666

667+
public void flush(String keyspace, String table1, String... tables)
668+
{
669+
tables = ArrayUtils.add(tables, table1);
670+
for (ColumnFamilyStore store : getTables(keyspace, tables))
671+
Util.flush(store);
672+
}
673+
674+
private List<ColumnFamilyStore> getTables(String keyspace, String[] tables)
675+
{
676+
List<ColumnFamilyStore> stores = new ArrayList<>(tables.length);
677+
for (String name : tables)
678+
stores.add(getColumnFamilyStore(keyspace, name));
679+
return stores;
680+
}
681+
666682
public void disableCompaction(String keyspace)
667683
{
668684
ColumnFamilyStore store = getCurrentColumnFamilyStore(keyspace);

0 commit comments

Comments
 (0)