yifan-c commented on code in PR #4411:
URL: https://github.com/apache/cassandra/pull/4411#discussion_r2466772709
##########
src/java/org/apache/cassandra/schema/ColumnMetadata.java:
##########
@@ -788,7 +831,8 @@ public long serializedSize(ColumnMetadata t, Version
version)
BOOL_SIZE +
((t.mask == null) ? 0 :
ColumnMask.serializer.serializedSize(t.mask, version)) +
constraintsSize +
- (version.isAtLeast(Version.V7) ? sizeofVInt(t.uniqueId) :
0);
+ (version.isAtLeast(Version.V7) ? sizeofVInt(t.uniqueId) :
0) +
+ (version.isAtLeast(Version.V8) ? (sizeof(t.comment) +
sizeof(t.securityLabel)) : 0);
Review Comment:
Seriously, should we split this long statement up? Those chained ternary
operators are better written in if-else for readability.
##########
src/java/org/apache/cassandra/schema/TableParams.java:
##########
@@ -335,6 +341,7 @@ public String toString()
{
return MoreObjects.toStringHelper(this)
.add(COMMENT.toString(), comment)
+ .add(Option.SECURITY_LABEL.toString(), securityLabel)
Review Comment:
nit: let it be consistent with the other lines? Please statically import
`SECURITY_LABEL`
##########
src/java/org/apache/cassandra/schema/KeyspaceParams.java:
##########
@@ -174,13 +204,21 @@ public KeyspaceParams deserialize(DataInputPlus in,
Version version) throws IOEx
FastPathStrategy fastPath = version.isAtLeast(MIN_ACCORD_VERSION)
? FastPathStrategy.serializer.deserialize(in, version)
: FastPathStrategy.simple();
- return new KeyspaceParams(durableWrites, params, fastPath);
+ String comment = EMPTY_COMMENT;
+ String securityLabel = EMPTY_SECURITY_LABEL;
+ if (version.isAtLeast(Version.V8))
+ {
+ comment = in.readUTF();
+ securityLabel = in.readUTF();
+ }
+ return new KeyspaceParams(durableWrites, params, fastPath,
comment, securityLabel);
}
public long serializedSize(KeyspaceParams t, Version version)
{
return ReplicationParams.serializer.serializedSize(t.replication,
version) +
TypeSizes.sizeof(t.durableWrites) +
+ (version.isAtLeast(Version.V8) ?
(TypeSizes.sizeof(t.comment) + TypeSizes.sizeof(t.securityLabel)) : 0) +
(version.isAtLeast(MIN_ACCORD_VERSION) ?
FastPathStrategy.serializer.serializedSize(t.fastPath, version) : 0);
Review Comment:
nit: split the long single statement up. It is fine so far, but would be
good if we do it early.
##########
src/java/org/apache/cassandra/schema/Types.java:
##########
@@ -498,16 +570,101 @@ public long serializedSize(Types t, Version version)
for (UserType type : t.types.values())
{
size += sizeof(type.getNameAsString());
- List<String> fieldNames =
type.fieldNames().stream().map(FieldIdentifier::toString).collect(toList());
- List<String> fieldTypes =
type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList());
+ List<FieldIdentifier> fieldIdentifiers = type.fieldNames();
+ List<String> fieldNames =
fieldIdentifiers.stream().map(FieldIdentifier::toString).collect(toList());
+ List<String> fieldTypes =
type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList());;
size += sizeof(fieldNames.size());
for (String s : fieldNames)
size += sizeof(s);
size += sizeof(fieldTypes.size());
for (String s : fieldTypes)
size += sizeof(s);
+ if (version.isAtLeast(Version.V8))
+ {
+ size += sizeof(type.comment);
+ size += sizeof(type.securityLabel);
+ size += fieldMetadataSize(type, fieldIdentifiers);
+ }
}
return size;
}
+
+ private void serializeFieldMetadata(UserType type,
List<FieldIdentifier> fieldIdentifiers, DataOutputPlus out) throws IOException
+ {
+ // Sparse serialization: only serialize non-empty metadata with
their positions
+ serializeSparseMetadata(fieldIdentifiers, type::fieldComment, out);
+ serializeSparseMetadata(fieldIdentifiers,
type::fieldSecurityLabel, out);
+ }
+
+ private void serializeSparseMetadata(List<FieldIdentifier>
fieldIdentifiers,
+
java.util.function.Function<FieldIdentifier, String> metadataGetter,
+ DataOutputPlus out) throws
IOException
+ {
+ // Count non-empty values
+ int nonEmptyCount = 0;
+ for (FieldIdentifier fieldId : fieldIdentifiers)
+ {
+ if (!metadataGetter.apply(fieldId).isEmpty())
+ nonEmptyCount++;
+ }
+
+ // Write count followed by position-value pairs
+ out.writeInt(nonEmptyCount);
Review Comment:
`writeUnsignedVInt32`?
##########
src/java/org/apache/cassandra/schema/TableParams.java:
##########
@@ -375,6 +382,8 @@ public void appendCqlTo(CqlBuilder builder, boolean isView)
.newLine()
.append("AND cdc = ").append(cdc)
.newLine()
+ // TODO: AND comment should be deprecatod in future releases in
favor of
+ // JIRA Introducing comments and security labels for schema
elements
.append("AND comment = ").appendWithSingleQuotes(comment)
Review Comment:
Could you file a JIRA and update the TODO here? The Comment statement is
introduced in Cassandra 6.0, and it could be a good idea to drop the comment
table attribute in 7.0.
##########
src/java/org/apache/cassandra/schema/Types.java:
##########
@@ -498,16 +570,101 @@ public long serializedSize(Types t, Version version)
for (UserType type : t.types.values())
{
size += sizeof(type.getNameAsString());
- List<String> fieldNames =
type.fieldNames().stream().map(FieldIdentifier::toString).collect(toList());
- List<String> fieldTypes =
type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList());
+ List<FieldIdentifier> fieldIdentifiers = type.fieldNames();
+ List<String> fieldNames =
fieldIdentifiers.stream().map(FieldIdentifier::toString).collect(toList());
+ List<String> fieldTypes =
type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList());;
size += sizeof(fieldNames.size());
for (String s : fieldNames)
size += sizeof(s);
size += sizeof(fieldTypes.size());
for (String s : fieldTypes)
size += sizeof(s);
+ if (version.isAtLeast(Version.V8))
+ {
+ size += sizeof(type.comment);
+ size += sizeof(type.securityLabel);
+ size += fieldMetadataSize(type, fieldIdentifiers);
+ }
}
return size;
}
+
+ private void serializeFieldMetadata(UserType type,
List<FieldIdentifier> fieldIdentifiers, DataOutputPlus out) throws IOException
+ {
+ // Sparse serialization: only serialize non-empty metadata with
their positions
+ serializeSparseMetadata(fieldIdentifiers, type::fieldComment, out);
+ serializeSparseMetadata(fieldIdentifiers,
type::fieldSecurityLabel, out);
+ }
+
+ private void serializeSparseMetadata(List<FieldIdentifier>
fieldIdentifiers,
+
java.util.function.Function<FieldIdentifier, String> metadataGetter,
Review Comment:
why use the full qualified class name?
##########
src/java/org/apache/cassandra/schema/Types.java:
##########
@@ -498,16 +570,101 @@ public long serializedSize(Types t, Version version)
for (UserType type : t.types.values())
{
size += sizeof(type.getNameAsString());
- List<String> fieldNames =
type.fieldNames().stream().map(FieldIdentifier::toString).collect(toList());
- List<String> fieldTypes =
type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList());
+ List<FieldIdentifier> fieldIdentifiers = type.fieldNames();
+ List<String> fieldNames =
fieldIdentifiers.stream().map(FieldIdentifier::toString).collect(toList());
+ List<String> fieldTypes =
type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList());;
size += sizeof(fieldNames.size());
for (String s : fieldNames)
size += sizeof(s);
size += sizeof(fieldTypes.size());
for (String s : fieldTypes)
size += sizeof(s);
+ if (version.isAtLeast(Version.V8))
+ {
+ size += sizeof(type.comment);
+ size += sizeof(type.securityLabel);
+ size += fieldMetadataSize(type, fieldIdentifiers);
+ }
}
return size;
}
+
+ private void serializeFieldMetadata(UserType type,
List<FieldIdentifier> fieldIdentifiers, DataOutputPlus out) throws IOException
+ {
+ // Sparse serialization: only serialize non-empty metadata with
their positions
+ serializeSparseMetadata(fieldIdentifiers, type::fieldComment, out);
+ serializeSparseMetadata(fieldIdentifiers,
type::fieldSecurityLabel, out);
+ }
+
+ private void serializeSparseMetadata(List<FieldIdentifier>
fieldIdentifiers,
+
java.util.function.Function<FieldIdentifier, String> metadataGetter,
+ DataOutputPlus out) throws
IOException
+ {
+ // Count non-empty values
+ int nonEmptyCount = 0;
+ for (FieldIdentifier fieldId : fieldIdentifiers)
+ {
+ if (!metadataGetter.apply(fieldId).isEmpty())
Review Comment:
should `metadataGetter` just return `null`, instead of `getOrDefault` and
return empty string? I do not see the value of returning an empty string here.
It just adds a few more instruments to process.
##########
src/java/org/apache/cassandra/schema/Types.java:
##########
@@ -498,16 +570,101 @@ public long serializedSize(Types t, Version version)
for (UserType type : t.types.values())
{
size += sizeof(type.getNameAsString());
- List<String> fieldNames =
type.fieldNames().stream().map(FieldIdentifier::toString).collect(toList());
- List<String> fieldTypes =
type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList());
+ List<FieldIdentifier> fieldIdentifiers = type.fieldNames();
+ List<String> fieldNames =
fieldIdentifiers.stream().map(FieldIdentifier::toString).collect(toList());
+ List<String> fieldTypes =
type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList());;
size += sizeof(fieldNames.size());
for (String s : fieldNames)
size += sizeof(s);
size += sizeof(fieldTypes.size());
for (String s : fieldTypes)
size += sizeof(s);
+ if (version.isAtLeast(Version.V8))
+ {
+ size += sizeof(type.comment);
+ size += sizeof(type.securityLabel);
+ size += fieldMetadataSize(type, fieldIdentifiers);
+ }
}
return size;
}
+
+ private void serializeFieldMetadata(UserType type,
List<FieldIdentifier> fieldIdentifiers, DataOutputPlus out) throws IOException
+ {
+ // Sparse serialization: only serialize non-empty metadata with
their positions
+ serializeSparseMetadata(fieldIdentifiers, type::fieldComment, out);
+ serializeSparseMetadata(fieldIdentifiers,
type::fieldSecurityLabel, out);
+ }
+
+ private void serializeSparseMetadata(List<FieldIdentifier>
fieldIdentifiers,
+
java.util.function.Function<FieldIdentifier, String> metadataGetter,
+ DataOutputPlus out) throws
IOException
+ {
+ // Count non-empty values
+ int nonEmptyCount = 0;
+ for (FieldIdentifier fieldId : fieldIdentifiers)
+ {
+ if (!metadataGetter.apply(fieldId).isEmpty())
+ nonEmptyCount++;
+ }
+
+ // Write count followed by position-value pairs
+ out.writeInt(nonEmptyCount);
+ for (int i = 0; i < fieldIdentifiers.size(); i++)
+ {
+ String value = metadataGetter.apply(fieldIdentifiers.get(i));
+ if (!value.isEmpty())
+ {
+ out.writeInt(i);
+ out.writeUTF(value);
+ }
+ }
+ }
+
+ private void deserializeFieldMetadata(DataInputPlus in, List<String>
fieldComments, List<String> fieldSecurityLabels) throws IOException
+ {
+ deserializeSparseMetadata(in, fieldComments);
+ deserializeSparseMetadata(in, fieldSecurityLabels);
+ }
+
+ private void deserializeSparseMetadata(DataInputPlus in, List<String>
target) throws IOException
+ {
+ int count = in.readInt();
+ for (int i = 0; i < count; i++)
+ {
+ int position = in.readInt();
+ String value = in.readUTF();
+ // Ensure the list is large enough to accommodate this position
+ while (target.size() <= position)
+ target.add("");
Review Comment:
I do not think this is needed.
You know the number of all fields already in
`org.apache.cassandra.schema.Types.Serializer#deserialize` before calling this
method. You can create the list with the exact size, `new
ArrayList<>(fieldNamesSize)` and update the field at the position in
`deserializeSparseMetadata`
##########
src/java/org/apache/cassandra/db/marshal/UserType.java:
##########
@@ -442,14 +474,66 @@ public UserType withUpdatedUserType(UserType udt)
{
return isMultiCell == udt.isMultiCell
? udt
- : new UserType(keyspace, name, udt.fieldNames(),
udt.fieldTypes(), isMultiCell);
+ : new UserType(keyspace, name, udt.fieldNames(),
udt.fieldTypes(), isMultiCell, udt.comment, udt.securityLabel,
udt.fieldComments, udt.fieldSecurityLabels);
}
return new UserType(keyspace,
name,
fieldNames,
Lists.newArrayList(transform(fieldTypes(), t ->
t.withUpdatedUserType(udt))),
- isMultiCell());
+ isMultiCell(),
+ comment,
+ securityLabel,
+ fieldComments,
+ fieldSecurityLabels);
+ }
+
+ public UserType withComment(String comment)
+ {
+ return new UserType(keyspace, name, fieldNames, fieldTypes(),
isMultiCell(), comment, securityLabel, fieldComments, fieldSecurityLabels);
+ }
+
+ public UserType withSecurityLabel(String securityLabel)
+ {
+ return new UserType(keyspace, name, fieldNames, fieldTypes(),
isMultiCell(), comment, securityLabel, fieldComments, fieldSecurityLabels);
+ }
+
+ public UserType withFieldComment(FieldIdentifier fieldName, String
fieldComment)
+ {
+ if (fieldPosition(fieldName) == -1)
+ throw new IllegalArgumentException(String.format("Field '%s'
doesn't exist in type '%s.%s'", fieldName, keyspace, getNameAsString()));
+
+ Map<FieldIdentifier, String> newFieldComments = new
HashMap<>(fieldComments);
+ if (fieldComment == null || fieldComment.isEmpty())
+ newFieldComments.remove(fieldName);
Review Comment:
field comment is removed when user sets the comment to empty string? In the
example, one only removes the comment by setting the comment to `NULL`.
##########
src/java/org/apache/cassandra/schema/Types.java:
##########
@@ -498,16 +570,101 @@ public long serializedSize(Types t, Version version)
for (UserType type : t.types.values())
{
size += sizeof(type.getNameAsString());
- List<String> fieldNames =
type.fieldNames().stream().map(FieldIdentifier::toString).collect(toList());
- List<String> fieldTypes =
type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList());
+ List<FieldIdentifier> fieldIdentifiers = type.fieldNames();
+ List<String> fieldNames =
fieldIdentifiers.stream().map(FieldIdentifier::toString).collect(toList());
+ List<String> fieldTypes =
type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList());;
size += sizeof(fieldNames.size());
for (String s : fieldNames)
size += sizeof(s);
size += sizeof(fieldTypes.size());
for (String s : fieldTypes)
size += sizeof(s);
+ if (version.isAtLeast(Version.V8))
+ {
+ size += sizeof(type.comment);
+ size += sizeof(type.securityLabel);
+ size += fieldMetadataSize(type, fieldIdentifiers);
+ }
}
return size;
}
+
+ private void serializeFieldMetadata(UserType type,
List<FieldIdentifier> fieldIdentifiers, DataOutputPlus out) throws IOException
+ {
+ // Sparse serialization: only serialize non-empty metadata with
their positions
+ serializeSparseMetadata(fieldIdentifiers, type::fieldComment, out);
+ serializeSparseMetadata(fieldIdentifiers,
type::fieldSecurityLabel, out);
+ }
+
+ private void serializeSparseMetadata(List<FieldIdentifier>
fieldIdentifiers,
+
java.util.function.Function<FieldIdentifier, String> metadataGetter,
+ DataOutputPlus out) throws
IOException
+ {
+ // Count non-empty values
+ int nonEmptyCount = 0;
+ for (FieldIdentifier fieldId : fieldIdentifiers)
+ {
+ if (!metadataGetter.apply(fieldId).isEmpty())
+ nonEmptyCount++;
+ }
+
+ // Write count followed by position-value pairs
+ out.writeInt(nonEmptyCount);
+ for (int i = 0; i < fieldIdentifiers.size(); i++)
+ {
+ String value = metadataGetter.apply(fieldIdentifiers.get(i));
+ if (!value.isEmpty())
+ {
+ out.writeInt(i);
+ out.writeUTF(value);
+ }
+ }
+ }
+
+ private void deserializeFieldMetadata(DataInputPlus in, List<String>
fieldComments, List<String> fieldSecurityLabels) throws IOException
+ {
+ deserializeSparseMetadata(in, fieldComments);
+ deserializeSparseMetadata(in, fieldSecurityLabels);
+ }
+
+ private void deserializeSparseMetadata(DataInputPlus in, List<String>
target) throws IOException
+ {
+ int count = in.readInt();
+ for (int i = 0; i < count; i++)
+ {
+ int position = in.readInt();
+ String value = in.readUTF();
+ // Ensure the list is large enough to accommodate this position
+ while (target.size() <= position)
+ target.add("");
+ target.set(position, value);
+ }
+ }
+
+ private long fieldMetadataSize(UserType type, List<FieldIdentifier>
fieldIdentifiers)
+ {
+ return sparseMetadataSize(fieldIdentifiers, type::fieldComment)
+ + sparseMetadataSize(fieldIdentifiers,
type::fieldSecurityLabel);
+ }
+
+ private long sparseMetadataSize(List<FieldIdentifier> fieldIdentifiers,
+
java.util.function.Function<FieldIdentifier, String> metadataGetter)
+ {
+ int nonEmptyCount = 0;
+ long dataSize = 0;
+
+ for (FieldIdentifier fieldId : fieldIdentifiers)
+ {
+ String value = metadataGetter.apply(fieldId);
+ if (!value.isEmpty())
+ {
+ nonEmptyCount++;
+ dataSize += sizeof(0); // position (int)
Review Comment:
update to use `sizeofUnsignedVInt` when using vint elsewhere.
##########
src/java/org/apache/cassandra/schema/Types.java:
##########
@@ -498,16 +570,101 @@ public long serializedSize(Types t, Version version)
for (UserType type : t.types.values())
{
size += sizeof(type.getNameAsString());
- List<String> fieldNames =
type.fieldNames().stream().map(FieldIdentifier::toString).collect(toList());
- List<String> fieldTypes =
type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList());
+ List<FieldIdentifier> fieldIdentifiers = type.fieldNames();
+ List<String> fieldNames =
fieldIdentifiers.stream().map(FieldIdentifier::toString).collect(toList());
+ List<String> fieldTypes =
type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList());;
size += sizeof(fieldNames.size());
for (String s : fieldNames)
size += sizeof(s);
size += sizeof(fieldTypes.size());
for (String s : fieldTypes)
size += sizeof(s);
+ if (version.isAtLeast(Version.V8))
+ {
+ size += sizeof(type.comment);
+ size += sizeof(type.securityLabel);
+ size += fieldMetadataSize(type, fieldIdentifiers);
+ }
}
return size;
}
+
+ private void serializeFieldMetadata(UserType type,
List<FieldIdentifier> fieldIdentifiers, DataOutputPlus out) throws IOException
+ {
+ // Sparse serialization: only serialize non-empty metadata with
their positions
+ serializeSparseMetadata(fieldIdentifiers, type::fieldComment, out);
+ serializeSparseMetadata(fieldIdentifiers,
type::fieldSecurityLabel, out);
+ }
+
+ private void serializeSparseMetadata(List<FieldIdentifier>
fieldIdentifiers,
+
java.util.function.Function<FieldIdentifier, String> metadataGetter,
+ DataOutputPlus out) throws
IOException
+ {
+ // Count non-empty values
+ int nonEmptyCount = 0;
+ for (FieldIdentifier fieldId : fieldIdentifiers)
+ {
+ if (!metadataGetter.apply(fieldId).isEmpty())
+ nonEmptyCount++;
+ }
+
+ // Write count followed by position-value pairs
+ out.writeInt(nonEmptyCount);
+ for (int i = 0; i < fieldIdentifiers.size(); i++)
+ {
+ String value = metadataGetter.apply(fieldIdentifiers.get(i));
+ if (!value.isEmpty())
+ {
+ out.writeInt(i);
Review Comment:
hmm, I think it does not need to use `int` in practice. However, there is
not restriction on limit the number of fields in a UDT. So theoretically, `int`
is required. One can go really crazy and create Integer.MAX_VALUE number of
fields.
Add restriction is out of the scope of this PR.
But I think you might want to write the variable int to avoid write bunch 0s.
```suggestion
out.writeUnsignedVInt32(i);
```
##########
src/java/org/apache/cassandra/schema/Types.java:
##########
@@ -498,16 +570,101 @@ public long serializedSize(Types t, Version version)
for (UserType type : t.types.values())
{
size += sizeof(type.getNameAsString());
- List<String> fieldNames =
type.fieldNames().stream().map(FieldIdentifier::toString).collect(toList());
- List<String> fieldTypes =
type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList());
+ List<FieldIdentifier> fieldIdentifiers = type.fieldNames();
+ List<String> fieldNames =
fieldIdentifiers.stream().map(FieldIdentifier::toString).collect(toList());
+ List<String> fieldTypes =
type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList());;
size += sizeof(fieldNames.size());
for (String s : fieldNames)
size += sizeof(s);
size += sizeof(fieldTypes.size());
for (String s : fieldTypes)
size += sizeof(s);
+ if (version.isAtLeast(Version.V8))
+ {
+ size += sizeof(type.comment);
+ size += sizeof(type.securityLabel);
+ size += fieldMetadataSize(type, fieldIdentifiers);
+ }
}
return size;
}
+
+ private void serializeFieldMetadata(UserType type,
List<FieldIdentifier> fieldIdentifiers, DataOutputPlus out) throws IOException
+ {
+ // Sparse serialization: only serialize non-empty metadata with
their positions
+ serializeSparseMetadata(fieldIdentifiers, type::fieldComment, out);
+ serializeSparseMetadata(fieldIdentifiers,
type::fieldSecurityLabel, out);
+ }
+
+ private void serializeSparseMetadata(List<FieldIdentifier>
fieldIdentifiers,
+
java.util.function.Function<FieldIdentifier, String> metadataGetter,
+ DataOutputPlus out) throws
IOException
+ {
+ // Count non-empty values
+ int nonEmptyCount = 0;
+ for (FieldIdentifier fieldId : fieldIdentifiers)
+ {
+ if (!metadataGetter.apply(fieldId).isEmpty())
+ nonEmptyCount++;
+ }
+
+ // Write count followed by position-value pairs
+ out.writeInt(nonEmptyCount);
+ for (int i = 0; i < fieldIdentifiers.size(); i++)
+ {
+ String value = metadataGetter.apply(fieldIdentifiers.get(i));
+ if (!value.isEmpty())
+ {
+ out.writeInt(i);
+ out.writeUTF(value);
+ }
+ }
+ }
+
+ private void deserializeFieldMetadata(DataInputPlus in, List<String>
fieldComments, List<String> fieldSecurityLabels) throws IOException
+ {
+ deserializeSparseMetadata(in, fieldComments);
+ deserializeSparseMetadata(in, fieldSecurityLabels);
+ }
+
+ private void deserializeSparseMetadata(DataInputPlus in, List<String>
target) throws IOException
+ {
+ int count = in.readInt();
Review Comment:
`readUnsignedVInt32`?
##########
src/java/org/apache/cassandra/schema/Types.java:
##########
@@ -498,16 +570,101 @@ public long serializedSize(Types t, Version version)
for (UserType type : t.types.values())
{
size += sizeof(type.getNameAsString());
- List<String> fieldNames =
type.fieldNames().stream().map(FieldIdentifier::toString).collect(toList());
- List<String> fieldTypes =
type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList());
+ List<FieldIdentifier> fieldIdentifiers = type.fieldNames();
+ List<String> fieldNames =
fieldIdentifiers.stream().map(FieldIdentifier::toString).collect(toList());
+ List<String> fieldTypes =
type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList());;
size += sizeof(fieldNames.size());
for (String s : fieldNames)
size += sizeof(s);
size += sizeof(fieldTypes.size());
for (String s : fieldTypes)
size += sizeof(s);
+ if (version.isAtLeast(Version.V8))
+ {
+ size += sizeof(type.comment);
+ size += sizeof(type.securityLabel);
+ size += fieldMetadataSize(type, fieldIdentifiers);
+ }
}
return size;
}
+
+ private void serializeFieldMetadata(UserType type,
List<FieldIdentifier> fieldIdentifiers, DataOutputPlus out) throws IOException
+ {
+ // Sparse serialization: only serialize non-empty metadata with
their positions
+ serializeSparseMetadata(fieldIdentifiers, type::fieldComment, out);
+ serializeSparseMetadata(fieldIdentifiers,
type::fieldSecurityLabel, out);
+ }
+
+ private void serializeSparseMetadata(List<FieldIdentifier>
fieldIdentifiers,
+
java.util.function.Function<FieldIdentifier, String> metadataGetter,
+ DataOutputPlus out) throws
IOException
+ {
+ // Count non-empty values
+ int nonEmptyCount = 0;
+ for (FieldIdentifier fieldId : fieldIdentifiers)
+ {
+ if (!metadataGetter.apply(fieldId).isEmpty())
+ nonEmptyCount++;
+ }
+
+ // Write count followed by position-value pairs
+ out.writeInt(nonEmptyCount);
+ for (int i = 0; i < fieldIdentifiers.size(); i++)
+ {
+ String value = metadataGetter.apply(fieldIdentifiers.get(i));
+ if (!value.isEmpty())
Review Comment:
You should be able to avoid the second evaluation. It is a waste.
##########
doc/modules/cassandra/pages/developing/cql/ddl.adoc:
##########
@@ -801,3 +801,195 @@ statements.
However, tables are the only object that can be truncated currently, and the
`TABLE` keyword can be omitted.
Truncating a table permanently removes all existing data from the table, but
without removing the table itself.
+
+[[comment-statement]]
+== COMMENT ON
+
+The `COMMENT ON` statement allows you to add descriptive comments to schema
elements for documentation purposes.
+Comments are stored in the schema metadata and displayed when using `DESCRIBE`
statements.
+
+=== COMMENT ON KEYSPACE
+
+Add or modify a comment on a keyspace:
+
+[source,cql]
+----
+COMMENT ON KEYSPACE keyspace_name IS 'comment text';
+COMMENT ON KEYSPACE keyspace_name IS NULL; -- Remove comment
+----
+
+Example:
+
+[source,cql]
+----
+COMMENT ON KEYSPACE cycling IS 'Keyspace for cycling application data';
+----
+
+=== COMMENT ON TABLE
+
+Add or modify a comment on a table:
+
+[source,cql]
+----
+COMMENT ON TABLE [keyspace_name.]table_name IS 'comment text';
+COMMENT ON TABLE [keyspace_name.]table_name IS NULL; -- Remove comment
+----
+
+Example:
+
+[source,cql]
+----
+COMMENT ON TABLE cycling.cyclist_name IS 'Table storing cyclist names and
basic information';
+----
+
+=== COMMENT ON COLUMN
+
+Add or modify a comment on a column:
+
+[source,cql]
+----
+COMMENT ON COLUMN [keyspace_name.]table_name.column_name IS 'comment text';
+COMMENT ON COLUMN [keyspace_name.]table_name.column_name IS NULL; -- Remove
comment
+----
Review Comment:
Once the comment is removed, describe table should not print the `COMMENT ON
COLUMN` statement.
##########
src/java/org/apache/cassandra/schema/Types.java:
##########
@@ -498,16 +570,101 @@ public long serializedSize(Types t, Version version)
for (UserType type : t.types.values())
{
size += sizeof(type.getNameAsString());
- List<String> fieldNames =
type.fieldNames().stream().map(FieldIdentifier::toString).collect(toList());
- List<String> fieldTypes =
type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList());
+ List<FieldIdentifier> fieldIdentifiers = type.fieldNames();
+ List<String> fieldNames =
fieldIdentifiers.stream().map(FieldIdentifier::toString).collect(toList());
+ List<String> fieldTypes =
type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList());;
size += sizeof(fieldNames.size());
for (String s : fieldNames)
size += sizeof(s);
size += sizeof(fieldTypes.size());
for (String s : fieldTypes)
size += sizeof(s);
+ if (version.isAtLeast(Version.V8))
+ {
+ size += sizeof(type.comment);
+ size += sizeof(type.securityLabel);
+ size += fieldMetadataSize(type, fieldIdentifiers);
+ }
}
return size;
}
+
+ private void serializeFieldMetadata(UserType type,
List<FieldIdentifier> fieldIdentifiers, DataOutputPlus out) throws IOException
+ {
+ // Sparse serialization: only serialize non-empty metadata with
their positions
+ serializeSparseMetadata(fieldIdentifiers, type::fieldComment, out);
+ serializeSparseMetadata(fieldIdentifiers,
type::fieldSecurityLabel, out);
+ }
+
+ private void serializeSparseMetadata(List<FieldIdentifier>
fieldIdentifiers,
+
java.util.function.Function<FieldIdentifier, String> metadataGetter,
+ DataOutputPlus out) throws
IOException
+ {
+ // Count non-empty values
+ int nonEmptyCount = 0;
+ for (FieldIdentifier fieldId : fieldIdentifiers)
+ {
+ if (!metadataGetter.apply(fieldId).isEmpty())
+ nonEmptyCount++;
+ }
+
+ // Write count followed by position-value pairs
+ out.writeInt(nonEmptyCount);
+ for (int i = 0; i < fieldIdentifiers.size(); i++)
+ {
+ String value = metadataGetter.apply(fieldIdentifiers.get(i));
+ if (!value.isEmpty())
+ {
+ out.writeInt(i);
+ out.writeUTF(value);
+ }
+ }
+ }
+
+ private void deserializeFieldMetadata(DataInputPlus in, List<String>
fieldComments, List<String> fieldSecurityLabels) throws IOException
+ {
+ deserializeSparseMetadata(in, fieldComments);
+ deserializeSparseMetadata(in, fieldSecurityLabels);
+ }
+
+ private void deserializeSparseMetadata(DataInputPlus in, List<String>
target) throws IOException
+ {
+ int count = in.readInt();
+ for (int i = 0; i < count; i++)
+ {
+ int position = in.readInt();
Review Comment:
Update here too, if you have updated the serialization method.
```suggestion
int position = in.readUnsignedVInt32();
```
##########
src/java/org/apache/cassandra/cql3/ColumnSpecification.java:
##########
@@ -28,17 +28,28 @@
public class ColumnSpecification
{
+ protected static final String EMPTY_COMMENT = "";
+ protected static final String EMPTY_SECURITY_LABEL = "";
public final String ksName;
public final String cfName;
public final ColumnIdentifier name;
public final AbstractType<?> type;
+ public final String comment;
+ public final String securityLabel;
public ColumnSpecification(String ksName, String cfName, ColumnIdentifier
name, AbstractType<?> type)
+ {
+ this(ksName, cfName, name, type, EMPTY_COMMENT, EMPTY_SECURITY_LABEL);
+ }
+
+ public ColumnSpecification(String ksName, String cfName, ColumnIdentifier
name, AbstractType<?> type, String comment, String securityLabel)
{
this.ksName = ksName;
this.cfName = cfName;
this.name = name;
this.type = type;
+ this.comment = comment == null ? EMPTY_COMMENT : comment;
+ this.securityLabel = securityLabel == null ? EMPTY_SECURITY_LABEL :
securityLabel;
Review Comment:
Empty string is assigned when the value is null. Then, how do un-setting
comment and security label queries work?
##########
test/unit/org/apache/cassandra/cql3/statements/DescribeStatementTest.java:
##########
@@ -1050,6 +1051,201 @@ public void
testDescFunctionAndAggregateShouldNotOmitQuotations() throws Throwab
}
}
+ @Test
+ public void testDescribeKeyspaceWithCommentsAndSecurityLabels() throws
Throwable
+ {
+ try
+ {
+ // Create keyspace with comment and security label
+ execute("CREATE KEYSPACE test_comments WITH REPLICATION = {'class'
: 'SimpleStrategy', 'replication_factor' : 1}");
+ execute("COMMENT ON KEYSPACE test_comments IS 'This is a test
keyspace with comments'");
+ execute("SECURITY LABEL ON KEYSPACE test_comments IS
'confidential'");
+
+ String expectedKeyspaceOutput = "CREATE KEYSPACE test_comments
WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}" +
+ " AND durable_writes = true" +
+ " AND fast_path = 'simple';\n" +
+ "COMMENT ON KEYSPACE test_comments
IS 'This is a test keyspace with comments';\n" +
+ "SECURITY LABEL ON KEYSPACE
test_comments IS 'confidential';";
+
+ for (String describeKeyword : new String[]{ "DESCRIBE", "DESC" })
+ {
+ assertRowsNet(executeDescribeNet(describeKeyword + " ONLY
KEYSPACE test_comments"),
+ row("test_comments", "keyspace",
"test_comments", expectedKeyspaceOutput));
+ }
+ }
+ finally
+ {
+ execute("DROP KEYSPACE IF EXISTS test_comments");
+ }
+ }
+
+ @Test
+ public void testDescribeTableWithCommentsAndSecurityLabels() throws
Throwable
+ {
+ try
+ {
+ // Create keyspace and table with comments and security labels
+ execute("CREATE KEYSPACE test_table_comments WITH REPLICATION =
{'class' : 'SimpleStrategy', 'replication_factor' : 1}");
+ execute("CREATE TABLE test_table_comments.users (id int PRIMARY
KEY, name text, email text)");
+
+ // Add comments and security labels to table and columns
+ execute("COMMENT ON TABLE test_table_comments.users IS 'User
information table'");
+ execute("SECURITY LABEL ON TABLE test_table_comments.users IS
'restricted'");
+ execute("COMMENT ON COLUMN test_table_comments.users.name IS 'User
full name'");
+ execute("SECURITY LABEL ON COLUMN test_table_comments.users.name
IS 'pii'");
+
+ String expectedTableOutput = "CREATE TABLE
test_table_comments.users (\n" +
+ " id int PRIMARY KEY,\n" +
+ " email text,\n" +
+ " name text\n" +
+ ") WITH " + tableParametersCql("User
information table") + "\n" +
+ "COMMENT ON TABLE
test_table_comments.users IS 'User information table';\n" +
+ "SECURITY LABEL ON TABLE
test_table_comments.users IS 'restricted';\n" +
+ "COMMENT ON COLUMN
test_table_comments.users.name IS 'User full name';\n" +
+ "SECURITY LABEL ON COLUMN
test_table_comments.users.name IS 'pii';";
+ for (String describeKeyword : new String[]{ "DESCRIBE", "DESC" })
+ {
+ assertRowsNet(executeDescribeNet("test_table_comments",
describeKeyword + " TABLE users"),
+ row("test_table_comments", "table", "users",
expectedTableOutput));
+ }
+ }
+ finally
+ {
+ execute("DROP KEYSPACE IF EXISTS test_table_comments");
+ }
+ }
+
+ @Test
+ public void testDescribeUserTypeWithCommentsAndSecurityLabels() throws
Throwable
+ {
+ try
+ {
+ // Create a user-defined type with comments and security labels
+ String type = createType(KEYSPACE_PER_TEST, "CREATE TYPE %s (name
text, age int, address text)");
+
+ execute("COMMENT ON TYPE " + KEYSPACE_PER_TEST + "." + type + " IS
'Person information type'");
+ execute("SECURITY LABEL ON TYPE " + KEYSPACE_PER_TEST + "." + type
+ " IS 'personal_data'");
+
+ String expectedTypeOutput = "CREATE TYPE " + KEYSPACE_PER_TEST +
"." + type + " (\n" +
+ " name text,\n" +
+ " age int,\n" +
+ " address text\n" +
+ ");\n" +
+ "COMMENT ON TYPE " + KEYSPACE_PER_TEST
+ "." + type + " IS 'Person information type';\n" +
+ "SECURITY LABEL ON TYPE " +
KEYSPACE_PER_TEST + "." + type + " IS 'personal_data';";
+
+ for (String describeKeyword : new String[]{ "DESCRIBE", "DESC" })
+ {
+ assertRowsNet(executeDescribeNet(KEYSPACE_PER_TEST,
describeKeyword + " TYPE " + type),
+ row(KEYSPACE_PER_TEST, "type", type,
expectedTypeOutput));
+ }
+ }
+ finally
+ {
+ // Cleanup is handled by the base test class
+ }
+ }
+
+ @Test
+ public void testDescribeUserTypeWithFieldCommentsAndSecurityLabels()
throws Throwable
+ {
+ try
+ {
+ // Create a user-defined type with field-level comments and
security labels
+ String type = createType(KEYSPACE_PER_TEST, "CREATE TYPE %s
(latitude double, longitude double)");
+
+ execute("COMMENT ON TYPE " + KEYSPACE_PER_TEST + "." + type + " IS
'Geographic location'");
+ execute("SECURITY LABEL ON TYPE " + KEYSPACE_PER_TEST + "." + type
+ " IS 'GEODATA'");
+ execute("COMMENT ON FIELD " + KEYSPACE_PER_TEST + "." + type +
".latitude IS 'Latitude in decimal degrees'");
+ execute("SECURITY LABEL ON FIELD " + KEYSPACE_PER_TEST + "." +
type + ".latitude IS 'SENSITIVE'");
+ execute("COMMENT ON FIELD " + KEYSPACE_PER_TEST + "." + type +
".longitude IS 'Longitude in decimal degrees'");
+ execute("SECURITY LABEL ON FIELD " + KEYSPACE_PER_TEST + "." +
type + ".longitude IS 'SENSITIVE'");
+
+ String expectedTypeOutput = "CREATE TYPE " + KEYSPACE_PER_TEST +
"." + type + " (\n" +
+ " latitude double,\n" +
+ " longitude double\n" +
+ ");\n" +
+ "COMMENT ON TYPE " + KEYSPACE_PER_TEST
+ "." + type + " IS 'Geographic location';\n" +
+ "COMMENT ON FIELD " + KEYSPACE_PER_TEST
+ "." + type + ".latitude IS 'Latitude in decimal degrees';\n" +
+ "COMMENT ON FIELD " + KEYSPACE_PER_TEST
+ "." + type + ".longitude IS 'Longitude in decimal degrees';\n" +
+ "SECURITY LABEL ON TYPE " +
KEYSPACE_PER_TEST + "." + type + " IS 'GEODATA';\n" +
+ "SECURITY LABEL ON FIELD " +
KEYSPACE_PER_TEST + "." + type + ".latitude IS 'SENSITIVE';\n" +
+ "SECURITY LABEL ON FIELD " +
KEYSPACE_PER_TEST + "." + type + ".longitude IS 'SENSITIVE';";
+
+ for (String describeKeyword : new String[]{ "DESCRIBE", "DESC" })
+ {
+ assertRowsNet(executeDescribeNet(KEYSPACE_PER_TEST,
describeKeyword + " TYPE " + type),
+ row(KEYSPACE_PER_TEST, "type", type,
expectedTypeOutput));
+ }
+ }
+ finally
+ {
+ // Cleanup is handled by the base test class
+ }
+ }
+
+ @Test
+ public void testDescribeSchemaWithCommentsAndSecurityLabels() throws
Throwable
+ {
+ try
+ {
+ // Create a comprehensive schema with comments and security labels
+ execute("CREATE KEYSPACE test_schema WITH REPLICATION = {'class' :
'SimpleStrategy', 'replication_factor' : 1}");
+ execute("COMMENT ON KEYSPACE test_schema IS 'Test schema for
comments and security labels'");
+ execute("SECURITY LABEL ON KEYSPACE test_schema IS
'test_environment'");
+
+ execute("CREATE TYPE test_schema.address_type (street text, city
text, zip text)");
+ String type = "address_type";
+ execute("COMMENT ON TYPE test_schema." + type + " IS 'Address
information'");
+ execute("SECURITY LABEL ON TYPE test_schema." + type + " IS
'location_data'");
+
+ execute("CREATE TABLE test_schema.customers (id int PRIMARY KEY,
name text, addr " + type + ")");
+ execute("COMMENT ON TABLE test_schema.customers IS 'Customer
information'");
+ execute("SECURITY LABEL ON TABLE test_schema.customers IS
'customer_data'");
+ execute("COMMENT ON COLUMN test_schema.customers.name IS 'Customer
name'");
+ execute("SECURITY LABEL ON COLUMN test_schema.customers.name IS
'pii'");
+
+ ResultSet result = executeDescribeNet("DESCRIBE SCHEMA");
+ boolean foundKeyspaceWithComment = false;
+ boolean foundTypeWithComment = false;
+ boolean foundTableWithComment = false;
+
+ for (Row row : result.all())
+ {
+ String createStatement = row.getString("create_statement");
+
+ if (row.getString("type").equals("keyspace") &&
row.getString("name").equals("test_schema"))
+ {
+ assertTrue("Keyspace should include comment",
createStatement.contains("COMMENT ON KEYSPACE test_schema IS 'Test schema for
comments and security labels'"));
+ assertTrue("Keyspace should include security label",
createStatement.contains("SECURITY LABEL ON KEYSPACE test_schema IS
'test_environment'"));
+ foundKeyspaceWithComment = true;
+ }
+ else if (row.getString("type").equals("type") &&
row.getString("name").equals(type))
+ {
+ assertTrue("Type should include comment",
createStatement.contains("COMMENT ON TYPE test_schema." + type + " IS 'Address
information'"));
+ assertTrue("Type should include security label",
createStatement.contains("SECURITY LABEL ON TYPE test_schema." + type + " IS
'location_data'"));
+ foundTypeWithComment = true;
+ }
+ else if (row.getString("type").equals("table") &&
row.getString("name").equals("customers"))
+ {
+ assertTrue("Table should include comment",
createStatement.contains("COMMENT ON TABLE test_schema.customers IS 'Customer
information';"));
+ assertTrue("Table should include security label",
createStatement.contains("SECURITY LABEL ON TABLE test_schema.customers IS
'customer_data'"));
+ assertTrue("Table should include column comment",
createStatement.contains("COMMENT ON COLUMN test_schema.customers.name IS
'Customer name'"));
+ assertTrue("Table should include column security label",
createStatement.contains("SECURITY LABEL ON COLUMN test_schema.customers.name
IS 'pii'"));
+ foundTableWithComment = true;
+ }
+ }
+
+ assertTrue("Should find keyspace with comment in schema",
foundKeyspaceWithComment);
+ assertTrue("Should find type with comment in schema",
foundTypeWithComment);
+ assertTrue("Should find table with comment in schema",
foundTableWithComment);
+ }
+ finally
+ {
+ execute("DROP KEYSPACE IF EXISTS test_schema");
+ }
+ }
+
Review Comment:
Please add test scenarios.
1. `DESCRIBE` after `comment|security label` is unset
2. Make `comment|security label` on keyspace|table|column|etc.` and drop the
`keyspace|table|column|etc.` and run `DESCRIBE`
Both test scenarios should not print the `comment|security label` statements.
##########
src/java/org/apache/cassandra/schema/TableMetadata.java:
##########
@@ -1482,6 +1488,32 @@ public Builder alterColumnMask(ColumnIdentifier name,
@Nullable ColumnMask mask)
return this;
}
+ public Builder alterColumnComment(ColumnIdentifier name, String
comment)
+ {
+ ColumnMetadata column = columns.get(name.bytes);
+ if (column == null)
+ throw new IllegalArgumentException("Column " + name + " is
invalid");
Review Comment:
Why it is invalid? Should it say the column does not exist?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]