Skip to content

Commit 6c29686

Browse files
k-rusnetudima
authored andcommitted
Prevent too long table names not fitting file names
The length of table names was not controlled. This is likely due to confusion between validation methods with similar names. As result creating tables with too long names led to the too long file name exceptions during table creations. This commit adds a validation of table name lengths to avoid the too long file name errors. The validation length is based on how the table name is used to create file/directory names, and needs to be exact to prevent the too long file name exception, but allow all other table names, which didn't lead to the too long file name exception. This length limit is different from the existing name length limit of 48 characters used by common validation functions. Thus, this commit moves out the length validation from the validation methods into a separate length validation method, so the errors on names are more specific. The non-length validation methods combined into a single method, which checks for empty names and valid characters. New constants are added for the length limits. Table name related code are moved into methods in TableMetadata class, so their semantics are more clear and to allow reuse, e.g., in asserting the table name length constant. Tests are added for the long table names and non-alphanumeric names. Keyspace name validation function is now shared between two classes and a unit test of it is added. Patch by Ruslan Fomkin; reviewed by Piotr Kołaczkowski, Dmitry Konstantinov, Maxwell Guo for CASSANDRA-20389
1 parent 4484f2b commit 6c29686

File tree

12 files changed

+216
-89
lines changed

12 files changed

+216
-89
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
4.0.19
2+
* Prevent too long table names not fitting file names (CASSANDRA-20389)
23
* Fix IndexOutOfBoundsException in sstablemetadata tool when a range tombstone is a max clustering value (CASSANDRA-20855)
34
* Update Jackson to 2.19.2 (CASSANDRA-20848)
45
* Update commons-lang3 to 3.18.0 (CASSANDRA-20849)

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

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

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

106-
validateKeyspaceName();
108+
validateKeyspaceName(keyspaceName, AlterSchemaStatement::ire);
107109

108110
KeyspacesDiff diff = MigrationManager.announce(this, locally);
109111

@@ -126,16 +128,6 @@ public ResultMessage execute(QueryState state, boolean locally)
126128
return new ResultMessage.SchemaChange(schemaChangeEvent(diff));
127129
}
128130

129-
private void validateKeyspaceName()
130-
{
131-
if (!SchemaConstants.isValidName(keyspaceName))
132-
{
133-
throw ire("Keyspace name must not be empty, more than %d characters long, " +
134-
"or contain non-alphanumeric-underscore characters (got '%s')",
135-
SchemaConstants.NAME_LENGTH, keyspaceName);
136-
}
137-
}
138-
139131
private void grantPermissionsOnResource(IResource resource, AuthenticatedUser user)
140132
{
141133
try

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ public class Directories
8989
public static final String SNAPSHOT_SUBDIR = "snapshots";
9090
public static final String TMP_SUBDIR = "tmp";
9191
public static final String SECONDARY_INDEX_NAME_SEPARATOR = ".";
92+
public static final String TABLE_DIRECTORY_NAME_SEPARATOR = "-";
9293

9394
/**
9495
* The directories used to store keyspaces data.
@@ -196,14 +197,11 @@ public Directories(final TableMetadata metadata, DataDirectory[] paths)
196197
this.metadata = metadata;
197198
this.paths = paths;
198199
ImmutableMap.Builder<Path, DataDirectory> canonicalPathsBuilder = ImmutableMap.builder();
199-
String tableId = metadata.id.toHexString();
200-
int idx = metadata.name.indexOf(SECONDARY_INDEX_NAME_SEPARATOR);
201-
String cfName = idx >= 0 ? metadata.name.substring(0, idx) : metadata.name;
202-
String indexNameWithDot = idx >= 0 ? metadata.name.substring(idx) : null;
200+
String indexNameWithDot = metadata.getIndexNameWithDot();
203201

204202
this.dataPaths = new File[paths.length];
205203
// If upgraded from version less than 2.1, use existing directories
206-
String oldSSTableRelativePath = join(metadata.keyspace, cfName);
204+
String oldSSTableRelativePath = join(metadata.keyspace, metadata.getTableName());
207205
for (int i = 0; i < paths.length; ++i)
208206
{
209207
// check if old SSTable directory exists
@@ -216,7 +214,7 @@ public Directories(final TableMetadata metadata, DataDirectory[] paths)
216214
{
217215
canonicalPathsBuilder = ImmutableMap.builder();
218216
// use 2.1+ style
219-
String newSSTableRelativePath = join(metadata.keyspace, cfName + '-' + tableId);
217+
String newSSTableRelativePath = join(metadata.keyspace, metadata.getTableDirectoryName());
220218
for (int i = 0; i < paths.length; ++i)
221219
{
222220
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
@@ -301,16 +320,8 @@ public String toCqlString(boolean withInternals, boolean ifNotExists)
301320

302321
public void validate()
303322
{
304-
if (!SchemaConstants.isValidName(name))
305-
{
306-
throw new ConfigurationException(format("Keyspace name must not be empty, more than %s characters long, "
307-
+ "or contain non-alphanumeric-underscore characters (got \"%s\")",
308-
SchemaConstants.NAME_LENGTH,
309-
name));
310-
}
311-
323+
validateKeyspaceName(name, ConfigurationException::new);
312324
params.validate(name);
313-
314325
tablesAndViews().forEach(TableMetadata::validate);
315326

316327
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
@@ -28,9 +28,12 @@
2828

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

31+
import static org.apache.cassandra.db.Directories.TABLE_DIRECTORY_NAME_SEPARATOR;
32+
3133
public final class SchemaConstants
3234
{
3335
public static final Pattern PATTERN_WORD_CHARS = Pattern.compile("\\w+");
36+
public static final Pattern PATTERN_NON_WORD_CHAR = Pattern.compile("\\W");
3437

3538
public static final String SYSTEM_KEYSPACE_NAME = "system";
3639
public static final String SCHEMA_KEYSPACE_NAME = "system_schema";
@@ -58,14 +61,37 @@ public final class SchemaConstants
5861
*/
5962
public static final int NAME_LENGTH = 48;
6063

64+
/**
65+
* Longest acceptable file name. Longer names lead to too long file name error.
66+
*/
67+
public static final int FILENAME_LENGTH = 255;
68+
69+
/**
70+
* Length of a table uuid as a hex string.
71+
*/
72+
public static final int TABLE_UUID_AS_HEX_LENGTH = 32;
73+
74+
/**
75+
* Longest acceptable table name, so it can be used in a directory
76+
* name constructed with a suffix of a table id and a separator.
77+
*/
78+
public static final int TABLE_NAME_LENGTH = FILENAME_LENGTH - TABLE_UUID_AS_HEX_LENGTH - TABLE_DIRECTORY_NAME_SEPARATOR.length();
79+
6180
// 59adb24e-f3cd-3e02-97f0-5b395827453f
6281
public static final UUID emptyVersion;
6382

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

66-
public static boolean isValidName(String name)
85+
/**
86+
* Validates that a name is not empty and contains only alphanumeric characters or
87+
* underscore, so it can be used in file or directory names.
88+
*
89+
* @param name the name to check
90+
* @return whether the non-empty name contains only valid characters
91+
*/
92+
public static boolean isValidCharsName(String name)
6793
{
68-
return name != null && !name.isEmpty() && name.length() <= NAME_LENGTH && PATTERN_WORD_CHARS.matcher(name).matches();
94+
return name != null && !name.isEmpty() && PATTERN_WORD_CHARS.matcher(name).matches();
6995
}
7096

7197
static

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

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

5358
@Unmetered
5459
public class TableMetadata implements SchemaElement
@@ -415,11 +420,9 @@ public boolean hasStaticColumns()
415420

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

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

424427
params.validate();
425428

@@ -447,6 +450,17 @@ public void validate()
447450
indexes.validate(this);
448451
}
449452

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

549597
/**
@@ -601,9 +649,14 @@ public TableMetadata withUpdatedUserType(UserType udt)
601649
return builder.build();
602650
}
603651

652+
private ConfigurationException prepareConfigurationException(String format, Object... args)
653+
{
654+
return new ConfigurationException(keyspace + '.' + name + ": " + format(format, args));
655+
}
656+
604657
protected void except(String format, Object... args)
605658
{
606-
throw new ConfigurationException(keyspace + "." + name + ": " + format(format, args));
659+
throw prepareConfigurationException(format, args);
607660
}
608661

609662
@Override

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

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

51+
import org.apache.commons.lang3.ArrayUtils;
5152
import org.junit.*;
5253
import org.slf4j.Logger;
5354
import org.slf4j.LoggerFactory;
@@ -573,6 +574,14 @@ public void flush(String keyspace)
573574
store.forceBlockingFlush();
574575
}
575576

577+
public void flush(String keyspaceName, String table1, String... tables)
578+
{
579+
tables = ArrayUtils.add(tables, table1);
580+
Keyspace keyspace = Keyspace.open(keyspaceName);
581+
for (String tableName : tables)
582+
keyspace.getColumnFamilyStore(tableName).forceBlockingFlush();
583+
}
584+
576585
public void disableCompaction(String keyspace)
577586
{
578587
ColumnFamilyStore store = getCurrentColumnFamilyStore(keyspace);

0 commit comments

Comments
 (0)