Skip to content

Commit 7f92e1a

Browse files
committed
Merge branch 'cassandra-4.1' into cassandra-5.0
* cassandra-4.1: Prevent too long table names not fitting file names
2 parents 5d83175 + bff43df commit 7f92e1a

File tree

13 files changed

+218
-97
lines changed

13 files changed

+218
-97
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
Merged from 4.1:
55
* Redact security-sensitive information in system_views.settings (CASSANDRA-20856)
66
Merged from 4.0:
7+
* Prevent too long table names not fitting file names (CASSANDRA-20389)
78
* Update Jackson to 2.19.2 (CASSANDRA-20848)
89
* Update commons-lang3 to 3.18.0 (CASSANDRA-20849)
910
* Add NativeTransportMaxConcurrentConnectionsPerIp to StorageProxyMBean (CASSANDRA-20642)

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

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

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

114-
validateKeyspaceName();
116+
validateKeyspaceName(keyspaceName, AlterSchemaStatement::ire);
115117

116118
SchemaTransformationResult result = Schema.instance.transform(this, locally);
117119

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

137-
private void validateKeyspaceName()
138-
{
139-
if (!SchemaConstants.isValidName(keyspaceName))
140-
{
141-
throw ire("Keyspace name must not be empty, more than %d characters long, " +
142-
"or contain non-alphanumeric-underscore characters (got '%s')",
143-
SchemaConstants.NAME_LENGTH, keyspaceName);
144-
}
145-
}
146-
147139
protected void validateDefaultTimeToLive(TableParams params)
148140
{
149141
if (params.defaultTimeToLive == 0

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ public final class CreateIndexStatement extends AlterSchemaStatement
6060
public static final String CUSTOM_MULTIPLE_COLUMNS = "Only CUSTOM indexes support multiple columns";
6161
public static final String DUPLICATE_TARGET_COLUMN = "Duplicate column '%s' in index target list";
6262
public static final String COLUMN_DOES_NOT_EXIST = "Column '%s' doesn't exist";
63-
public static final String INVALID_CUSTOM_INDEX_TARGET = "Column '%s' is longer than the permissible name length of %d characters or" +
64-
" contains non-alphanumeric-underscore characters";
63+
public static final String INVALID_CHARS_CUSTOM_INDEX_TARGET = "Column '%s' contains non-alphanumeric-underscore characters";
64+
public static final String TOO_LONG_CUSTOM_INDEX_TARGET = "Column '%s' is longer than the permissible name length of %d characters";
6565
public static final String COLLECTIONS_WITH_DURATIONS_NOT_SUPPORTED = "Secondary indexes are not supported on collections containing durations";
6666
public static final String TUPLES_WITH_DURATIONS_NOT_SUPPORTED = "Secondary indexes are not supported on tuples containing durations";
6767
public static final String DURATIONS_NOT_SUPPORTED = "Secondary indexes are not supported on duration columns";
@@ -206,6 +206,14 @@ Set<String> clientWarnings(KeyspacesDiff diff)
206206
return ImmutableSet.of();
207207
}
208208

209+
private void validateCustomIndexColumnName(String name)
210+
{
211+
if (!SchemaConstants.isValidCharsName(name))
212+
throw ire(INVALID_CHARS_CUSTOM_INDEX_TARGET, name);
213+
if (name.length() > SchemaConstants.NAME_LENGTH)
214+
throw ire(TOO_LONG_CUSTOM_INDEX_TARGET, name, SchemaConstants.NAME_LENGTH);
215+
}
216+
209217
private void validateIndexTarget(TableMetadata table, IndexMetadata.Kind kind, IndexTarget target)
210218
{
211219
ColumnMetadata column = table.getColumn(target.column);
@@ -215,8 +223,9 @@ private void validateIndexTarget(TableMetadata table, IndexMetadata.Kind kind, I
215223

216224
AbstractType<?> baseType = column.type.unwrap();
217225

218-
if ((kind == IndexMetadata.Kind.CUSTOM) && !SchemaConstants.isValidName(target.column.toString()))
219-
throw ire(INVALID_CUSTOM_INDEX_TARGET, target.column, SchemaConstants.NAME_LENGTH);
226+
// TODO: this check needs to be removed with CASSANDRA-20235
227+
if ((kind == IndexMetadata.Kind.CUSTOM))
228+
validateCustomIndexColumnName(target.column.toString());
220229

221230
if (column.type.referencesDuration())
222231
{

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ public class Directories
120120
public static final String SNAPSHOT_SUBDIR = "snapshots";
121121
public static final String TMP_SUBDIR = "tmp";
122122
public static final String SECONDARY_INDEX_NAME_SEPARATOR = ".";
123+
public static final String TABLE_DIRECTORY_NAME_SEPARATOR = "-";
123124

124125
/**
125126
* The directories used to store keyspaces data.
@@ -227,14 +228,11 @@ public Directories(final TableMetadata metadata, DataDirectory[] paths)
227228
this.metadata = metadata;
228229
this.paths = paths;
229230
ImmutableMap.Builder<Path, DataDirectory> canonicalPathsBuilder = ImmutableMap.builder();
230-
String tableId = metadata.id.toHexString();
231-
int idx = metadata.name.indexOf(SECONDARY_INDEX_NAME_SEPARATOR);
232-
String cfName = idx >= 0 ? metadata.name.substring(0, idx) : metadata.name;
233-
String indexNameWithDot = idx >= 0 ? metadata.name.substring(idx) : null;
231+
String indexNameWithDot = metadata.getIndexNameWithDot();
234232

235233
this.dataPaths = new File[paths.length];
236234
// If upgraded from version less than 2.1, use existing directories
237-
String oldSSTableRelativePath = join(metadata.keyspace, cfName);
235+
String oldSSTableRelativePath = join(metadata.keyspace, metadata.getTableName());
238236
for (int i = 0; i < paths.length; ++i)
239237
{
240238
// check if old SSTable directory exists
@@ -247,7 +245,7 @@ public Directories(final TableMetadata metadata, DataDirectory[] paths)
247245
{
248246
canonicalPathsBuilder = ImmutableMap.builder();
249247
// use 2.1+ style
250-
String newSSTableRelativePath = join(metadata.keyspace, cfName + '-' + tableId);
248+
String newSSTableRelativePath = join(metadata.keyspace, metadata.getTableDirectoryName());
251249
for (int i = 0; i < paths.length; ++i)
252250
{
253251
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
@@ -22,7 +22,6 @@
2222
import java.lang.reflect.InvocationTargetException;
2323
import java.util.*;
2424
import java.util.concurrent.ConcurrentHashMap;
25-
import java.util.regex.Pattern;
2625
import java.util.stream.Collectors;
2726

2827
import com.google.common.base.Objects;
@@ -47,17 +46,16 @@
4746
import org.apache.cassandra.utils.FBUtilities;
4847
import org.apache.cassandra.utils.UUIDSerializer;
4948

49+
import static org.apache.cassandra.schema.SchemaConstants.PATTERN_NON_WORD_CHAR;
50+
import static org.apache.cassandra.schema.SchemaConstants.isValidCharsName;
51+
5052
/**
5153
* An immutable representation of secondary index metadata.
5254
*/
5355
public final class IndexMetadata
5456
{
5557
private static final Logger logger = LoggerFactory.getLogger(IndexMetadata.class);
5658

57-
private static final Pattern PATTERN_NON_WORD_CHAR = Pattern.compile("\\W");
58-
private static final Pattern PATTERN_WORD_CHARS = Pattern.compile("\\w+");
59-
60-
6159
public static final Serializer serializer = new Serializer();
6260

6361
/**
@@ -111,11 +109,6 @@ public static IndexMetadata fromIndexTargets(List<IndexTarget> targets,
111109
return new IndexMetadata(name, newOptions, kind);
112110
}
113111

114-
public static boolean isNameValid(String name)
115-
{
116-
return name != null && !name.isEmpty() && PATTERN_WORD_CHARS.matcher(name).matches();
117-
}
118-
119112
public static String generateDefaultIndexName(String table, ColumnIdentifier column)
120113
{
121114
return PATTERN_NON_WORD_CHAR.matcher(table + "_" + column.toString() + "_idx").replaceAll("");
@@ -128,7 +121,8 @@ public static String generateDefaultIndexName(String table)
128121

129122
public void validate(TableMetadata table)
130123
{
131-
if (!isNameValid(name))
124+
// TODO: address validating the length by CASSANDRA-20445
125+
if (!isValidCharsName(name))
132126
throw new ConfigurationException("Illegal index name " + name);
133127

134128
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
@@ -36,6 +36,7 @@
3636
import org.apache.cassandra.cql3.functions.UDFunction;
3737
import org.apache.cassandra.db.marshal.UserType;
3838
import org.apache.cassandra.exceptions.ConfigurationException;
39+
import org.apache.cassandra.exceptions.RequestValidationException;
3940
import org.apache.cassandra.locator.AbstractReplicationStrategy;
4041
import org.apache.cassandra.schema.UserFunctions.FunctionsDiff;
4142
import org.apache.cassandra.schema.Tables.TablesDiff;
@@ -46,12 +47,30 @@
4647
import static java.lang.String.format;
4748

4849
import static com.google.common.collect.Iterables.any;
50+
import static org.apache.cassandra.schema.SchemaConstants.TABLE_NAME_LENGTH;
4951

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

318337
public void validate()
319338
{
320-
if (!SchemaConstants.isValidName(name))
321-
{
322-
throw new ConfigurationException(format("Keyspace name must not be empty, more than %s characters long, "
323-
+ "or contain non-alphanumeric-underscore characters (got \"%s\")",
324-
SchemaConstants.NAME_LENGTH,
325-
name));
326-
}
327-
339+
validateKeyspaceName(name, ConfigurationException::new);
328340
params.validate(name, null);
329-
330341
tablesAndViews().forEach(TableMetadata::validate);
331342

332343
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
@@ -32,12 +32,15 @@
3232
import org.apache.cassandra.db.SystemKeyspace;
3333
import org.apache.cassandra.tracing.TraceKeyspace;
3434

35+
import static org.apache.cassandra.db.Directories.TABLE_DIRECTORY_NAME_SEPARATOR;
36+
3537
/**
3638
* When adding new String keyspace names here, double check if it needs to be added to PartitionDenylist.canDenylistKeyspace
3739
*/
3840
public final class SchemaConstants
3941
{
4042
public static final Pattern PATTERN_WORD_CHARS = Pattern.compile("\\w+");
43+
public static final Pattern PATTERN_NON_WORD_CHAR = Pattern.compile("\\W");
4144

4245
public static final String SYSTEM_KEYSPACE_NAME = "system";
4346
public static final String SCHEMA_KEYSPACE_NAME = "system_schema";
@@ -71,14 +74,37 @@ public final class SchemaConstants
7174
*/
7275
public static final int NAME_LENGTH = 48;
7376

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

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

79-
public static boolean isValidName(String name)
98+
/**
99+
* Validates that a name is not empty and contains only alphanumeric characters or
100+
* underscore, so it can be used in file or directory names.
101+
*
102+
* @param name the name to check
103+
* @return whether the non-empty name contains only valid characters
104+
*/
105+
public static boolean isValidCharsName(String name)
80106
{
81-
return name != null && !name.isEmpty() && name.length() <= NAME_LENGTH && PATTERN_WORD_CHARS.matcher(name).matches();
107+
return name != null && !name.isEmpty() && PATTERN_WORD_CHARS.matcher(name).matches();
82108
}
83109

84110
static

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

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@
5252
import org.apache.cassandra.db.Clustering;
5353
import org.apache.cassandra.db.ClusteringComparator;
5454
import org.apache.cassandra.db.Columns;
55-
import org.apache.cassandra.db.Directories;
5655
import org.apache.cassandra.db.Keyspace;
5756
import org.apache.cassandra.db.RegularAndStaticColumns;
5857
import org.apache.cassandra.db.marshal.AbstractType;
@@ -73,7 +72,12 @@
7372
import static java.lang.String.format;
7473
import static java.util.stream.Collectors.toList;
7574
import static java.util.stream.Collectors.toSet;
76-
import static org.apache.cassandra.schema.IndexMetadata.isNameValid;
75+
import static org.apache.cassandra.db.Directories.SECONDARY_INDEX_NAME_SEPARATOR;
76+
import static org.apache.cassandra.db.Directories.TABLE_DIRECTORY_NAME_SEPARATOR;
77+
import static org.apache.cassandra.schema.KeyspaceMetadata.validateKeyspaceName;
78+
import static org.apache.cassandra.schema.SchemaConstants.FILENAME_LENGTH;
79+
import static org.apache.cassandra.schema.SchemaConstants.TABLE_NAME_LENGTH;
80+
import static org.apache.cassandra.schema.SchemaConstants.isValidCharsName;
7781

7882
@Unmetered
7983
public class TableMetadata implements SchemaElement
@@ -475,11 +479,9 @@ public boolean dependsOn(Function function)
475479

476480
public void validate()
477481
{
478-
if (!isNameValid(keyspace))
479-
except("Keyspace name must not be empty, more than %s characters long, or contain non-alphanumeric-underscore characters (got \"%s\")", SchemaConstants.NAME_LENGTH, keyspace);
482+
validateKeyspaceName(keyspace, this::prepareConfigurationException);
480483

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

484486
params.validate();
485487

@@ -507,6 +509,17 @@ public void validate()
507509
indexes.validate(this);
508510
}
509511

512+
private void validateTableName()
513+
{
514+
if (!isValidCharsName(name))
515+
except("Table name must not be empty or not contain non-alphanumeric-underscore characters (got \"%s\")", name);
516+
517+
if (name.length() > TABLE_NAME_LENGTH)
518+
except("Table name must not be more than %d characters long (got %d characters for \"%s\")", TABLE_NAME_LENGTH, name.length(), name);
519+
520+
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());
521+
}
522+
510523
/**
511524
* To support backward compatibility with thrift super columns in the C* 3.0+ storage engine, we encode said super
512525
* columns as a CQL {@code map<blob, blob>}. To ensure the name of this map did not conflict with any other user
@@ -603,7 +616,41 @@ public ClusteringComparator partitionKeyAsClusteringComparator()
603616
public String indexTableName(IndexMetadata info)
604617
{
605618
// TODO simplify this when info.index_name is guaranteed to be set
606-
return name + Directories.SECONDARY_INDEX_NAME_SEPARATOR + info.name;
619+
return name + SECONDARY_INDEX_NAME_SEPARATOR + info.name;
620+
}
621+
622+
/**
623+
* Returns the table part of the index table name or the entire table name
624+
* if not an index table.
625+
* @return table name part
626+
*/
627+
public String getTableName()
628+
{
629+
int idx = name.indexOf(SECONDARY_INDEX_NAME_SEPARATOR);
630+
return idx >= 0 ? name.substring(0, idx) : name;
631+
}
632+
633+
/**
634+
* Generates a directory name for the table by using table part of
635+
* the (index) table name and table id.
636+
* @return directory name
637+
*/
638+
public String getTableDirectoryName()
639+
{
640+
return getTableName() + TABLE_DIRECTORY_NAME_SEPARATOR + id.toHexString();
641+
}
642+
643+
/**
644+
* Returns the index name from the name of an index table
645+
* including the dot prefixing the index name, see {@link #indexTableName}.
646+
* If not an index table, returns null.
647+
* @return index name prefixed with dot prefix or null
648+
*/
649+
@Nullable
650+
public String getIndexNameWithDot()
651+
{
652+
int idx = name.indexOf(SECONDARY_INDEX_NAME_SEPARATOR);
653+
return idx >= 0 ? name.substring(idx) : null;
607654
}
608655

609656
/**
@@ -661,9 +708,14 @@ public TableMetadata withUpdatedUserType(UserType udt)
661708
return builder.build();
662709
}
663710

711+
private ConfigurationException prepareConfigurationException(String format, Object... args)
712+
{
713+
return new ConfigurationException(keyspace + '.' + name + ": " + format(format, args));
714+
}
715+
664716
protected void except(String format, Object... args)
665717
{
666-
throw new ConfigurationException(keyspace + "." + name + ": " + format(format, args));
718+
throw prepareConfigurationException(format, args);
667719
}
668720

669721
@Override
@@ -1722,4 +1774,4 @@ public static ColumnMetadata getCompactValueColumn(RegularAndStaticColumns colum
17221774

17231775
}
17241776

1725-
}
1777+
}

0 commit comments

Comments
 (0)