You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by jk...@apache.org on 2025/11/19 19:46:52 UTC
(cassandra) branch trunk updated: CASSANDRA-20943 Introducing comments and security labels for schema elements
This is an automated email from the ASF dual-hosted git repository.
jkonisa pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra.git
The following commit(s) were added to refs/heads/trunk by this push:
new b11633be4f CASSANDRA-20943 Introducing comments and security labels for schema elements
b11633be4f is described below
commit b11633be4f3ee4b7aa5096dc7afdcc86accc2deb
Author: jkonisa <jk...@apple.com>
AuthorDate: Fri Oct 3 15:15:42 2025 -0700
CASSANDRA-20943 Introducing comments and security labels for schema elements
---
CHANGES.txt | 1 +
conf/cassandra.yaml | 8 +
.../cassandra/pages/developing/cql/ddl.adoc | 189 +++++++
pylib/cqlshlib/cql3handling.py | 36 ++
pylib/cqlshlib/test/test_cqlsh_completion.py | 12 +-
src/antlr/Lexer.g | 9 +
src/antlr/Parser.g | 125 ++++-
.../apache/cassandra/audit/AuditLogEntryType.java | 8 +
src/java/org/apache/cassandra/config/Config.java | 3 +
.../cassandra/config/DatabaseDescriptor.java | 10 +
.../apache/cassandra/cql3/ColumnSpecification.java | 15 +-
.../org/apache/cassandra/cql3/SchemaElement.java | 19 +
.../cql3/statements/DescribeStatement.java | 4 +-
.../cql3/statements/SchemaDescriptionsUtil.java | 273 ++++++++++
.../schema/CommentOnColumnStatement.java | 129 +++++
.../schema/CommentOnKeyspaceStatement.java | 108 ++++
.../statements/schema/CommentOnTableStatement.java | 122 +++++
.../schema/CommentOnUserTypeFieldStatement.java | 126 +++++
.../schema/CommentOnUserTypeStatement.java | 119 +++++
.../cql3/statements/schema/CopyTableStatement.java | 189 ++++---
.../schema/SchemaDescriptionStatement.java | 145 ++++++
.../schema/SecurityLabelOnColumnStatement.java | 161 ++++++
.../schema/SecurityLabelOnKeyspaceStatement.java | 140 ++++++
.../schema/SecurityLabelOnTableStatement.java | 152 ++++++
.../SecurityLabelOnUserTypeFieldStatement.java | 157 ++++++
.../schema/SecurityLabelOnUserTypeStatement.java | 148 ++++++
.../org/apache/cassandra/db/marshal/UserType.java | 101 +++-
.../db/virtual/AbstractSchemaMetadataTable.java | 312 ++++++++++++
.../cassandra/db/virtual/SchemaCommentsTable.java | 92 ++++
.../db/virtual/SchemaSecurityLabelsTable.java | 92 ++++
.../cassandra/db/virtual/SystemViewsKeyspace.java | 2 +
.../apache/cassandra/schema/ColumnMetadata.java | 66 ++-
.../apache/cassandra/schema/KeyspaceMetadata.java | 11 +
.../apache/cassandra/schema/KeyspaceParams.java | 57 ++-
.../org/apache/cassandra/schema/TableMetadata.java | 45 +-
.../org/apache/cassandra/schema/TableParams.java | 27 +-
src/java/org/apache/cassandra/schema/Types.java | 165 +++++-
.../cassandra/tcm/membership/NodeVersion.java | 2 +-
.../cassandra/tcm/serialization/Version.java | 5 +
.../cql3/statements/DescribeStatementTest.java | 189 ++++++-
.../miscellaneous/CommentAndSecurityLabelTest.java | 558 +++++++++++++++++++++
.../db/virtual/SchemaMetadataTableTest.java | 336 +++++++++++++
.../org/apache/cassandra/schema/MockSchema.java | 20 +-
.../schema/SchemaMetadataSerializationTest.java | 290 +++++++++++
.../schema/createlike/CreateLikeTest.java | 79 ++-
.../createlike/CreateLikeWithSessionTest.java | 2 +-
.../ExportImportListCompressionDictionaryTest.java | 8 +-
47 files changed, 4749 insertions(+), 118 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 5c22973ed0..06b33e9846 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
5.1
+ * Introducing comments and security labels for schema elements (CASSANDRA-20943)
* Extend nodetool tablestats for dictionary memory usage (CASSANDRA-20940)
* Introduce separate GCInspector thresholds for concurrent GC events (CASSANDRA-20980)
* Reduce contention in MemtableAllocator.allocate (CASSANDRA-20226)
diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml
index 20476ee547..106f5e01e2 100644
--- a/conf/cassandra.yaml
+++ b/conf/cassandra.yaml
@@ -2610,6 +2610,14 @@ drop_compact_storage_enabled: false
# Defaults to true, which means that reconfiguration of password validator via JMX is possible.
#password_policy_reconfiguration_enabled: true
+# Maximum allowed length for comments on schema elements (keyspaces, tables, columns, types, type fields).
+# Comments exceeding this length will be rejected. Defaults to 128 characters.
+max_comment_length: 128
+
+# Maximum allowed length for security labels on schema elements (tables, columns).
+# Security labels exceeding this length will be rejected. Defaults to 48 characters.
+max_security_label_length: 48
+
# Guardrail to enable a CREATE or ALTER TABLE statement when default_time_to_live is set to 0
# and the table is using TimeWindowCompactionStrategy compaction or a subclass of it.
# It is suspicious to use default_time_to_live set to 0 with such compaction strategy.
diff --git a/doc/modules/cassandra/pages/developing/cql/ddl.adoc b/doc/modules/cassandra/pages/developing/cql/ddl.adoc
index d771f4e2bc..6b42eabc5f 100644
--- a/doc/modules/cassandra/pages/developing/cql/ddl.adoc
+++ b/doc/modules/cassandra/pages/developing/cql/ddl.adoc
@@ -803,3 +803,192 @@ 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
+
+The `COMMENT` 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
+----
+
+Example:
+
+[source,cql]
+----
+COMMENT ON COLUMN cycling.cyclist_name.id IS 'Unique identifier for each cyclist';
+----
+
+=== COMMENT ON TYPE
+
+Add or modify a comment on a user-defined type:
+
+[source,cql]
+----
+COMMENT ON TYPE keyspace_name.type_name IS 'comment text';
+COMMENT ON TYPE keyspace_name.type_name IS NULL; -- Remove comment
+----
+
+Example:
+
+[source,cql]
+----
+COMMENT ON TYPE cycling.address IS 'User-defined type for storing address information';
+----
+
+=== COMMENT ON FIELD
+
+Add or modify a comment on a field within a user-defined type:
+
+[source,cql]
+----
+COMMENT ON FIELD keyspace_name.type_name.field_name IS 'comment text';
+COMMENT ON FIELD keyspace_name.type_name.field_name IS NULL; -- Remove comment
+----
+
+Example:
+
+[source,cql]
+----
+COMMENT ON FIELD cycling.address.street IS 'Street address line';
+----
+
+NOTE: Comments can be removed by setting them to `NULL`. Comments are displayed when using `DESCRIBE` statements
+and are useful for documenting the purpose and structure of your schema elements.
+
+[[security-label-statement]]
+== SECURITY LABEL
+
+The `SECURITY LABEL` statement allows you to add security classification labels to schema elements.
+Security labels are stored in the schema metadata and displayed when using `DESCRIBE` statements.
+These labels can be used to mark data sensitivity levels or compliance requirements.
+
+=== SECURITY LABEL ON KEYSPACE
+
+Add or modify a security label on a keyspace:
+
+[source,cql]
+----
+SECURITY LABEL ON KEYSPACE keyspace_name IS 'label';
+SECURITY LABEL ON KEYSPACE keyspace_name IS NULL; -- Remove label
+----
+
+Example:
+
+[source,cql]
+----
+SECURITY LABEL ON KEYSPACE cycling IS 'CONFIDENTIAL';
+----
+
+=== SECURITY LABEL ON TABLE
+
+Add or modify a security label on a table:
+
+[source,cql]
+----
+SECURITY LABEL ON TABLE keyspace_name.table_name IS 'label';
+SECURITY LABEL ON TABLE keyspace_name.table_name IS NULL; -- Remove label
+----
+
+Example:
+
+[source,cql]
+----
+SECURITY LABEL ON TABLE cycling.cyclist_name IS 'PII';
+----
+
+=== SECURITY LABEL ON COLUMN
+
+Add or modify a security label on a column:
+
+[source,cql]
+----
+SECURITY LABEL ON COLUMN keyspace_name.table_name.column_name IS 'label';
+SECURITY LABEL ON COLUMN keyspace_name.table_name.column_name IS NULL; -- Remove label
+----
+
+Example:
+
+[source,cql]
+----
+SECURITY LABEL ON COLUMN cycling.cyclist_name.email IS 'PII-EMAIL';
+----
+
+=== SECURITY LABEL ON TYPE
+
+Add or modify a security label on a user-defined type:
+
+[source,cql]
+----
+SECURITY LABEL ON TYPE keyspace_name.type_name IS 'label';
+SECURITY LABEL ON TYPE keyspace_name.type_name IS NULL; -- Remove label
+----
+
+Example:
+
+[source,cql]
+----
+SECURITY LABEL ON TYPE cycling.address IS 'SENSITIVE';
+----
+
+=== SECURITY LABEL ON FIELD
+
+Add or modify a security label on a field within a user-defined type:
+
+[source,cql]
+----
+SECURITY LABEL ON FIELD keyspace_name.type_name.field_name IS 'label';
+SECURITY LABEL ON FIELD keyspace_name.type_name.field_name IS NULL; -- Remove label
+----
+
+Example:
+
+[source,cql]
+----
+SECURITY LABEL ON FIELD cycling.personal_info.ssn IS 'PII-SSN';
+----
+
+NOTE: Security labels can be removed by setting them to `NULL`. Security labels are displayed when using `DESCRIBE` statements
+and can be used in conjunction with custom authorization plugins or audit systems to enforce data access policies.
diff --git a/pylib/cqlshlib/cql3handling.py b/pylib/cqlshlib/cql3handling.py
index 4587727d97..a83c6535b8 100644
--- a/pylib/cqlshlib/cql3handling.py
+++ b/pylib/cqlshlib/cql3handling.py
@@ -299,6 +299,14 @@ JUNK ::= /([ \t\r\f\v]+|(--|[/][/])[^\n\r]*([\n\r]|$)|[/][*].*?[*][/])/ ;
| <alterTableStatement>
| <alterKeyspaceStatement>
| <alterUserTypeStatement>
+ | <commentOnKeyspaceStatement>
+ | <commentOnTableStatement>
+ | <commentOnColumnStatement>
+ | <commentOnTypeStatement>
+ | <securityLabelOnKeyspaceStatement>
+ | <securityLabelOnTableStatement>
+ | <securityLabelOnColumnStatement>
+ | <securityLabelOnTypeStatement>
;
<authenticationStatement> ::= <createUserStatement>
@@ -402,6 +410,8 @@ JUNK ::= /([ \t\r\f\v]+|(--|[/][/])[^\n\r]*([\n\r]|$)|[/][*].*?[*][/])/ ;
;
<propertyOrOption> ::= <property>
| "INDEXES"
+ | "COMMENTS"
+ | "SECURITY" "LABELS"
;
'''
@@ -1593,6 +1603,32 @@ syntax_rules += r'''
;
'''
+syntax_rules += r'''
+<commentOnKeyspaceStatement> ::= "COMMENT" "ON" "KEYSPACE" ks=<keyspaceName> "IS" comment=( <stringLiteral> | "NULL" )
+ ;
+
+<commentOnTableStatement> ::= "COMMENT" "ON" wat=( "COLUMNFAMILY" | "TABLE" ) cf=<columnFamilyName> "IS" comment=( <stringLiteral> | "NULL" )
+ ;
+
+<commentOnColumnStatement> ::= "COMMENT" "ON" "COLUMN" cf=<columnFamilyName> dot="." col=<cident> "IS" comment=( <stringLiteral> | "NULL" )
+ ;
+
+<commentOnTypeStatement> ::= "COMMENT" "ON" "TYPE" ut=<userTypeName> "IS" comment=( <stringLiteral> | "NULL" )
+ ;
+
+<securityLabelOnKeyspaceStatement> ::= "SECURITY" "LABEL" "ON" "KEYSPACE" ks=<keyspaceName> "IS" label=( <stringLiteral> | "NULL" )
+ ;
+
+<securityLabelOnTableStatement> ::= "SECURITY" "LABEL" "ON" wat=( "COLUMNFAMILY" | "TABLE" ) cf=<columnFamilyName> "IS" label=( <stringLiteral> | "NULL" )
+ ;
+
+<securityLabelOnColumnStatement> ::= "SECURITY" "LABEL" "ON" "COLUMN" cf=<columnFamilyName> dot="." col=<cident> "IS" label=( <stringLiteral> | "NULL" )
+ ;
+
+<securityLabelOnTypeStatement> ::= "SECURITY" "LABEL" "ON" "TYPE" ut=<userTypeName> "IS" label=( <stringLiteral> | "NULL" )
+ ;
+'''
+
syntax_rules += r'''
<username> ::= name=( <identifier> | <stringLiteral> )
;
diff --git a/pylib/cqlshlib/test/test_cqlsh_completion.py b/pylib/cqlshlib/test/test_cqlsh_completion.py
index 44650ae074..aa7904471b 100644
--- a/pylib/cqlshlib/test/test_cqlsh_completion.py
+++ b/pylib/cqlshlib/test/test_cqlsh_completion.py
@@ -166,10 +166,10 @@ class TestCqlshCompletion(CqlshCompletionCase):
cqlver = '3.1.6'
def test_complete_on_empty_string(self):
- self.trycompletions('', choices=('?', 'ADD', 'ALTER', 'BEGIN', 'CAPTURE', 'CONSISTENCY',
+ self.trycompletions('', choices=('?', 'ADD', 'ALTER', 'BEGIN', 'CAPTURE', 'COMMENT', 'CONSISTENCY',
'COPY', 'CREATE', 'DEBUG', 'DELETE', 'DESC', 'DESCRIBE',
'DROP', 'GRANT', 'HELP', 'INSERT', 'LIST', 'LOGIN', 'PAGING', 'REVOKE',
- 'SELECT', 'SHOW', 'SOURCE', 'TRACING', 'ELAPSED', 'EXPAND', 'SERIAL', 'TRUNCATE',
+ 'SECURITY', 'SELECT', 'SHOW', 'SOURCE', 'TRACING', 'ELAPSED', 'EXPAND', 'SERIAL', 'TRUNCATE',
'UPDATE', 'USE', 'exit', 'quit', 'CLEAR', 'CLS', 'history'))
def test_complete_command_words(self):
@@ -288,10 +288,10 @@ class TestCqlshCompletion(CqlshCompletionCase):
self.trycompletions(
("INSERT INTO twenty_rows_composite_table (a, b, c) "
"VALUES ( 'eggs', 'sausage', 'spam');"),
- choices=['?', 'ADD', 'ALTER', 'BEGIN', 'CAPTURE', 'CONSISTENCY', 'COPY',
+ choices=['?', 'ADD', 'ALTER', 'BEGIN', 'CAPTURE', 'COMMENT', 'CONSISTENCY', 'COPY',
'CREATE', 'DEBUG', 'DELETE', 'DESC', 'DESCRIBE', 'DROP',
'ELAPSED', 'EXPAND', 'GRANT', 'HELP', 'INSERT', 'LIST', 'LOGIN', 'PAGING',
- 'REVOKE', 'SELECT', 'SHOW', 'SOURCE', 'SERIAL', 'TRACING',
+ 'REVOKE', 'SECURITY', 'SELECT', 'SHOW', 'SOURCE', 'SERIAL', 'TRACING',
'TRUNCATE', 'UPDATE', 'USE', 'exit', 'history', 'quit',
'CLEAR', 'CLS'])
@@ -856,7 +856,7 @@ class TestCqlshCompletion(CqlshCompletionCase):
'min_index_interval',
'speculative_retry', 'additional_write_policy',
'cdc', 'read_repair',
- 'INDEXES'])
+ 'INDEXES', 'COMMENTS', 'SECURITY'])
self.trycompletions('CREATE TABLE new_table LIKE old_table WITH INDEXES ',
choices=[';' , '=', 'AND'])
self.trycompletions('CREATE TABLE ' + 'new_table LIKE old_table WITH bloom_filter_fp_chance ',
@@ -909,7 +909,7 @@ class TestCqlshCompletion(CqlshCompletionCase):
'min_index_interval',
'speculative_retry', 'additional_write_policy',
'cdc', 'read_repair',
- 'INDEXES'])
+ 'INDEXES', 'COMMENTS', 'SECURITY'])
self.trycompletions('CREATE TABLE ' + "new_table LIKE old_table WITH compaction = "
+ "{'class': 'TimeWindowCompactionStrategy', '",
choices=['compaction_window_unit', 'compaction_window_size',
diff --git a/src/antlr/Lexer.g b/src/antlr/Lexer.g
index b3ae1aba49..c7065f7351 100644
--- a/src/antlr/Lexer.g
+++ b/src/antlr/Lexer.g
@@ -96,6 +96,7 @@ K_KEYSPACE: ( K E Y S P A C E
K_KEYSPACES: K E Y S P A C E S;
K_COLUMNFAMILY:( C O L U M N F A M I L Y
| T A B L E );
+K_COLUMN: C O L U M N;
K_TABLES: ( C O L U M N F A M I L I E S
| T A B L E S );
K_MATERIALIZED:M A T E R I A L I Z E D;
@@ -209,6 +210,8 @@ K_TUPLE: T U P L E;
K_TRIGGER: T R I G G E R;
K_STATIC: S T A T I C;
K_FROZEN: F R O Z E N;
+K_FOR: F O R;
+K_FIELD: F I E L D;
K_FUNCTION: F U N C T I O N;
K_FUNCTIONS: F U N C T I O N S;
@@ -237,6 +240,12 @@ K_SELECT_MASKED: S E L E C T '_' M A S K E D;
K_VECTOR: V E C T O R;
K_ANN: A N N;
+K_COMMENT: C O M M E N T;
+K_COMMENTS: C O M M E N T S;
+K_SECURITY: S E C U R I T Y;
+K_LABEL: L A B E L;
+K_LABELS: L A B E L S;
+
// Case-insensitive alpha characters
fragment A: ('a'|'A');
fragment B: ('b'|'B');
diff --git a/src/antlr/Parser.g b/src/antlr/Parser.g
index ea52617e4c..8794d00d02 100644
--- a/src/antlr/Parser.g
+++ b/src/antlr/Parser.g
@@ -284,6 +284,16 @@ cqlStatement returns [CQLStatement.Raw stmt]
| st45=copyTableStatement { $stmt = st45; }
| st46=batchTxnStatement { $stmt = st46; }
| st47=letStatement { $stmt = st47; }
+ | st48=commentOnKeyspaceStatement { $stmt = st48; }
+ | st49=securityLabelOnKeyspaceStatement { $stmt = st49; }
+ | st50=commentOnTableStatement { $stmt = st50; }
+ | st51=securityLabelOnTableStatement { $stmt = st51; }
+ | st52=commentOnColumnStatement { $stmt = st52; }
+ | st53=securityLabelOnColumnStatement { $stmt = st53; }
+ | st54=commentOnUserTypeStatement { $stmt = st54; }
+ | st55=securityLabelOnUserTypeStatement { $stmt = st55; }
+ | st56=commentOnUserTypeFieldStatement { $stmt = st56; }
+ | st57=securityLabelOnUserTypeFieldStatement { $stmt = st57; }
;
/*
@@ -1056,12 +1066,14 @@ copyTableStatement returns [CopyTableStatement.Raw stmt]
;
propertyOrOption[CopyTableStatement.Raw stmt]
- : tableLikeSingleOption[stmt]
+ : likeOption[stmt]
| property[stmt.attrs]
;
-tableLikeSingleOption[CopyTableStatement.Raw stmt]
- : K_INDEXES {$stmt.withLikeOption(CopyTableStatement.CreateLikeOption.INDEXES);}
+likeOption[CopyTableStatement.Raw stmt]
+ : K_INDEXES {$stmt.addLikeOption(CopyTableStatement.CreateLikeOption.INDEXES);}
+ | K_COMMENTS {$stmt.addLikeOption(CopyTableStatement.CreateLikeOption.COMMENTS);}
+ | K_SECURITY K_LABELS {$stmt.addLikeOption(CopyTableStatement.CreateLikeOption.SECURITY_LABELS);}
;
/**
@@ -1275,6 +1287,89 @@ dropKeyspaceStatement returns [DropKeyspaceStatement.Raw stmt]
: K_DROP K_KEYSPACE (K_IF K_EXISTS { ifExists = true; } )? ks=keyspaceName { $stmt = new DropKeyspaceStatement.Raw(ks, ifExists); }
;
+/**
+ * COMMENT ON KEYSPACE <keyspace> IS <comment>;
+ */
+commentOnKeyspaceStatement returns [CommentOnKeyspaceStatement.Raw stmt]
+ : K_COMMENT K_ON K_KEYSPACE ks=keyspaceName K_IS (comment=STRING_LITERAL | K_NULL) { $stmt = new CommentOnKeyspaceStatement.Raw(ks, comment != null ? $comment.text : null); }
+ ;
+
+/**
+ * SECURITY LABEL [FOR <provider>] ON KEYSPACE <keyspace> IS <label>;
+ */
+securityLabelOnKeyspaceStatement returns [SecurityLabelOnKeyspaceStatement.Raw stmt]
+ @init { String provider = null; }
+ : K_SECURITY K_LABEL (K_FOR prov=noncol_ident { provider = prov.toString(); })? K_ON K_KEYSPACE ks=keyspaceName K_IS (label=STRING_LITERAL | K_NULL) { $stmt = new SecurityLabelOnKeyspaceStatement.Raw(ks, label != null ? $label.text : null, provider); }
+ ;
+
+/**
+ * COMMENT ON TABLE <table> IS <comment>;
+ */
+commentOnTableStatement returns [CommentOnTableStatement.Raw stmt]
+ : K_COMMENT K_ON K_COLUMNFAMILY cf=columnFamilyName K_IS (comment=STRING_LITERAL | K_NULL) { $stmt = new CommentOnTableStatement.Raw(cf, comment != null ? $comment.text : null); }
+ ;
+
+/**
+ * SECURITY LABEL [FOR <provider>] ON TABLE <table> IS <label>;
+ */
+securityLabelOnTableStatement returns [SecurityLabelOnTableStatement.Raw stmt]
+ @init { String provider = null; }
+ : K_SECURITY K_LABEL (K_FOR prov=noncol_ident { provider = prov.toString(); })? K_ON K_COLUMNFAMILY cf=columnFamilyName K_IS (label=STRING_LITERAL | K_NULL) { $stmt = new SecurityLabelOnTableStatement.Raw(cf, label != null ? $label.text : null, provider); }
+ ;
+
+/**
+ * COMMENT ON COLUMN <table>.<column> IS <comment>;
+ * COMMENT ON COLUMN <keyspace>.<table>.<column> IS <comment>;
+ */
+commentOnColumnStatement returns [CommentOnColumnStatement.Raw stmt]
+ : K_COMMENT K_ON K_COLUMN columnRef=columnReference K_IS (comment=STRING_LITERAL | K_NULL)
+ { $stmt = new CommentOnColumnStatement.Raw($columnRef.table, $columnRef.column, comment != null ? $comment.text : null); }
+ ;
+
+/**
+ * SECURITY LABEL [FOR <provider>] ON COLUMN <table>.<column> IS <label>;
+ * SECURITY LABEL [FOR <provider>] ON COLUMN <keyspace>.<table>.<column> IS <label>;
+ */
+securityLabelOnColumnStatement returns [SecurityLabelOnColumnStatement.Raw stmt]
+ @init { String provider = null; }
+ : K_SECURITY K_LABEL (K_FOR prov=noncol_ident { provider = prov.toString(); })? K_ON K_COLUMN columnRef=columnReference K_IS (label=STRING_LITERAL | K_NULL)
+ { $stmt = new SecurityLabelOnColumnStatement.Raw($columnRef.table, $columnRef.column, label != null ? $label.text : null, provider); }
+ ;
+
+/**
+ * COMMENT ON TYPE <type> IS <comment>;
+ */
+commentOnUserTypeStatement returns [CommentOnUserTypeStatement.Raw stmt]
+ : K_COMMENT K_ON K_TYPE tn=userTypeName K_IS (comment=STRING_LITERAL | K_NULL) { $stmt = new CommentOnUserTypeStatement.Raw(tn, comment != null ? $comment.text : null); }
+ ;
+
+/**
+ * SECURITY LABEL [FOR <provider>] ON TYPE <type> IS <label>;
+ */
+securityLabelOnUserTypeStatement returns [SecurityLabelOnUserTypeStatement.Raw stmt]
+ @init { String provider = null; }
+ : K_SECURITY K_LABEL (K_FOR prov=noncol_ident { provider = prov.toString(); })? K_ON K_TYPE tn=userTypeName K_IS (label=STRING_LITERAL | K_NULL) { $stmt = new SecurityLabelOnUserTypeStatement.Raw(tn, label != null ? $label.text : null, provider); }
+ ;
+
+/**
+ * COMMENT ON FIELD <type>.<field> IS <comment>;
+ * COMMENT ON FIELD <keyspace>.<type>.<field> IS <comment>;
+ */
+commentOnUserTypeFieldStatement returns [CommentOnUserTypeFieldStatement.Raw stmt]
+ : K_COMMENT K_ON K_FIELD typeFieldRef=typeFieldReference K_IS (comment=STRING_LITERAL | K_NULL)
+ { $stmt = new CommentOnUserTypeFieldStatement.Raw($typeFieldRef.typeName, $typeFieldRef.field, comment != null ? $comment.text : null); }
+ ;
+
+/**
+ * SECURITY LABEL [FOR <provider>] ON FIELD <type>.<field> IS <label>;
+ * SECURITY LABEL [FOR <provider>] ON FIELD <keyspace>.<type>.<field> IS <label>;
+ */
+securityLabelOnUserTypeFieldStatement returns [SecurityLabelOnUserTypeFieldStatement.Raw stmt]
+ @init { String provider = null; }
+ : K_SECURITY K_LABEL (K_FOR prov=noncol_ident { provider = prov.toString(); })? K_ON K_FIELD typeFieldRef=typeFieldReference K_IS (label=STRING_LITERAL | K_NULL)
+ { $stmt = new SecurityLabelOnUserTypeFieldStatement.Raw($typeFieldRef.typeName, $typeFieldRef.field, label != null ? $label.text : null, provider); }
+ ;
+
/**
* DROP TABLE [IF EXISTS] <table>;
*/
@@ -1784,6 +1879,22 @@ columnFamilyName returns [QualifiedName name]
: (ksName[name] '.')? cfName[name]
;
+columnReference returns [QualifiedName table, ColumnIdentifier column]
+ @init { $table = new QualifiedName(); }
+ : cfName[$table] '.' col=cident
+ { $column = col; }
+ | ksName[$table] '.' cfName[$table] '.' col=cident
+ { $column = col; }
+ ;
+
+typeFieldReference returns [UTName typeName, FieldIdentifier field]
+ : ut=non_type_ident '.' fld=fident
+ { $typeName = new UTName(null, ut); $field = fld; }
+ | ks=noncol_ident '.' ut=non_type_ident '.' fld=fident
+ { $typeName = new UTName(ks, ut); $field = fld; }
+ ;
+
+
userTypeName returns [UTName name]
: (ks=noncol_ident '.')? ut=non_type_ident { $name = new UTName(ks, ut); }
;
@@ -2324,6 +2435,7 @@ basic_unreserved_keyword returns [String str]
| K_ONLY
| K_STATIC
| K_FROZEN
+ | K_FOR
| K_TUPLE
| K_FUNCTION
| K_FUNCTIONS
@@ -2362,5 +2474,12 @@ basic_unreserved_keyword returns [String str]
| K_LET
| K_THEN
| K_TRANSACTION
+ | K_COMMENT
+ | K_COMMENTS
+ | K_SECURITY
+ | K_LABEL
+ | K_LABELS
+ | K_FIELD
+ | K_COLUMN
) { $str = $k.text; }
;
diff --git a/src/java/org/apache/cassandra/audit/AuditLogEntryType.java b/src/java/org/apache/cassandra/audit/AuditLogEntryType.java
index 4ee3348055..ff61f2f2a7 100644
--- a/src/java/org/apache/cassandra/audit/AuditLogEntryType.java
+++ b/src/java/org/apache/cassandra/audit/AuditLogEntryType.java
@@ -64,6 +64,14 @@ public enum AuditLogEntryType
USE_KEYSPACE(AuditLogEntryCategory.OTHER),
DESCRIBE(AuditLogEntryCategory.OTHER),
TRANSACTION(AuditLogEntryCategory.TRANSACTION),
+ COMMENT_KEYSPACE(AuditLogEntryCategory.DDL),
+ COMMENT_TABLE(AuditLogEntryCategory.DDL),
+ COMMENT_COLUMN(AuditLogEntryCategory.DDL),
+ COMMENT_TYPE(AuditLogEntryCategory.DDL),
+ SECURITY_LABEL_KEYSPACE(AuditLogEntryCategory.DDL),
+ SECURITY_LABEL_TABLE(AuditLogEntryCategory.DDL),
+ SECURITY_LABEL_COLUMN(AuditLogEntryCategory.DDL),
+ SECURITY_LABEL_TYPE(AuditLogEntryCategory.DDL),
/*
* Common Audit Log Entry Types
diff --git a/src/java/org/apache/cassandra/config/Config.java b/src/java/org/apache/cassandra/config/Config.java
index 7ea67ebc4c..986346da41 100644
--- a/src/java/org/apache/cassandra/config/Config.java
+++ b/src/java/org/apache/cassandra/config/Config.java
@@ -108,6 +108,9 @@ public class Config
public volatile DurationSpec.IntMillisecondsBound credentials_update_interval = null;
public volatile boolean credentials_cache_active_update = false;
+ public int max_comment_length = 128;
+ public int max_security_label_length = 48;
+
/* Hashing strategy Random or OPHF */
public String partitioner;
diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
index dec260f9b9..bf0beadd0e 100644
--- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
+++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
@@ -2212,6 +2212,16 @@ public class DatabaseDescriptor
conf.credentials_cache_active_update = update;
}
+ public static int getMaxCommentLength()
+ {
+ return conf.max_comment_length;
+ }
+
+ public static int getMaxSecurityLabelLength()
+ {
+ return conf.max_security_label_length;
+ }
+
public static int getMaxValueSize()
{
return Ints.saturatedCast(conf.max_value_size.toMebibytes() * 1024L * 1024);
diff --git a/src/java/org/apache/cassandra/cql3/ColumnSpecification.java b/src/java/org/apache/cassandra/cql3/ColumnSpecification.java
index 8cf869bd38..863f9eb5b8 100644
--- a/src/java/org/apache/cassandra/cql3/ColumnSpecification.java
+++ b/src/java/org/apache/cassandra/cql3/ColumnSpecification.java
@@ -28,17 +28,28 @@ import java.util.Iterator;
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;
}
/**
@@ -49,7 +60,7 @@ public class ColumnSpecification
*/
public ColumnSpecification withAlias(ColumnIdentifier alias)
{
- return new ColumnSpecification(ksName, cfName, alias, type);
+ return new ColumnSpecification(ksName, cfName, alias, type, comment, securityLabel);
}
public boolean isReversedType()
@@ -100,6 +111,8 @@ public class ColumnSpecification
return MoreObjects.toStringHelper(this)
.add("name", name)
.add("type", type)
+ .add("comment", comment)
+ .add("securityLabel", securityLabel)
.toString();
}
}
diff --git a/src/java/org/apache/cassandra/cql3/SchemaElement.java b/src/java/org/apache/cassandra/cql3/SchemaElement.java
index 4fd96f84b5..6544bdc5e4 100644
--- a/src/java/org/apache/cassandra/cql3/SchemaElement.java
+++ b/src/java/org/apache/cassandra/cql3/SchemaElement.java
@@ -96,4 +96,23 @@ public interface SchemaElement
* @return a CQL representation of this element
*/
String toCqlString(boolean withWarnings, boolean withInternals, boolean ifNotExists);
+
+ /**
+ * Returns a complete CQL representation of this schema element for DESCRIBE statements.
+ * <p>
+ * This method returns the full schema definition including the CREATE statement and any
+ * associated metadata such as comments and security labels. It is specifically designed
+ * for use by DESCRIBE statements to provide a comprehensive view of the schema element.
+ * </p>
+ *
+ * @param withWarnings if commented warnings should be included
+ * @param withInternals if the internals part of the CQL should be exposed
+ * @param ifNotExists if "IF NOT EXISTS" should be included
+ * @return a complete CQL representation of this element including comments and security labels
+ * @see org.apache.cassandra.cql3.statements.SchemaDescriptionsUtil
+ */
+ default String describe(boolean withWarnings, boolean withInternals, boolean ifNotExists)
+ {
+ return toCqlString(withWarnings, withInternals, ifNotExists);
+ }
}
diff --git a/src/java/org/apache/cassandra/cql3/statements/DescribeStatement.java b/src/java/org/apache/cassandra/cql3/statements/DescribeStatement.java
index 64b7862d6d..6ee6561cc1 100644
--- a/src/java/org/apache/cassandra/cql3/statements/DescribeStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/DescribeStatement.java
@@ -374,7 +374,7 @@ public abstract class DescribeStatement<T> extends CQLStatement.Raw implements C
return ImmutableList.of(bytes(element.elementKeyspaceQuotedIfNeeded()),
bytes(element.elementType().toString()),
bytes(element.elementNameQuotedIfNeeded()),
- bytes(element.toCqlString(true, withInternals, false)));
+ bytes(element.describe(true, withInternals, false)));
}
};
}
@@ -424,7 +424,7 @@ public abstract class DescribeStatement<T> extends CQLStatement.Raw implements C
return ImmutableList.of(bytes(element.elementKeyspaceQuotedIfNeeded()),
bytes(element.elementType().toString()),
bytes(element.elementNameQuotedIfNeeded()),
- bytes(element.toCqlString(true, withInternals, false)));
+ bytes(element.describe(true, withInternals, false)));
}
}
diff --git a/src/java/org/apache/cassandra/cql3/statements/SchemaDescriptionsUtil.java b/src/java/org/apache/cassandra/cql3/statements/SchemaDescriptionsUtil.java
new file mode 100644
index 0000000000..e524f4c2eb
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/statements/SchemaDescriptionsUtil.java
@@ -0,0 +1,273 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.statements;
+
+import org.apache.commons.lang3.StringUtils;
+
+import org.apache.cassandra.db.marshal.UserType;
+import org.apache.cassandra.schema.ColumnMetadata;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.TableMetadata;
+
+/**
+ * Utility class for appending COMMENT and SECURITY LABEL statements to schema element descriptions.
+ * <p>
+ * This class provides methods to append CQL statements for comments and security labels on various
+ * schema elements (keyspaces, tables, columns, and types) to their CREATE statement output.
+ * These are typically used by DESCRIBE statements to show the complete schema definition including
+ * any associated metadata.
+ * </p>
+ */
+public class SchemaDescriptionsUtil
+{
+ private static final String COMMENT_DESCRIPTION_TYPE = "COMMENT";
+ private static final String SECURITY_LABEL_DESCRIPTION_TYPE = "SECURITY LABEL";
+ private static final String KEYSPACE_SCHEMA_ELEMENT = "KEYSPACE";
+ private static final String TABLE_SCHEMA_ELEMENT = "TABLE";
+ private static final String COLUMN_SCHEMA_ELEMENT = "COLUMN";
+ private static final String TYPE_SCHEMA_ELEMENT = "TYPE";
+ private static final String FIELD_SCHEMA_ELEMENT = "FIELD";
+
+ /**
+ * Maximum length for comments and security labels.
+ * This limit helps prevent excessive memory usage and storage overhead.
+ */
+
+ private SchemaDescriptionsUtil()
+ {
+ }
+
+ /**
+ * Appends a COMMENT ON KEYSPACE statement to the builder if the keyspace has a comment.
+ *
+ * @param builder the StringBuilder to append to
+ * @param keyspaceMetadata the keyspace metadata containing the comment
+ */
+ public static void appendCommentOnKeyspace(StringBuilder builder, KeyspaceMetadata keyspaceMetadata)
+ {
+ addDescription(builder, COMMENT_DESCRIPTION_TYPE, KEYSPACE_SCHEMA_ELEMENT, keyspaceMetadata.name, keyspaceMetadata.params.comment);
+ }
+
+ /**
+ * Appends a SECURITY LABEL ON KEYSPACE statement to the builder if the keyspace has a security label.
+ *
+ * @param builder the StringBuilder to append to
+ * @param keyspaceMetadata the keyspace metadata containing the security label
+ */
+ public static void appendSecurityLabelOnKeyspace(StringBuilder builder, KeyspaceMetadata keyspaceMetadata)
+ {
+ addDescription(builder, SECURITY_LABEL_DESCRIPTION_TYPE, KEYSPACE_SCHEMA_ELEMENT, keyspaceMetadata.name, keyspaceMetadata.params.securityLabel);
+ }
+
+ /**
+ * Appends a COMMENT ON TABLE statement to the builder if the table has a comment.
+ *
+ * @param builder the StringBuilder to append to
+ * @param tableMetadata the table metadata containing the comment
+ */
+ public static void appendCommentOnTable(StringBuilder builder, TableMetadata tableMetadata)
+ {
+ String schemaElement = String.format("%s.%s", tableMetadata.keyspace, tableMetadata.name);
+ addDescription(builder, COMMENT_DESCRIPTION_TYPE, TABLE_SCHEMA_ELEMENT, schemaElement, tableMetadata.params.comment);
+ }
+
+ /**
+ * Appends SECURITY LABEL ON TABLE statement and COMMENT/SECURITY LABEL ON COLUMN statements
+ * to the builder if the table or any of its columns have security labels or comments.
+ *
+ * @param builder the StringBuilder to append to
+ * @param tableMetadata the table metadata containing the security label and columns
+ */
+ public static void appendSecurityLabelOnTable(StringBuilder builder, TableMetadata tableMetadata)
+ {
+ String schemaElement = String.format("%s.%s", tableMetadata.keyspace, tableMetadata.name);
+ addDescription(builder, SECURITY_LABEL_DESCRIPTION_TYPE, TABLE_SCHEMA_ELEMENT, schemaElement, tableMetadata.params.securityLabel);
+
+ // Add comments and security labels on columns
+ for (ColumnMetadata column: tableMetadata.columns())
+ {
+ appendCommentOnColumn(builder, tableMetadata, column);
+ appendSecurityLabelOnColumn(builder, tableMetadata, column);
+ }
+ }
+
+ /**
+ * Appends a COMMENT ON TYPE statement to the builder if the type has a comment.
+ *
+ * @param builder the StringBuilder to append to
+ * @param userType the user-defined type containing the comment
+ */
+ public static void appendCommentOnType(StringBuilder builder, UserType userType)
+ {
+ String schemaElement = String.format("%s.%s", userType.keyspace, userType.getNameAsString());
+ addDescription(builder, COMMENT_DESCRIPTION_TYPE, TYPE_SCHEMA_ELEMENT, schemaElement, userType.comment);
+
+ // Add comments on fields
+ for (int i = 0; i < userType.size(); i++)
+ appendCommentOnField(builder, userType, i);
+ }
+
+ /**
+ * Appends SECURITY LABEL ON TYPE statement and COMMENT/SECURITY LABEL ON FIELD statements
+ * to the builder if the type or any of its fields have security labels or comments.
+ *
+ * @param builder the StringBuilder to append to
+ * @param userType the user-defined type containing the security label and fields
+ */
+ public static void appendSecurityLabelOnType(StringBuilder builder, UserType userType)
+ {
+ String schemaElement = String.format("%s.%s", userType.keyspace, userType.getNameAsString());
+ addDescription(builder, SECURITY_LABEL_DESCRIPTION_TYPE, TYPE_SCHEMA_ELEMENT, schemaElement, userType.securityLabel);
+
+ // Add security labels on fields
+ for (int i = 0; i < userType.size(); i++)
+ appendSecurityLabelOnField(builder, userType, i);
+ }
+
+ /* Helper methods */
+
+ /**
+ * Appends a COMMENT ON COLUMN statement to the builder if the column has a comment.
+ *
+ * @param builder the StringBuilder to append to
+ * @param tableMetadata the table containing the column
+ * @param columnMetadata the column metadata containing the comment
+ */
+ private static void appendCommentOnColumn(StringBuilder builder, TableMetadata tableMetadata, ColumnMetadata columnMetadata)
+ {
+ appendColumnDescription(builder, COMMENT_DESCRIPTION_TYPE, tableMetadata, columnMetadata, columnMetadata.comment);
+ }
+
+ /**
+ * Appends a SECURITY LABEL ON COLUMN statement to the builder if the column has a security label.
+ *
+ * @param builder the StringBuilder to append to
+ * @param tableMetadata the table containing the column
+ * @param columnMetadata the column metadata containing the security label
+ */
+ private static void appendSecurityLabelOnColumn(StringBuilder builder, TableMetadata tableMetadata, ColumnMetadata columnMetadata)
+ {
+ appendColumnDescription(builder, SECURITY_LABEL_DESCRIPTION_TYPE, tableMetadata, columnMetadata, columnMetadata.securityLabel);
+ }
+
+ /**
+ * Helper method to append a column-level comment or security label statement.
+ *
+ * @param builder the StringBuilder to append to
+ * @param descriptionType the type of description (COMMENT or SECURITY LABEL)
+ * @param tableMetadata the table containing the column
+ * @param columnMetadata the column metadata
+ * @param description the comment or security label text
+ */
+ private static void appendColumnDescription(StringBuilder builder, String descriptionType, TableMetadata tableMetadata, ColumnMetadata columnMetadata, String description)
+ {
+ String schemaElement = String.format("%s.%s.%s", tableMetadata.keyspace, tableMetadata.name, columnMetadata.name.toCQLString());
+ addDescription(builder, descriptionType, COLUMN_SCHEMA_ELEMENT, schemaElement, description);
+ }
+
+ /**
+ * Appends a COMMENT ON FIELD statement to the builder if the field has a comment.
+ *
+ * @param builder the StringBuilder to append to
+ * @param userType the user-defined type containing the field
+ * @param fieldIndex the index of the field
+ */
+ private static void appendCommentOnField(StringBuilder builder, UserType userType, int fieldIndex)
+ {
+ String comment = userType.fieldComment(userType.fieldName(fieldIndex));
+ appendFieldDescription(builder, COMMENT_DESCRIPTION_TYPE, userType, fieldIndex, comment);
+ }
+
+ /**
+ * Appends a SECURITY LABEL ON FIELD statement to the builder if the field has a security label.
+ *
+ * @param builder the StringBuilder to append to
+ * @param userType the user-defined type containing the field
+ * @param fieldIndex the index of the field
+ */
+ private static void appendSecurityLabelOnField(StringBuilder builder, UserType userType, int fieldIndex)
+ {
+ String securityLabel = userType.fieldSecurityLabel(userType.fieldName(fieldIndex));
+ appendFieldDescription(builder, SECURITY_LABEL_DESCRIPTION_TYPE, userType, fieldIndex, securityLabel);
+ }
+
+ /**
+ * Helper method to append a field-level comment or security label statement.
+ *
+ * @param builder the StringBuilder to append to
+ * @param descriptionType the type of description (COMMENT or SECURITY LABEL)
+ * @param userType the user-defined type containing the field
+ * @param fieldIndex the index of the field
+ * @param description the comment or security label text
+ */
+ private static void appendFieldDescription(StringBuilder builder, String descriptionType, UserType userType, int fieldIndex, String description)
+ {
+ String schemaElement = String.format("%s.%s.%s", userType.keyspace, userType.getNameAsString(), userType.fieldName(fieldIndex).toString());
+ addDescription(builder, descriptionType, FIELD_SCHEMA_ELEMENT, schemaElement, description);
+ }
+
+ /**
+ * Helper method to append a comment or security label statement to the builder.
+ * <p>
+ * Generates CQL statements in the format:
+ * {@code <descriptionType> ON <schemaElement> <schemaElementName> IS '<comment>';}
+ * </p>
+ *
+ * @param builder the StringBuilder to append to
+ * @param descriptionType the type of description (COMMENT or SECURITY LABEL)
+ * @param schemaElement the type of schema element (KEYSPACE, TABLE, COLUMN, or TYPE)
+ * @param schemaElementName the fully qualified name of the schema element
+ * @param description the comment or security label text (null or empty values are ignored)
+ */
+ private static void addDescription(StringBuilder builder, String descriptionType, String schemaElement, String schemaElementName, String description)
+ {
+ if (StringUtils.isEmpty(description))
+ {
+ return;
+ }
+
+ /**
+ * CQL Injection Prevention: Single quotes in comments and security labels must be escaped.
+ *
+ * Example of unescaped input creating a CQL injection vulnerability:
+ * COMMENT ON COLUMN keyspace1.tbl.key IS 'a'; DROP TABLE keyspace1.tbl;';
+ *
+ * Without proper escaping, this would execute as two separate statements:
+ * 1. COMMENT ON COLUMN keyspace1.tbl.key IS 'a';
+ * 2. DROP TABLE keyspace1.tbl;';
+ *
+ * This is particularly dangerous when operators use DESCRIBE statements to export
+ * schema from one database and import it into another, as malicious comments could
+ * execute arbitrary CQL commands in the target database.
+ */
+ String escapedSingleQuoteDescription = StringUtils.replace(description, "'", "''");
+ builder.append("\n")
+ .append(descriptionType)
+ .append(" ON ")
+ .append(schemaElement)
+ .append(" ")
+ .append(schemaElementName)
+ .append(" IS ")
+ .append('\'')
+ .append(escapedSingleQuoteDescription)
+ .append('\'')
+ .append(';');
+ }
+
+}
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CommentOnColumnStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CommentOnColumnStatement.java
new file mode 100644
index 0000000000..0bcb6088c8
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/CommentOnColumnStatement.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.cql3.statements.schema;
+
+import org.apache.cassandra.audit.AuditLogContext;
+import org.apache.cassandra.audit.AuditLogEntryType;
+import org.apache.cassandra.auth.Permission;
+import org.apache.cassandra.cql3.CQLStatement;
+import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.cql3.QualifiedName;
+import org.apache.cassandra.schema.ColumnMetadata;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.Keyspaces;
+import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.tcm.ClusterMetadata;
+import org.apache.cassandra.transport.Event.SchemaChange;
+import org.apache.cassandra.transport.Event.SchemaChange.Change;
+import org.apache.cassandra.transport.Event.SchemaChange.Target;
+
+/**
+ * Handles the execution of COMMENT ON COLUMN CQL statements.
+ * <p>
+ * This statement allows users to add descriptive comments to individual columns for documentation purposes.
+ * Comments are stored in the column metadata and displayed when using DESCRIBE TABLE.
+ * </p>
+ * <p>
+ * Syntax: {@code COMMENT ON COLUMN [keyspace_name.]table_name.column_name IS 'comment text';}
+ * </p>
+ * <p>
+ * Comments can be removed by setting them to an empty string or null.
+ * </p>
+ *
+ * @see CommentOnKeyspaceStatement
+ * @see CommentOnTableStatement
+ * @see CommentOnUserTypeStatement
+ */
+public final class CommentOnColumnStatement extends SchemaDescriptionStatement
+{
+ private final String tableName;
+ private final ColumnIdentifier columnName;
+
+ public CommentOnColumnStatement(String keyspaceName, String tableName, ColumnIdentifier columnName, String comment)
+ {
+ super(keyspaceName, comment, DescriptionType.COMMENT);
+ this.tableName = tableName;
+ this.columnName = columnName;
+ }
+
+ @Override
+ public Keyspaces apply(ClusterMetadata metadata)
+ {
+ Keyspaces schema = metadata.schema.getKeyspaces();
+ TableMetadata table = validateAndGetTable(schema, tableName);
+ ColumnMetadata columnMetadata = table.getColumn(columnName);
+ if (null == columnMetadata)
+ throw ire("Column '%s' doesn't exist in table '%s.%s'", columnName, keyspaceName, tableName);
+
+ KeyspaceMetadata keyspace = schema.getNullable(keyspaceName);
+ TableMetadata newTable = table.unbuild().alterColumnComment(columnName, effectiveDescription()).build();
+ KeyspaceMetadata newKeyspace = keyspace.withSwapped(keyspace.tables.withSwapped(newTable));
+
+ return schema.withAddedOrUpdated(newKeyspace);
+ }
+
+ @Override
+ SchemaChange schemaChangeEvent(KeyspacesDiff diff)
+ {
+ return new SchemaChange(Change.UPDATED, Target.TABLE, keyspaceName, tableName);
+ }
+
+ @Override
+ public void authorize(ClientState client)
+ {
+ client.ensureTablePermission(keyspaceName, tableName, Permission.ALTER);
+ }
+
+ @Override
+ public AuditLogContext getAuditLogContext()
+ {
+ return new AuditLogContext(AuditLogEntryType.COMMENT_COLUMN, keyspaceName, tableName);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s (%s.%s.%s, %s)", getClass().getSimpleName(), keyspaceName, tableName, columnName, description);
+ }
+
+ public static final class Raw extends CQLStatement.Raw
+ {
+ private final QualifiedName tableName;
+ private final ColumnIdentifier columnName;
+ private final String comment;
+
+ public Raw(QualifiedName tableName, ColumnIdentifier columnName, String comment)
+ {
+ this.tableName = tableName;
+ this.columnName = columnName;
+ this.comment = comment;
+ }
+
+ @Override
+ public CommentOnColumnStatement prepare(ClientState state)
+ {
+ String keyspaceName = tableName.hasKeyspace() ? tableName.getKeyspace() : state.getKeyspace();
+ return new CommentOnColumnStatement(keyspaceName,
+ tableName.getName(),
+ columnName,
+ comment);
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CommentOnKeyspaceStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CommentOnKeyspaceStatement.java
new file mode 100644
index 0000000000..5def5af758
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/CommentOnKeyspaceStatement.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.cql3.statements.schema;
+
+import org.apache.cassandra.audit.AuditLogContext;
+import org.apache.cassandra.audit.AuditLogEntryType;
+import org.apache.cassandra.auth.Permission;
+import org.apache.cassandra.cql3.CQLStatement;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.schema.Keyspaces;
+import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.tcm.ClusterMetadata;
+import org.apache.cassandra.transport.Event.SchemaChange;
+import org.apache.cassandra.transport.Event.SchemaChange.Change;
+
+/**
+ * Handles the execution of COMMENT ON KEYSPACE CQL statements.
+ * <p>
+ * This statement allows users to add descriptive comments to keyspaces for documentation purposes.
+ * Comments are stored in the keyspace metadata and displayed when using DESCRIBE KEYSPACE.
+ * </p>
+ * <p>
+ * Syntax: {@code COMMENT ON KEYSPACE keyspace_name IS 'comment text';}
+ * </p>
+ * <p>
+ * Comments can be removed by setting them to an empty string or null.
+ * </p>
+ *
+ * @see CommentOnTableStatement
+ * @see CommentOnColumnStatement
+ * @see CommentOnUserTypeStatement
+ */
+public final class CommentOnKeyspaceStatement extends SchemaDescriptionStatement
+{
+ public CommentOnKeyspaceStatement(String keyspaceName, String comment)
+ {
+ super(keyspaceName, comment, DescriptionType.COMMENT);
+ }
+
+ @Override
+ public Keyspaces apply(ClusterMetadata metadata)
+ {
+ Keyspaces schema = metadata.schema.getKeyspaces();
+ KeyspaceMetadata keyspace = validateAndGetKeyspace(schema);
+ KeyspaceParams newParams = keyspace.params.withComment(effectiveDescription());
+ KeyspaceMetadata newKeyspace = keyspace.withSwapped(newParams);
+ return schema.withAddedOrUpdated(newKeyspace);
+ }
+
+ @Override
+ SchemaChange schemaChangeEvent(KeyspacesDiff diff)
+ {
+ return new SchemaChange(Change.UPDATED, keyspaceName);
+ }
+
+ @Override
+ public void authorize(ClientState client)
+ {
+ client.ensureKeyspacePermission(keyspaceName, Permission.ALTER);
+ }
+
+ @Override
+ public AuditLogContext getAuditLogContext()
+ {
+ return new AuditLogContext(AuditLogEntryType.COMMENT_KEYSPACE, keyspaceName);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s (%s, %s)", getClass().getSimpleName(), keyspaceName, description);
+ }
+
+ public static final class Raw extends CQLStatement.Raw
+ {
+ private final String keyspaceName;
+ private final String comment;
+
+ public Raw(String keyspaceName, String comment)
+ {
+ this.keyspaceName = keyspaceName;
+ this.comment = comment;
+ }
+
+ @Override
+ public CommentOnKeyspaceStatement prepare(ClientState state)
+ {
+ return new CommentOnKeyspaceStatement(keyspaceName, comment);
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CommentOnTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CommentOnTableStatement.java
new file mode 100644
index 0000000000..0c2ced7daf
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/CommentOnTableStatement.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.cql3.statements.schema;
+
+import org.apache.cassandra.audit.AuditLogContext;
+import org.apache.cassandra.audit.AuditLogEntryType;
+import org.apache.cassandra.auth.Permission;
+import org.apache.cassandra.cql3.CQLStatement;
+import org.apache.cassandra.cql3.QualifiedName;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.Keyspaces;
+import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.schema.TableParams;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.tcm.ClusterMetadata;
+import org.apache.cassandra.transport.Event.SchemaChange;
+import org.apache.cassandra.transport.Event.SchemaChange.Change;
+import org.apache.cassandra.transport.Event.SchemaChange.Target;
+
+/**
+ * Handles the execution of COMMENT ON TABLE CQL statements.
+ * <p>
+ * This statement allows users to add descriptive comments to tables for documentation purposes.
+ * Comments are stored in the table metadata and displayed when using DESCRIBE TABLE.
+ * </p>
+ * <p>
+ * Syntax: {@code COMMENT ON TABLE [keyspace_name.]table_name IS 'comment text';}
+ * </p>
+ * <p>
+ * Comments can be removed by setting them to an empty string or null.
+ * </p>
+ *
+ * @see CommentOnKeyspaceStatement
+ * @see CommentOnColumnStatement
+ * @see CommentOnUserTypeStatement
+ */
+public final class CommentOnTableStatement extends SchemaDescriptionStatement
+{
+ private final String tableName;
+
+ public CommentOnTableStatement(String keyspaceName, String tableName, String comment)
+ {
+ super(keyspaceName, comment, DescriptionType.COMMENT);
+ this.tableName = tableName;
+ }
+
+ @Override
+ public Keyspaces apply(ClusterMetadata metadata)
+ {
+ Keyspaces schema = metadata.schema.getKeyspaces();
+
+ TableMetadata table = validateAndGetTable(schema, tableName);
+ TableParams newParams = table.params.unbuild().comment(effectiveDescription()).build();
+ TableMetadata newTable = table.withSwapped(newParams);
+
+ KeyspaceMetadata keyspace = schema.getNullable(keyspaceName);
+ KeyspaceMetadata newKeyspace = keyspace.withSwapped(keyspace.tables.withSwapped(newTable));
+
+ return schema.withAddedOrUpdated(newKeyspace);
+ }
+
+ @Override
+ SchemaChange schemaChangeEvent(KeyspacesDiff diff)
+ {
+ return new SchemaChange(Change.UPDATED, Target.TABLE, keyspaceName, tableName);
+ }
+
+ @Override
+ public void authorize(ClientState client)
+ {
+ client.ensureTablePermission(keyspaceName, tableName, Permission.ALTER);
+ }
+
+ @Override
+ public AuditLogContext getAuditLogContext()
+ {
+ return new AuditLogContext(AuditLogEntryType.COMMENT_TABLE, keyspaceName, tableName);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s (%s.%s, %s)", getClass().getSimpleName(), keyspaceName, tableName, description);
+ }
+
+ public static final class Raw extends CQLStatement.Raw
+ {
+ private final QualifiedName tableName;
+ private final String comment;
+
+ public Raw(QualifiedName tableName, String comment)
+ {
+ this.tableName = tableName;
+ this.comment = comment;
+ }
+
+ @Override
+ public CommentOnTableStatement prepare(ClientState state)
+ {
+ String keyspaceName = tableName.hasKeyspace() ? tableName.getKeyspace() : state.getKeyspace();
+ return new CommentOnTableStatement(keyspaceName,
+ tableName.getName(),
+ comment);
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CommentOnUserTypeFieldStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CommentOnUserTypeFieldStatement.java
new file mode 100644
index 0000000000..049b2de7e1
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/CommentOnUserTypeFieldStatement.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.cql3.statements.schema;
+
+import org.apache.cassandra.audit.AuditLogContext;
+import org.apache.cassandra.audit.AuditLogEntryType;
+import org.apache.cassandra.auth.Permission;
+import org.apache.cassandra.cql3.CQLStatement;
+import org.apache.cassandra.cql3.FieldIdentifier;
+import org.apache.cassandra.cql3.UTName;
+import org.apache.cassandra.db.marshal.UserType;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.Keyspaces;
+import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.tcm.ClusterMetadata;
+import org.apache.cassandra.transport.Event.SchemaChange;
+import org.apache.cassandra.transport.Event.SchemaChange.Change;
+import org.apache.cassandra.transport.Event.SchemaChange.Target;
+
+/**
+ * Handles the execution of COMMENT ON FIELD CQL statements.
+ * <p>
+ * This statement allows users to add descriptive comments to individual fields of user-defined types
+ * for documentation purposes. Comments are stored in the type metadata and displayed when using DESCRIBE TYPE.
+ * </p>
+ * <p>
+ * Syntax: {@code COMMENT ON FIELD [keyspace_name.]type_name.field_name IS 'comment text';}
+ * </p>
+ * <p>
+ * Comments can be removed by setting them to an empty string or null.
+ * </p>
+ *
+ * @see CommentOnUserTypeStatement
+ * @see CommentOnColumnStatement
+ */
+public final class CommentOnUserTypeFieldStatement extends SchemaDescriptionStatement
+{
+ private final String typeName;
+ private final FieldIdentifier fieldName;
+
+ public CommentOnUserTypeFieldStatement(String keyspaceName, String typeName, FieldIdentifier fieldName, String comment)
+ {
+ super(keyspaceName, comment, DescriptionType.COMMENT);
+ this.typeName = typeName;
+ this.fieldName = fieldName;
+ }
+
+ @Override
+ public Keyspaces apply(ClusterMetadata metadata)
+ {
+ Keyspaces schema = metadata.schema.getKeyspaces();
+ UserType type = validateAndGetTypeField(schema, typeName, fieldName);
+
+ String effectiveComment = effectiveDescription();
+ UserType newType = type.withFieldComment(fieldName, effectiveComment);
+
+ KeyspaceMetadata keyspace = schema.getNullable(keyspaceName);
+ KeyspaceMetadata newKeyspace = keyspace.withSwapped(keyspace.types.withUpdatedUserType(newType));
+
+ return schema.withAddedOrUpdated(newKeyspace);
+ }
+
+ @Override
+ SchemaChange schemaChangeEvent(KeyspacesDiff diff)
+ {
+ return new SchemaChange(Change.UPDATED, Target.TYPE, keyspaceName, typeName);
+ }
+
+ @Override
+ public void authorize(ClientState client)
+ {
+ client.ensureAllTablesPermission(keyspaceName, Permission.ALTER);
+ }
+
+ @Override
+ public AuditLogContext getAuditLogContext()
+ {
+ return new AuditLogContext(AuditLogEntryType.COMMENT_TYPE, keyspaceName, typeName);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s (%s.%s.%s, %s)", getClass().getSimpleName(), keyspaceName, typeName, fieldName, description);
+ }
+
+ public static final class Raw extends CQLStatement.Raw
+ {
+ private final UTName typeName;
+ private final FieldIdentifier fieldName;
+ private final String comment;
+
+ public Raw(UTName typeName, FieldIdentifier fieldName, String comment)
+ {
+ this.typeName = typeName;
+ this.fieldName = fieldName;
+ this.comment = comment;
+ }
+
+ @Override
+ public CommentOnUserTypeFieldStatement prepare(ClientState state)
+ {
+ String keyspaceName = typeName.hasKeyspace() ? typeName.getKeyspace() : state.getKeyspace();
+ return new CommentOnUserTypeFieldStatement(keyspaceName,
+ typeName.getStringTypeName(),
+ fieldName,
+ comment);
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CommentOnUserTypeStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CommentOnUserTypeStatement.java
new file mode 100644
index 0000000000..c12d9918cc
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/CommentOnUserTypeStatement.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.cql3.statements.schema;
+
+import org.apache.cassandra.audit.AuditLogContext;
+import org.apache.cassandra.audit.AuditLogEntryType;
+import org.apache.cassandra.auth.Permission;
+import org.apache.cassandra.cql3.CQLStatement;
+import org.apache.cassandra.cql3.UTName;
+import org.apache.cassandra.db.marshal.UserType;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.Keyspaces;
+import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.tcm.ClusterMetadata;
+import org.apache.cassandra.transport.Event.SchemaChange;
+import org.apache.cassandra.transport.Event.SchemaChange.Change;
+import org.apache.cassandra.transport.Event.SchemaChange.Target;
+
+/**
+ * Handles the execution of COMMENT ON TYPE CQL statements.
+ * <p>
+ * This statement allows users to add descriptive comments to user-defined types for documentation purposes.
+ * Comments are stored in the type metadata and displayed when using DESCRIBE TYPE.
+ * </p>
+ * <p>
+ * Syntax: {@code COMMENT ON TYPE [keyspace_name.]type_name IS 'comment text';}
+ * </p>
+ * <p>
+ * Comments can be removed by setting them to an empty string or null.
+ * </p>
+ *
+ * @see CommentOnKeyspaceStatement
+ * @see CommentOnTableStatement
+ * @see CommentOnColumnStatement
+ */
+public final class CommentOnUserTypeStatement extends SchemaDescriptionStatement
+{
+ private final String typeName;
+
+ public CommentOnUserTypeStatement(String keyspaceName, String typeName, String comment)
+ {
+ super(keyspaceName, comment, DescriptionType.COMMENT);
+ this.typeName = typeName;
+ }
+
+ @Override
+ public Keyspaces apply(ClusterMetadata metadata)
+ {
+ Keyspaces schema = metadata.schema.getKeyspaces();
+ UserType type = validateAndGetType(schema, typeName);
+ UserType newType = type.withComment(effectiveDescription());
+
+ KeyspaceMetadata keyspace = schema.getNullable(keyspaceName);
+ KeyspaceMetadata newKeyspace = keyspace.withSwapped(keyspace.types.withUpdatedUserType(newType));
+
+ return schema.withAddedOrUpdated(newKeyspace);
+ }
+
+ @Override
+ SchemaChange schemaChangeEvent(KeyspacesDiff diff)
+ {
+ return new SchemaChange(Change.UPDATED, Target.TYPE, keyspaceName, typeName);
+ }
+
+ @Override
+ public void authorize(ClientState client)
+ {
+ client.ensureAllTablesPermission(keyspaceName, Permission.ALTER);
+ }
+
+ @Override
+ public AuditLogContext getAuditLogContext()
+ {
+ return new AuditLogContext(AuditLogEntryType.COMMENT_TYPE, keyspaceName, typeName);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s (%s.%s, %s)", getClass().getSimpleName(), keyspaceName, typeName, description);
+ }
+
+ public static final class Raw extends CQLStatement.Raw
+ {
+ private final UTName typeName;
+ private final String comment;
+
+ public Raw(UTName typeName, String comment)
+ {
+ this.typeName = typeName;
+ this.comment = comment;
+ }
+
+ @Override
+ public CommentOnUserTypeStatement prepare(ClientState state)
+ {
+ String keyspaceName = typeName.hasKeyspace() ? typeName.getKeyspace() : state.getKeyspace();
+ return new CommentOnUserTypeStatement(keyspaceName,
+ typeName.getStringTypeName(),
+ comment);
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CopyTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CopyTableStatement.java
index c5b38e5e08..bde06c35dc 100644
--- a/src/java/org/apache/cassandra/cql3/statements/schema/CopyTableStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/CopyTableStatement.java
@@ -20,6 +20,7 @@ package org.apache.cassandra.cql3.statements.schema;
import java.nio.ByteBuffer;
import java.util.ArrayList;
+import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -27,6 +28,8 @@ import java.util.stream.Collectors;
import com.google.common.collect.Sets;
+import org.apache.commons.lang3.StringUtils;
+
import org.apache.cassandra.audit.AuditLogContext;
import org.apache.cassandra.audit.AuditLogEntryType;
import org.apache.cassandra.auth.Permission;
@@ -67,14 +70,14 @@ public final class CopyTableStatement extends AlterSchemaStatement
private final String targetTableName;
private final boolean ifNotExists;
private final TableAttributes attrs;
- private final CreateLikeOption createLikeOption;
+ private final Set<CreateLikeOption> createLikeOptions;
public CopyTableStatement(String sourceKeyspace,
String targetKeyspace,
String sourceTableName,
String targetTableName,
boolean ifNotExists,
- CreateLikeOption createLikeOption,
+ Set<CreateLikeOption> createLikeOptions,
TableAttributes attrs)
{
super(targetKeyspace);
@@ -83,7 +86,9 @@ public final class CopyTableStatement extends AlterSchemaStatement
this.sourceTableName = sourceTableName;
this.targetTableName = targetTableName;
this.ifNotExists = ifNotExists;
- this.createLikeOption = createLikeOption;
+ this.createLikeOptions = createLikeOptions != null
+ ? EnumSet.copyOf(createLikeOptions)
+ : EnumSet.noneOf(CreateLikeOption.class);
this.attrs = attrs;
}
@@ -115,10 +120,10 @@ public final class CopyTableStatement extends AlterSchemaStatement
if (null == sourceKeyspaceMeta)
throw ire("Source Keyspace '%s' doesn't exist", sourceKeyspace);
- TableMetadata sourceTableMeta = sourceKeyspaceMeta.getTableNullable(sourceTableName);
+ TableMetadata sourceTableMeta = sourceKeyspaceMeta.getTableOrViewNullable(sourceTableName);
if (null == sourceTableMeta)
- throw ire("Souce Table '%s.%s' doesn't exist", sourceKeyspace, sourceTableName);
+ throw ire("Source Table '%s.%s' doesn't exist", sourceKeyspace, sourceTableName);
if (sourceTableMeta.isIndex())
throw ire("Cannot use CREATE TABLE LIKE on an index table '%s.%s'.", sourceKeyspace, sourceTableName);
@@ -191,7 +196,7 @@ public final class CopyTableStatement extends AlterSchemaStatement
if (!sourceTableMeta.params.compression.isEnabled())
Guardrails.uncompressedTablesEnabled.ensureEnabled(state);
- // withInternals can be set to false as it is only used for souce table id, which is not need for target table and the table
+ // withInternals can be set to false as it is only used for source table id, which is not need for target table and the table
// id can be set through create table like cql using WITH ID
String sourceCQLString = sourceTableMeta.toCqlString(false, false, false, false);
@@ -203,9 +208,12 @@ public final class CopyTableStatement extends AlterSchemaStatement
.indexes(Indexes.none())
.triggers(Triggers.none());
+ // Copy requested features using the CreateLikeHandler pattern
+ for (CreateLikeOption option : createLikeOptions)
+ option.copy(targetBuilder, targetKeyspace, targetTableName, sourceTableMeta, targetKeyspaceMeta);
+
TableParams originalParams = targetBuilder.build().params;
TableParams newTableParams = attrs.asAlteredTableParams(originalParams);
- maybeCopyIndexes(targetBuilder, sourceTableMeta, targetKeyspaceMeta);
TableMetadata table = targetBuilder.params(newTableParams)
.id(TableId.get(metadata))
@@ -239,59 +247,13 @@ public final class CopyTableStatement extends AlterSchemaStatement
validateDefaultTimeToLive(attrs.asNewTableParams(keyspaceName));
}
- private void maybeCopyIndexes(TableMetadata.Builder builder, TableMetadata sourceTableMeta, KeyspaceMetadata targetKeyspaceMeta)
- {
- if (createLikeOption != CreateLikeOption.INDEXES || sourceTableMeta.indexes.isEmpty())
- return;
-
- Set<String> customIndexes = Sets.newTreeSet();
- List<IndexMetadata> indexesToCopy = new ArrayList<>();
- for (IndexMetadata indexMetadata : sourceTableMeta.indexes)
- {
- // only sai and legacy secondary index is supported
- if (indexMetadata.isCustom() && !StorageAttachedIndex.class.getCanonicalName().equals(indexMetadata.getIndexClassName()))
- {
- customIndexes.add(indexMetadata.name);
- continue;
- }
-
- ColumnMetadata targetColumn = sourceTableMeta.getColumn(UTF8Type.instance.decompose(indexMetadata.options.get("target")));
- String indexName;
- // The rules for generating the index names of the target table are:
- // (1) If the source table's index names follow the pattern sourcetablename_columnname_idx_number, the index names are considered to be generated by the system,
- // then we directly replace the name of source table with the name of target table, and increment the number after idx to avoid index name conflicts.
- // (2) Index names that do not follow the above pattern are considered user-defined, so the index names are retained and increment the number after idx to avoid conflicts.
- if (indexMetadata.name.startsWith(sourceTableName + "_" + targetColumn.name + "_idx"))
- {
- String baseName = IndexMetadata.generateDefaultIndexName(targetTableName, targetColumn.name);
- indexName = targetKeyspaceMeta.findAvailableIndexName(baseName, indexesToCopy, targetKeyspaceMeta);
- }
- else
- {
- indexName = targetKeyspaceMeta.findAvailableIndexName(indexMetadata.name, indexesToCopy, targetKeyspaceMeta);
- }
- indexesToCopy.add(IndexMetadata.fromSchemaMetadata(indexName, indexMetadata.kind, indexMetadata.options));
- }
-
- if (!indexesToCopy.isEmpty())
- builder.indexes(Indexes.builder().add(indexesToCopy).build());
-
- if (!customIndexes.isEmpty())
- ClientWarn.instance.warn(String.format("Source table %s.%s to copy indexes from to %s.%s has custom indexes. These indexes were not copied: %s",
- sourceKeyspace,
- sourceTableName,
- targetKeyspace,
- targetTableName,
- customIndexes));
- }
-
public final static class Raw extends CQLStatement.Raw
{
private final QualifiedName oldName;
private final QualifiedName newName;
private final boolean ifNotExists;
public final TableAttributes attrs = new TableAttributes();
- private CreateLikeOption createLikeOption = null;
+ private Set<CreateLikeOption> createLikeOptions = EnumSet.noneOf(CreateLikeOption.class);
public Raw(QualifiedName newName, QualifiedName oldName, boolean ifNotExists)
{
@@ -305,17 +267,126 @@ public final class CopyTableStatement extends AlterSchemaStatement
{
String oldKeyspace = oldName.hasKeyspace() ? oldName.getKeyspace() : state.getKeyspace();
String newKeyspace = newName.hasKeyspace() ? newName.getKeyspace() : state.getKeyspace();
- return new CopyTableStatement(oldKeyspace, newKeyspace, oldName.getName(), newName.getName(), ifNotExists, createLikeOption, attrs);
+ return new CopyTableStatement(oldKeyspace, newKeyspace, oldName.getName(), newName.getName(), ifNotExists, createLikeOptions, attrs);
}
- public void withLikeOption(CreateLikeOption option)
+ public void addLikeOption(CreateLikeOption option)
{
- this.createLikeOption = option;
+ this.createLikeOptions.add(option);
}
}
- public enum CreateLikeOption
+ /**
+ * Handler interface for copying specific table features when using CREATE TABLE LIKE.
+ * Each CreateLikeOption implements this to provide specific copy logic.
+ */
+ interface CreateLikeHandler
{
- INDEXES;
+ /**
+ * Copies a specific feature from the source table to the target table builder.
+ *
+ * @param targetTableBuilder the builder for the target table being created
+ * @param targetKeyspace the keyspace where the target table will be created
+ * @param targetTableName the name of the target table being created
+ * @param sourceTableMeta metadata of the source table to copy from
+ * @param targetKeyspaceMeta metadata of the target keyspace for schema lookup
+ */
+ void copy(TableMetadata.Builder targetTableBuilder,
+ String targetKeyspace,
+ String targetTableName,
+ TableMetadata sourceTableMeta,
+ KeyspaceMetadata targetKeyspaceMeta);
+ }
+
+ public enum CreateLikeOption implements CreateLikeHandler
+ {
+ INDEXES
+ {
+ @Override
+ public void copy(TableMetadata.Builder targetTableBuilder,
+ String targetKeyspace,
+ String targetTableName,
+ TableMetadata sourceTableMeta,
+ KeyspaceMetadata targetKeyspaceMeta)
+ {
+ if (sourceTableMeta.indexes.isEmpty())
+ return;
+
+ String sourceTableName = sourceTableMeta.name;
+ String sourceKeyspace = sourceTableMeta.keyspace;
+ Set<String> customIndexes = Sets.newTreeSet();
+ List<IndexMetadata> indexesToCopy = new ArrayList<>();
+ for (IndexMetadata indexMetadata : sourceTableMeta.indexes)
+ {
+ // only sai and legacy secondary index is supported
+ if (indexMetadata.isCustom() && !StorageAttachedIndex.class.getCanonicalName().equals(indexMetadata.getIndexClassName()))
+ {
+ customIndexes.add(indexMetadata.name);
+ continue;
+ }
+
+ ColumnMetadata targetColumn = sourceTableMeta.getColumn(UTF8Type.instance.decompose(indexMetadata.options.get("target")));
+ String indexName;
+ // The rules for generating the index names of the target table are:
+ // (1) If the source table's index names follow the pattern sourcetablename_columnname_idx_number, the index names are considered to be generated by the system,
+ // then we directly replace the name of source table with the name of target table, and increment the number after idx to avoid index name conflicts.
+ // (2) Index names that do not follow the above pattern are considered user-defined, so the index names are retained and increment the number after idx to avoid conflicts.
+ if (indexMetadata.name.startsWith(sourceTableName + "_" + targetColumn.name + "_idx"))
+ {
+ String baseName = IndexMetadata.generateDefaultIndexName(targetTableName, targetColumn.name);
+ indexName = targetKeyspaceMeta.findAvailableIndexName(baseName, indexesToCopy, targetKeyspaceMeta);
+ }
+ else
+ {
+ indexName = targetKeyspaceMeta.findAvailableIndexName(indexMetadata.name, indexesToCopy, targetKeyspaceMeta);
+ }
+ indexesToCopy.add(IndexMetadata.fromSchemaMetadata(indexName, indexMetadata.kind, indexMetadata.options));
+ }
+
+ if (!indexesToCopy.isEmpty())
+ targetTableBuilder.indexes(Indexes.builder().add(indexesToCopy).build());
+
+ if (!customIndexes.isEmpty())
+ ClientWarn.instance.warn(String.format("Source table %s.%s to copy indexes from to %s.%s has custom indexes. These indexes were not copied: %s",
+ sourceKeyspace,
+ sourceTableName,
+ targetKeyspace,
+ targetTableName,
+ customIndexes));
+ }
+ },
+ COMMENTS
+ {
+ @Override
+ public void copy(TableMetadata.Builder targetTableBuilder,
+ String targetKeyspace,
+ String targetTableName,
+ TableMetadata sourceTableMeta,
+ KeyspaceMetadata targetKeyspaceMeta)
+ {
+ if (!StringUtils.isEmpty(sourceTableMeta.params.comment))
+ targetTableBuilder.comment(sourceTableMeta.params.comment);
+
+ for (ColumnMetadata columnMetadata : sourceTableMeta.columns())
+ targetTableBuilder.alterColumnComment(columnMetadata.name, columnMetadata.comment);
+ }
+ },
+ SECURITY_LABELS
+ {
+ @Override
+ public void copy(TableMetadata.Builder targetTableBuilder,
+ String targetKeyspace,
+ String targetTableName,
+ TableMetadata sourceTableMeta,
+ KeyspaceMetadata targetKeyspaceMeta)
+ {
+ if (!StringUtils.isEmpty(sourceTableMeta.params.securityLabel))
+ targetTableBuilder.securityLabel(sourceTableMeta.params.securityLabel);
+
+ for (ColumnMetadata columnMetadata : sourceTableMeta.columns())
+ targetTableBuilder.alterColumnSecurityLabel(columnMetadata.name, columnMetadata.securityLabel);
+ }
+ }
+
}
}
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/SchemaDescriptionStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/SchemaDescriptionStatement.java
new file mode 100644
index 0000000000..c532376321
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/SchemaDescriptionStatement.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.statements.schema;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.FieldIdentifier;
+import org.apache.cassandra.db.marshal.UserType;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.Keyspaces;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.service.ClientWarn;
+
+import static org.apache.cassandra.schema.TableMetadata.Kind.REGULAR;
+import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
+
+public abstract class SchemaDescriptionStatement extends AlterSchemaStatement
+{
+ private static final String NO_DESCRIPTION = "";
+ protected final String description;
+ private final DescriptionType descriptionType;
+
+ protected SchemaDescriptionStatement(String keyspaceName, String comment, DescriptionType descriptionType)
+ {
+ super(keyspaceName);
+ this.description = comment;
+ this.descriptionType = descriptionType;
+ }
+
+ @Override
+ public void validate(ClientState state)
+ {
+ super.validate(state);
+ if(description == null)
+ {
+ // Statement is removing comment on schema elements, ignore
+ return;
+ }
+
+ if(description.isEmpty())
+ {
+ throw new InvalidRequestException(String.format("Cannot set %s to empty string", descriptionType.value));
+ }
+
+ if (description.length() > descriptionType.maxLength)
+ {
+ String msg = String.format("%s length (%d) exceeds maximum allowed length (%d)",
+ descriptionType.value,
+ description.length(),
+ descriptionType.maxLength);
+ throw new InvalidRequestException(msg);
+ }
+ }
+
+ protected KeyspaceMetadata validateAndGetKeyspace(Keyspaces schema)
+ {
+ KeyspaceMetadata keyspace = schema.getNullable(keyspaceName);
+
+ if (null == keyspace)
+ throw ire("Keyspace '%s' doesn't exist", keyspaceName);
+
+ return keyspace;
+ }
+
+ protected TableMetadata validateAndGetTable(Keyspaces schema, String tableName)
+ {
+ KeyspaceMetadata keyspaceMetadata = validateAndGetKeyspace(schema);
+
+ TableMetadata tableMetadata = keyspaceMetadata.getTableOrViewNullable(tableName);
+ if (null == tableMetadata)
+ throw ire("Table '%s.%s' doesn't exist", keyspaceName, tableName);
+
+ if (tableMetadata.kind != REGULAR)
+ throw ire("Cannot set %s on non-regular table '%s.%s'",descriptionType.value, keyspaceName, tableName);
+
+ return tableMetadata;
+ }
+
+ protected UserType validateAndGetType(Keyspaces schema, String typeName)
+ {
+ KeyspaceMetadata keyspaceMetadata = validateAndGetKeyspace(schema);
+
+ UserType type = keyspaceMetadata.types.getNullable(bytes(typeName));
+ if (null == type)
+ throw ire("Type '%s.%s' doesn't exist", keyspaceName, typeName);
+
+ return type;
+ }
+
+ protected UserType validateAndGetTypeField(Keyspaces schema,
+ String typeName,
+ FieldIdentifier fieldName)
+ {
+ UserType type = validateAndGetType(schema, typeName);
+
+ if (type.fieldPosition(fieldName) == -1)
+ throw ire("Field '%s' doesn't exist in type '%s.%s'", fieldName, keyspaceName, typeName);
+
+ return type;
+ }
+
+ protected String effectiveDescription()
+ {
+ return description == null ? NO_DESCRIPTION : description;
+ }
+
+ protected void providerWarning(String provider)
+ {
+ if (provider != null)
+ ClientWarn.instance.warn("Provider functionality not implemented." +
+ " Provider '" + provider + "' will not be loaded or invoked.");
+ }
+
+ protected enum DescriptionType
+ {
+ COMMENT("comment", DatabaseDescriptor.getMaxCommentLength()),
+ SECURITY_LABEL("security label", DatabaseDescriptor.getMaxSecurityLabelLength());
+
+ private final String value;
+ private final int maxLength;
+
+ DescriptionType(String value, int maxLength)
+ {
+ this.value = value;
+ this.maxLength = maxLength;
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/SecurityLabelOnColumnStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/SecurityLabelOnColumnStatement.java
new file mode 100644
index 0000000000..22cb81e6e9
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/SecurityLabelOnColumnStatement.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.cql3.statements.schema;
+
+import org.apache.cassandra.audit.AuditLogContext;
+import org.apache.cassandra.audit.AuditLogEntryType;
+import org.apache.cassandra.auth.Permission;
+import org.apache.cassandra.cql3.CQLStatement;
+import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.cql3.QualifiedName;
+import org.apache.cassandra.schema.ColumnMetadata;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.Keyspaces;
+import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.tcm.ClusterMetadata;
+import org.apache.cassandra.transport.Event.SchemaChange;
+import org.apache.cassandra.transport.Event.SchemaChange.Change;
+import org.apache.cassandra.transport.Event.SchemaChange.Target;
+
+/**
+ * Handles the execution of SECURITY LABEL ON COLUMN CQL statements.
+ * <p>
+ * This statement allows users to add security classification labels to individual columns.
+ * Security labels are stored in the column metadata and displayed when using DESCRIBE TABLE.
+ * </p>
+ * <p>
+ * Syntax: {@code SECURITY LABEL [FOR provider_name] ON COLUMN [keyspace_name.]table_name.column_name IS 'label';}
+ * </p>
+ * <p>
+ * Security labels can be used to mark data sensitivity levels or compliance requirements.
+ * Labels can be removed by setting them to NULL.
+ * </p>
+ * <h3>Provider Support</h3>
+ * <p>
+ * The optional {@code FOR provider_name} clause is accepted in the syntax to support future
+ * extensibility. Providers are intended to be pluggable security policy modules that can
+ * interpret and enforce access controls based on security labels. However, provider
+ * functionality is not currently implemented. If a provider is specified, a warning will be
+ * issued but the security label will still be applied. The provider parameter is reserved
+ * for future use.
+ * </p>
+ * <p>
+ * Example usage:
+ * <pre>
+ * // Basic syntax without provider
+ * SECURITY LABEL ON COLUMN users.ssn IS 'PII';
+ *
+ * // Syntax with provider (accepted but not yet implemented)
+ * SECURITY LABEL FOR custom_provider ON COLUMN users.ssn IS 'sensitive';
+ *
+ * // Remove security label
+ * SECURITY LABEL ON COLUMN users.ssn IS NULL;
+ * </pre>
+ * </p>
+ *
+ * @see SecurityLabelOnKeyspaceStatement
+ * @see SecurityLabelOnTableStatement
+ * @see SecurityLabelOnUserTypeStatement
+ */
+public final class SecurityLabelOnColumnStatement extends SchemaDescriptionStatement
+{
+ private final String tableName;
+ private final ColumnIdentifier columnName;
+ private final String provider;
+
+ public SecurityLabelOnColumnStatement(String keyspaceName, String tableName, ColumnIdentifier columnName, String securityLabel, String provider)
+ {
+ super(keyspaceName, securityLabel, DescriptionType.SECURITY_LABEL);
+ this.tableName = tableName;
+ this.columnName = columnName;
+ this.provider = provider;
+ }
+
+
+ @Override
+ public Keyspaces apply(ClusterMetadata metadata)
+ {
+ Keyspaces schema = metadata.schema.getKeyspaces();
+ TableMetadata table = validateAndGetTable(schema, tableName);
+
+ ColumnMetadata column = table.getColumn(columnName);
+ if (null == column)
+ throw ire("Column '%s' doesn't exist in table '%s.%s'", columnName, keyspaceName, tableName);
+
+ providerWarning(provider);
+
+ TableMetadata newTable = table.unbuild().alterColumnSecurityLabel(columnName, effectiveDescription()).build();
+ KeyspaceMetadata keyspace = schema.getNullable(keyspaceName);
+ KeyspaceMetadata newKeyspace = keyspace.withSwapped(keyspace.tables.withSwapped(newTable));
+
+ return schema.withAddedOrUpdated(newKeyspace);
+ }
+
+ @Override
+ SchemaChange schemaChangeEvent(KeyspacesDiff diff)
+ {
+ return new SchemaChange(Change.UPDATED, Target.TABLE, keyspaceName, tableName);
+ }
+
+ @Override
+ public void authorize(ClientState client)
+ {
+ client.ensureTablePermission(keyspaceName, tableName, Permission.ALTER);
+ }
+
+ @Override
+ public AuditLogContext getAuditLogContext()
+ {
+ return new AuditLogContext(AuditLogEntryType.SECURITY_LABEL_COLUMN, keyspaceName, tableName);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s (%s.%s.%s, %s)", getClass().getSimpleName(), keyspaceName, tableName, columnName, description);
+ }
+
+ public static final class Raw extends CQLStatement.Raw
+ {
+ private final QualifiedName tableName;
+ private final ColumnIdentifier columnName;
+ private final String securityLabel;
+ private final String provider;
+
+ public Raw(QualifiedName tableName, ColumnIdentifier columnName, String securityLabel, String provider)
+ {
+ this.tableName = tableName;
+ this.columnName = columnName;
+ this.securityLabel = securityLabel;
+ this.provider = provider;
+ }
+
+ @Override
+ public SecurityLabelOnColumnStatement prepare(ClientState state)
+ {
+ String keyspaceName = tableName.hasKeyspace() ? tableName.getKeyspace() : state.getKeyspace();
+ return new SecurityLabelOnColumnStatement(keyspaceName,
+ tableName.getName(),
+ columnName,
+ securityLabel,
+ provider);
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/SecurityLabelOnKeyspaceStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/SecurityLabelOnKeyspaceStatement.java
new file mode 100644
index 0000000000..e9ab8625d1
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/SecurityLabelOnKeyspaceStatement.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.cql3.statements.schema;
+
+import org.apache.cassandra.audit.AuditLogContext;
+import org.apache.cassandra.audit.AuditLogEntryType;
+import org.apache.cassandra.auth.Permission;
+import org.apache.cassandra.cql3.CQLStatement;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.schema.Keyspaces;
+import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.tcm.ClusterMetadata;
+import org.apache.cassandra.transport.Event.SchemaChange;
+import org.apache.cassandra.transport.Event.SchemaChange.Change;
+
+/**
+ * Handles the execution of SECURITY LABEL ON KEYSPACE CQL statements.
+ * <p>
+ * This statement allows users to add security classification labels to keyspaces.
+ * Security labels are stored in the keyspace metadata and displayed when using DESCRIBE KEYSPACE.
+ * </p>
+ * <p>
+ * Syntax: {@code SECURITY LABEL [FOR provider_name] ON KEYSPACE keyspace_name IS 'label';}
+ * </p>
+ * <p>
+ * Security labels can be used to mark data sensitivity levels or compliance requirements.
+ * Labels can be removed by setting them to NULL.
+ * </p>
+ * <h3>Provider Support</h3>
+ * <p>
+ * The optional {@code FOR provider_name} clause is accepted in the syntax to support future
+ * extensibility. Providers are intended to be pluggable security policy modules that can
+ * interpret and enforce access controls based on security labels. However, provider
+ * functionality is not currently implemented. If a provider is specified, a warning will be
+ * issued but the security label will still be applied. The provider parameter is reserved
+ * for future use.
+ * </p>
+ * <p>
+ * Example usage:
+ * <pre>
+ * // Basic syntax without provider
+ * SECURITY LABEL ON KEYSPACE production IS 'confidential';
+ *
+ * // Syntax with provider (accepted but not yet implemented)
+ * SECURITY LABEL FOR custom_provider ON KEYSPACE production IS 'top_secret';
+ *
+ * // Remove security label
+ * SECURITY LABEL ON KEYSPACE production IS NULL;
+ * </pre>
+ * </p>
+ *
+ * @see SecurityLabelOnTableStatement
+ * @see SecurityLabelOnColumnStatement
+ * @see SecurityLabelOnUserTypeStatement
+ */
+public final class SecurityLabelOnKeyspaceStatement extends SchemaDescriptionStatement
+{
+ private final String provider;
+
+ public SecurityLabelOnKeyspaceStatement(String keyspaceName, String securityLabel, String provider)
+ {
+ super(keyspaceName, securityLabel, DescriptionType.SECURITY_LABEL);
+ this.provider = provider;
+ }
+
+ @Override
+ public Keyspaces apply(ClusterMetadata metadata)
+ {
+ Keyspaces schema = metadata.schema.getKeyspaces();
+ KeyspaceMetadata keyspace = validateAndGetKeyspace(schema);
+
+ providerWarning(provider);
+
+ KeyspaceParams newParams = keyspace.params.withSecurityLabel(effectiveDescription());
+ KeyspaceMetadata newKeyspace = keyspace.withSwapped(newParams);
+
+ return schema.withAddedOrUpdated(newKeyspace);
+ }
+
+ @Override
+ SchemaChange schemaChangeEvent(KeyspacesDiff diff)
+ {
+ return new SchemaChange(Change.UPDATED, keyspaceName);
+ }
+
+ @Override
+ public void authorize(ClientState client)
+ {
+ client.ensureKeyspacePermission(keyspaceName, Permission.ALTER);
+ }
+
+ @Override
+ public AuditLogContext getAuditLogContext()
+ {
+ return new AuditLogContext(AuditLogEntryType.SECURITY_LABEL_KEYSPACE, keyspaceName);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s (%s, %s)", getClass().getSimpleName(), keyspaceName, description);
+ }
+
+ public static final class Raw extends CQLStatement.Raw
+ {
+ private final String keyspaceName;
+ private final String securityLabel;
+ private final String provider;
+
+ public Raw(String keyspaceName, String securityLabel, String provider)
+ {
+ this.keyspaceName = keyspaceName;
+ this.securityLabel = securityLabel;
+ this.provider = provider;
+ }
+
+ @Override
+ public SecurityLabelOnKeyspaceStatement prepare(ClientState state)
+ {
+ return new SecurityLabelOnKeyspaceStatement(keyspaceName, securityLabel, provider);
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/SecurityLabelOnTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/SecurityLabelOnTableStatement.java
new file mode 100644
index 0000000000..969014c8ab
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/SecurityLabelOnTableStatement.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.cql3.statements.schema;
+
+import org.apache.cassandra.audit.AuditLogContext;
+import org.apache.cassandra.audit.AuditLogEntryType;
+import org.apache.cassandra.auth.Permission;
+import org.apache.cassandra.cql3.CQLStatement;
+import org.apache.cassandra.cql3.QualifiedName;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.Keyspaces;
+import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.schema.TableParams;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.tcm.ClusterMetadata;
+import org.apache.cassandra.transport.Event.SchemaChange;
+import org.apache.cassandra.transport.Event.SchemaChange.Change;
+import org.apache.cassandra.transport.Event.SchemaChange.Target;
+
+/**
+ * Handles the execution of SECURITY LABEL ON TABLE CQL statements.
+ * <p>
+ * This statement allows users to add security classification labels to tables.
+ * Security labels are stored in the table metadata and displayed when using DESCRIBE TABLE.
+ * </p>
+ * <p>
+ * Syntax: {@code SECURITY LABEL [FOR provider_name] ON TABLE [keyspace_name.]table_name IS 'label';}
+ * </p>
+ * <p>
+ * Security labels can be used to mark data sensitivity levels or compliance requirements.
+ * Labels can be removed by setting them to NULL.
+ * </p>
+ * <h3>Provider Support</h3>
+ * <p>
+ * The optional {@code FOR provider_name} clause is accepted in the syntax to support future
+ * extensibility. Providers are intended to be pluggable security policy modules that can
+ * interpret and enforce access controls based on security labels. However, provider
+ * functionality is not currently implemented. If a provider is specified, a warning will be
+ * issued but the security label will still be applied. The provider parameter is reserved
+ * for future use.
+ * </p>
+ * <p>
+ * Example usage:
+ * <pre>
+ * // Basic syntax without provider
+ * SECURITY LABEL ON TABLE users IS 'restricted';
+ *
+ * // Syntax with provider (accepted but not yet implemented)
+ * SECURITY LABEL FOR custom_provider ON TABLE users IS 'classified';
+ *
+ * // Remove security label
+ * SECURITY LABEL ON TABLE users IS NULL;
+ * </pre>
+ * </p>
+ *
+ * @see SecurityLabelOnKeyspaceStatement
+ * @see SecurityLabelOnColumnStatement
+ * @see SecurityLabelOnUserTypeStatement
+ */
+public final class SecurityLabelOnTableStatement extends SchemaDescriptionStatement
+{
+ private final String tableName;
+ private final String provider;
+
+ public SecurityLabelOnTableStatement(String keyspaceName, String tableName, String securityLabel, String provider)
+ {
+ super(keyspaceName, securityLabel, DescriptionType.SECURITY_LABEL);
+ this.tableName = tableName;
+ this.provider = provider;
+ }
+
+ @Override
+ public Keyspaces apply(ClusterMetadata metadata)
+ {
+ Keyspaces schema = metadata.schema.getKeyspaces();
+
+ TableMetadata table = validateAndGetTable(schema, tableName);
+ providerWarning(provider);
+
+ TableParams newParams = table.params.unbuild().securityLabel(effectiveDescription()).build();
+ TableMetadata newTable = table.withSwapped(newParams);
+
+ KeyspaceMetadata keyspace = schema.getNullable(keyspaceName);
+ KeyspaceMetadata newKeyspace = keyspace.withSwapped(keyspace.tables.withSwapped(newTable));
+
+ return schema.withAddedOrUpdated(newKeyspace);
+ }
+
+ @Override
+ SchemaChange schemaChangeEvent(KeyspacesDiff diff)
+ {
+ return new SchemaChange(Change.UPDATED, Target.TABLE, keyspaceName, tableName);
+ }
+
+ @Override
+ public void authorize(ClientState client)
+ {
+ client.ensureTablePermission(keyspaceName, tableName, Permission.ALTER);
+ }
+
+ @Override
+ public AuditLogContext getAuditLogContext()
+ {
+ return new AuditLogContext(AuditLogEntryType.SECURITY_LABEL_TABLE, keyspaceName, tableName);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s (%s.%s, %s)", getClass().getSimpleName(), keyspaceName, tableName, description);
+ }
+
+ public static final class Raw extends CQLStatement.Raw
+ {
+ private final QualifiedName tableName;
+ private final String securityLabel;
+ private final String provider;
+
+ public Raw(QualifiedName tableName, String securityLabel, String provider)
+ {
+ this.tableName = tableName;
+ this.securityLabel = securityLabel;
+ this.provider = provider;
+ }
+
+ @Override
+ public SecurityLabelOnTableStatement prepare(ClientState state)
+ {
+ String keyspaceName = tableName.hasKeyspace() ? tableName.getKeyspace() : state.getKeyspace();
+ return new SecurityLabelOnTableStatement(keyspaceName,
+ tableName.getName(),
+ securityLabel,
+ provider);
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/SecurityLabelOnUserTypeFieldStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/SecurityLabelOnUserTypeFieldStatement.java
new file mode 100644
index 0000000000..fee6fa6b2f
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/SecurityLabelOnUserTypeFieldStatement.java
@@ -0,0 +1,157 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.cql3.statements.schema;
+
+import org.apache.cassandra.audit.AuditLogContext;
+import org.apache.cassandra.audit.AuditLogEntryType;
+import org.apache.cassandra.auth.Permission;
+import org.apache.cassandra.cql3.CQLStatement;
+import org.apache.cassandra.cql3.FieldIdentifier;
+import org.apache.cassandra.cql3.UTName;
+import org.apache.cassandra.db.marshal.UserType;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.Keyspaces;
+import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.service.ClientWarn;
+import org.apache.cassandra.tcm.ClusterMetadata;
+import org.apache.cassandra.transport.Event.SchemaChange;
+import org.apache.cassandra.transport.Event.SchemaChange.Change;
+import org.apache.cassandra.transport.Event.SchemaChange.Target;
+
+/**
+ * Handles the execution of SECURITY LABEL ON FIELD CQL statements.
+ * <p>
+ * This statement allows users to add security classification labels to individual fields of user-defined types.
+ * Security labels are stored in the type metadata and displayed when using DESCRIBE TYPE.
+ * </p>
+ * <p>
+ * Syntax: {@code SECURITY LABEL [FOR provider_name] ON FIELD [keyspace_name.]type_name.field_name IS 'label';}
+ * </p>
+ * <p>
+ * Security labels can be used to mark data sensitivity levels or compliance requirements.
+ * Labels can be removed by setting them to NULL.
+ * </p>
+ * <h3>Provider Support</h3>
+ * <p>
+ * The optional {@code FOR provider_name} clause is accepted in the syntax to support future
+ * extensibility. Providers are intended to be pluggable security policy modules that can
+ * interpret and enforce access controls based on security labels. However, provider
+ * functionality is not currently implemented. If a provider is specified, a warning will be
+ * issued but the security label will still be applied. The provider parameter is reserved
+ * for future use.
+ * </p>
+ * <p>
+ * Example usage:
+ * <pre>
+ * // Basic syntax without provider
+ * SECURITY LABEL ON FIELD address.street IS 'public';
+ *
+ * // Syntax with provider (accepted but not yet implemented)
+ * SECURITY LABEL FOR custom_provider ON FIELD address.street IS 'confidential';
+ *
+ * // Remove security label
+ * SECURITY LABEL ON FIELD address.street IS NULL;
+ * </pre>
+ * </p>
+ *
+ * @see SecurityLabelOnUserTypeStatement
+ * @see SecurityLabelOnColumnStatement
+ */
+public final class SecurityLabelOnUserTypeFieldStatement extends SchemaDescriptionStatement
+{
+ private final String typeName;
+ private final FieldIdentifier fieldName;
+ private final String provider;
+
+ public SecurityLabelOnUserTypeFieldStatement(String keyspaceName, String typeName, FieldIdentifier fieldName, String securityLabel, String provider)
+ {
+ super(keyspaceName, securityLabel, DescriptionType.SECURITY_LABEL);
+ this.typeName = typeName;
+ this.fieldName = fieldName;
+ this.provider = provider;
+ }
+
+ @Override
+ public Keyspaces apply(ClusterMetadata metadata)
+ {
+ Keyspaces schema = metadata.schema.getKeyspaces();
+ UserType type = validateAndGetTypeField(schema, typeName, fieldName);
+
+ if (provider != null)
+ ClientWarn.instance.warn("Provider functionality not implemented." +
+ " Provider '" + provider + "' will not be loaded or invoked.");
+
+ UserType newType = type.withFieldSecurityLabel(fieldName, effectiveDescription());
+ KeyspaceMetadata keyspace = schema.getNullable(keyspaceName);
+ KeyspaceMetadata newKeyspace = keyspace.withSwapped(keyspace.types.withUpdatedUserType(newType));
+
+ return schema.withAddedOrUpdated(newKeyspace);
+ }
+
+ @Override
+ SchemaChange schemaChangeEvent(KeyspacesDiff diff)
+ {
+ return new SchemaChange(Change.UPDATED, Target.TYPE, keyspaceName, typeName);
+ }
+
+ @Override
+ public void authorize(ClientState client)
+ {
+ client.ensureAllTablesPermission(keyspaceName, Permission.ALTER);
+ }
+
+ @Override
+ public AuditLogContext getAuditLogContext()
+ {
+ return new AuditLogContext(AuditLogEntryType.SECURITY_LABEL_TYPE, keyspaceName, typeName);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s (%s.%s.%s, %s)", getClass().getSimpleName(), keyspaceName, typeName, fieldName, description);
+ }
+
+ public static final class Raw extends CQLStatement.Raw
+ {
+ private final UTName typeName;
+ private final FieldIdentifier fieldName;
+ private final String securityLabel;
+ private final String provider;
+
+ public Raw(UTName typeName, FieldIdentifier fieldName, String securityLabel, String provider)
+ {
+ this.typeName = typeName;
+ this.fieldName = fieldName;
+ this.securityLabel = securityLabel;
+ this.provider = provider;
+ }
+
+ @Override
+ public SecurityLabelOnUserTypeFieldStatement prepare(ClientState state)
+ {
+ String keyspaceName = typeName.hasKeyspace() ? typeName.getKeyspace() : state.getKeyspace();
+ return new SecurityLabelOnUserTypeFieldStatement(keyspaceName,
+ typeName.getStringTypeName(),
+ fieldName,
+ securityLabel,
+ provider);
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/SecurityLabelOnUserTypeStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/SecurityLabelOnUserTypeStatement.java
new file mode 100644
index 0000000000..cd30e456a3
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/SecurityLabelOnUserTypeStatement.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.cql3.statements.schema;
+
+import org.apache.cassandra.audit.AuditLogContext;
+import org.apache.cassandra.audit.AuditLogEntryType;
+import org.apache.cassandra.auth.Permission;
+import org.apache.cassandra.cql3.CQLStatement;
+import org.apache.cassandra.cql3.UTName;
+import org.apache.cassandra.db.marshal.UserType;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.Keyspaces;
+import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.tcm.ClusterMetadata;
+import org.apache.cassandra.transport.Event.SchemaChange;
+import org.apache.cassandra.transport.Event.SchemaChange.Change;
+import org.apache.cassandra.transport.Event.SchemaChange.Target;
+
+/**
+ * Handles the execution of SECURITY LABEL ON TYPE CQL statements.
+ * <p>
+ * This statement allows users to add security classification labels to user-defined types.
+ * Security labels are stored in the type metadata and displayed when using DESCRIBE TYPE.
+ * </p>
+ * <p>
+ * Syntax: {@code SECURITY LABEL [FOR provider_name] ON TYPE [keyspace_name.]type_name IS 'label';}
+ * </p>
+ * <p>
+ * Security labels can be used to mark data sensitivity levels or compliance requirements.
+ * Labels can be removed by setting them to NULL.
+ * </p>
+ * <h3>Provider Support</h3>
+ * <p>
+ * The optional {@code FOR provider_name} clause is accepted in the syntax to support future
+ * extensibility. Providers are intended to be pluggable security policy modules that can
+ * interpret and enforce access controls based on security labels. However, provider
+ * functionality is not currently implemented. If a provider is specified, a warning will be
+ * issued but the security label will still be applied. The provider parameter is reserved
+ * for future use.
+ * </p>
+ * <p>
+ * Example usage:
+ * <pre>
+ * // Basic syntax without provider
+ * SECURITY LABEL ON TYPE address IS 'internal';
+ *
+ * // Syntax with provider (accepted but not yet implemented)
+ * SECURITY LABEL FOR custom_provider ON TYPE address IS 'restricted';
+ *
+ * // Remove security label
+ * SECURITY LABEL ON TYPE address IS NULL;
+ * </pre>
+ * </p>
+ *
+ * @see SecurityLabelOnKeyspaceStatement
+ * @see SecurityLabelOnTableStatement
+ * @see SecurityLabelOnColumnStatement
+ */
+public final class SecurityLabelOnUserTypeStatement extends SchemaDescriptionStatement
+{
+ private final String typeName;
+ private final String provider;
+
+ public SecurityLabelOnUserTypeStatement(String keyspaceName, String typeName, String securityLabel, String provider)
+ {
+ super(keyspaceName, securityLabel, DescriptionType.SECURITY_LABEL);
+ this.typeName = typeName;
+ this.provider = provider;
+ }
+
+ @Override
+ public Keyspaces apply(ClusterMetadata metadata)
+ {
+ Keyspaces schema = metadata.schema.getKeyspaces();
+ UserType type = validateAndGetType(schema, typeName);
+ providerWarning(provider);
+
+ UserType newType = type.withSecurityLabel(effectiveDescription());
+ KeyspaceMetadata keyspace = schema.getNullable(keyspaceName);
+ KeyspaceMetadata newKeyspace = keyspace.withSwapped(keyspace.types.withUpdatedUserType(newType));
+
+ return schema.withAddedOrUpdated(newKeyspace);
+ }
+
+ @Override
+ SchemaChange schemaChangeEvent(KeyspacesDiff diff)
+ {
+ return new SchemaChange(Change.UPDATED, Target.TYPE, keyspaceName, typeName);
+ }
+
+ @Override
+ public void authorize(ClientState client)
+ {
+ client.ensureAllTablesPermission(keyspaceName, Permission.ALTER);
+ }
+
+ @Override
+ public AuditLogContext getAuditLogContext()
+ {
+ return new AuditLogContext(AuditLogEntryType.SECURITY_LABEL_TYPE, keyspaceName, typeName);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s (%s.%s, %s)", getClass().getSimpleName(), keyspaceName, typeName, description);
+ }
+
+ public static final class Raw extends CQLStatement.Raw
+ {
+ private final UTName typeName;
+ private final String securityLabel;
+ private final String provider;
+
+ public Raw(UTName typeName, String securityLabel, String provider)
+ {
+ this.typeName = typeName;
+ this.securityLabel = securityLabel;
+ this.provider = provider;
+ }
+
+ @Override
+ public SecurityLabelOnUserTypeStatement prepare(ClientState state)
+ {
+ String keyspaceName = typeName.hasKeyspace() ? typeName.getKeyspace() : state.getKeyspace();
+ return new SecurityLabelOnUserTypeStatement(keyspaceName,
+ typeName.getStringTypeName(),
+ securityLabel,
+ provider);
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/db/marshal/UserType.java b/src/java/org/apache/cassandra/db/marshal/UserType.java
index 15ab78e82a..01d1d6a64e 100644
--- a/src/java/org/apache/cassandra/db/marshal/UserType.java
+++ b/src/java/org/apache/cassandra/db/marshal/UserType.java
@@ -31,6 +31,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.cql3.*;
+import org.apache.cassandra.cql3.statements.SchemaDescriptionsUtil;
import org.apache.cassandra.cql3.terms.Constants;
import org.apache.cassandra.cql3.terms.MultiElements;
import org.apache.cassandra.cql3.terms.Term;
@@ -63,22 +64,50 @@ public class UserType extends TupleType implements SchemaElement
private static final Logger logger = LoggerFactory.getLogger(UserType.class);
private static final ConflictBehavior CONFLICT_BEHAVIOR = ConflictBehavior.get();
+ private static final String EMPTY_COMMENT = "";
+ private static final String EMPTY_SECURITY_LABEL = "";
public final String keyspace;
public final ByteBuffer name;
+ public final String comment;
+ public final String securityLabel;
private final List<FieldIdentifier> fieldNames;
private final List<String> stringFieldNames;
+ private final Map<FieldIdentifier, String> fieldComments;
+ private final Map<FieldIdentifier, String> fieldSecurityLabels;
private final boolean isMultiCell;
private final UserTypeSerializer serializer;
public UserType(String keyspace, ByteBuffer name, List<FieldIdentifier> fieldNames, List<AbstractType<?>> fieldTypes, boolean isMultiCell)
+ {
+ this(keyspace, name, fieldNames, fieldTypes, isMultiCell, EMPTY_COMMENT, EMPTY_SECURITY_LABEL, Collections.emptyMap(), Collections.emptyMap());
+ }
+
+ public UserType(String keyspace, ByteBuffer name, List<FieldIdentifier> fieldNames, List<AbstractType<?>> fieldTypes, boolean isMultiCell, String comment, String securityLabel)
+ {
+ this(keyspace, name, fieldNames, fieldTypes, isMultiCell, comment, securityLabel, Collections.emptyMap(), Collections.emptyMap());
+ }
+
+ public UserType(String keyspace,
+ ByteBuffer name,
+ List<FieldIdentifier> fieldNames,
+ List<AbstractType<?>> fieldTypes,
+ boolean isMultiCell,
+ String comment,
+ String securityLabel,
+ Map<FieldIdentifier, String> fieldComments,
+ Map<FieldIdentifier, String> fieldSecurityLabels)
{
super(fieldTypes, false);
assert fieldNames.size() == fieldTypes.size();
this.keyspace = keyspace;
this.name = name;
+ this.comment = comment == null ? EMPTY_COMMENT : comment;
+ this.securityLabel = securityLabel == null ? EMPTY_SECURITY_LABEL : securityLabel;
this.fieldNames = fieldNames;
this.stringFieldNames = new ArrayList<>(fieldNames.size());
+ this.fieldComments = Collections.unmodifiableMap(new HashMap<>(fieldComments));
+ this.fieldSecurityLabels = Collections.unmodifiableMap(new HashMap<>(fieldSecurityLabels));
this.isMultiCell = isMultiCell;
LinkedHashMap<String , TypeSerializer<?>> fieldSerializers = new LinkedHashMap<>(fieldTypes.size());
@@ -106,7 +135,7 @@ public class UserType extends TupleType implements SchemaElement
columnTypes.add(p.right);
}
- return new UserType(keyspace, name, columnNames, columnTypes, true);
+ return new UserType(keyspace, name, columnNames, columnTypes, true, EMPTY_COMMENT, EMPTY_SECURITY_LABEL);
}
@Override
@@ -311,13 +340,13 @@ public class UserType extends TupleType implements SchemaElement
@Override
public UserType freeze()
{
- return isMultiCell ? new UserType(keyspace, name, fieldNames, fieldTypes(), false) : this;
+ return isMultiCell ? new UserType(keyspace, name, fieldNames, fieldTypes(), false, comment, securityLabel, fieldComments, fieldSecurityLabels) : this;
}
@Override
public UserType unfreeze()
{
- return isMultiCell ? this : new UserType(keyspace, name, fieldNames, fieldTypes(), true);
+ return isMultiCell ? this : new UserType(keyspace, name, fieldNames, fieldTypes(), true, comment, securityLabel, fieldComments, fieldSecurityLabels);
}
@Override
@@ -442,14 +471,66 @@ public class UserType extends TupleType implements SchemaElement
{
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);
+ else
+ newFieldComments.put(fieldName, fieldComment);
+
+ return new UserType(keyspace, name, fieldNames, fieldTypes(), isMultiCell(), comment, securityLabel, newFieldComments, fieldSecurityLabels);
+ }
+
+ public UserType withFieldSecurityLabel(FieldIdentifier fieldName, String fieldSecurityLabel)
+ {
+ if (fieldPosition(fieldName) == -1)
+ throw new IllegalArgumentException(String.format("Field '%s' doesn't exist in type '%s.%s'", fieldName, keyspace, getNameAsString()));
+
+ Map<FieldIdentifier, String> newFieldSecurityLabels = new HashMap<>(fieldSecurityLabels);
+ if (fieldSecurityLabel == null || fieldSecurityLabel.isEmpty())
+ newFieldSecurityLabels.remove(fieldName);
+ else
+ newFieldSecurityLabels.put(fieldName, fieldSecurityLabel);
+
+ return new UserType(keyspace, name, fieldNames, fieldTypes(), isMultiCell(), comment, securityLabel, fieldComments, newFieldSecurityLabels);
+ }
+
+ public String fieldComment(FieldIdentifier fieldName)
+ {
+ return fieldComments.getOrDefault(fieldName, "");
+ }
+
+ public String fieldSecurityLabel(FieldIdentifier fieldName)
+ {
+ return fieldSecurityLabels.getOrDefault(fieldName, "");
}
@Override
@@ -629,6 +710,16 @@ public class UserType extends TupleType implements SchemaElement
return builder.toString();
}
+ @Override
+ public String describe(boolean withWarnings, boolean withInternals, boolean ifNotExists)
+ {
+ String baseStatement = toCqlString(withWarnings, withInternals, ifNotExists);
+ StringBuilder result = new StringBuilder(baseStatement);
+ SchemaDescriptionsUtil.appendCommentOnType(result, this);
+ SchemaDescriptionsUtil.appendSecurityLabelOnType(result, this);
+ return result.toString();
+ }
+
@Override
protected String componentOrFieldName(int i)
{
diff --git a/src/java/org/apache/cassandra/db/virtual/AbstractSchemaMetadataTable.java b/src/java/org/apache/cassandra/db/virtual/AbstractSchemaMetadataTable.java
new file mode 100644
index 0000000000..dbcb3d95cc
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/virtual/AbstractSchemaMetadataTable.java
@@ -0,0 +1,312 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.db.virtual;
+
+import org.apache.cassandra.cql3.FieldIdentifier;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.marshal.CompositeType;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.dht.LocalPartitioner;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.schema.ColumnMetadata;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.Schema;
+import org.apache.cassandra.schema.SchemaProvider;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.db.marshal.UserType;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+import com.google.common.base.Strings;
+
+/**
+ * Abstract base class for virtual tables that expose metadata on schema elements.
+ * <p>
+ * This class provides a unified implementation for tables that expose metadata across:
+ * <ul>
+ * <li>Keyspaces - metadata on keyspaces</li>
+ * <li>Tables - metadata on tables</li>
+ * <li>Columns - metadata on columns</li>
+ * <li>User-Defined Types (UDTs) - metadata on UDTs</li>
+ * </ul>
+ * </p>
+ * <p>
+ * Subclasses must implement methods to extract the specific metadata field
+ * (e.g., comment, security label) from each schema element type.
+ * </p>
+ */
+abstract class AbstractSchemaMetadataTable extends AbstractVirtualTable
+{
+ /*
+ As clustering keys cannot be null, using an EMPTY_VALUE in the results for non-existent
+ columns in query results.
+ */
+ private static final String EMPTY_VALUE = "";
+
+ private static final String OBJECT_TYPE = "object_type";
+ private static final String KEYSPACE_NAME = "keyspace_name";
+ private static final String TABLE_NAME = "table_name";
+ private static final String COLUMN_NAME = "column_name";
+ private static final String UDT_NAME = "udt_name";
+ private static final String FIELD_NAME = "field_name";
+
+ private final SchemaTableType schemaTableType;
+
+ /**
+ * Schema object types that can have metadata.
+ */
+ protected enum ObjectType
+ {
+ KEYSPACE,
+ TABLE,
+ COLUMN,
+ UDT,
+ FIELD;
+
+ @Override
+ public String toString()
+ {
+ return name();
+ }
+
+ /**
+ * Parse object type string to enum.
+ * @param objectType the string representation of the object type
+ * @return ObjectType enum value or null if invalid
+ */
+ static ObjectType parse(String objectType)
+ {
+ try
+ {
+ return ObjectType.valueOf(objectType);
+ }
+ catch (IllegalArgumentException e)
+ {
+ return null;
+ }
+ }
+ }
+
+ protected enum SchemaTableType
+ {
+ COMMENT("schema_comments", "comment"),
+ SECURITY_LABEL("schema_security_labels", "security_label");
+
+ private final String tableName;
+ private final String columnName;
+
+ SchemaTableType(String tableName, String columnName)
+ {
+ this.tableName = tableName;
+ this.columnName = columnName;
+ }
+ }
+
+ protected AbstractSchemaMetadataTable(String keyspace, SchemaTableType schemaTableType)
+ {
+ super(TableMetadata.builder(keyspace, schemaTableType.tableName)
+ .kind(TableMetadata.Kind.VIRTUAL)
+ .partitioner(new LocalPartitioner(CompositeType.getInstance(UTF8Type.instance, UTF8Type.instance)))
+ .addPartitionKeyColumn(OBJECT_TYPE, UTF8Type.instance)
+ .addPartitionKeyColumn(KEYSPACE_NAME, UTF8Type.instance)
+ .addClusteringColumn(TABLE_NAME, UTF8Type.instance)
+ .addClusteringColumn(COLUMN_NAME, UTF8Type.instance)
+ .addClusteringColumn(UDT_NAME, UTF8Type.instance)
+ .addClusteringColumn(FIELD_NAME, UTF8Type.instance)
+ .addRegularColumn(schemaTableType.columnName, UTF8Type.instance)
+ .build());
+ this.schemaTableType = schemaTableType;
+ }
+
+ /**
+ * Extract metadata from a keyspace.
+ * @param keyspace the keyspace metadata
+ * @return the metadata value, or null if not present
+ */
+ protected abstract String extractKeyspaceMetadata(KeyspaceMetadata keyspace);
+
+ /**
+ * Extract metadata from a table.
+ * @param table the table metadata
+ * @return the metadata value, or null if not present
+ */
+ protected abstract String extractTableMetadata(TableMetadata table);
+
+ /**
+ * Extract metadata from a column.
+ * @param column the column metadata
+ * @return the metadata value, or null if not present
+ */
+ protected abstract String extractColumnMetadata(ColumnMetadata column);
+
+ /**
+ * Extract metadata from a UDT.
+ * @param udt the user-defined type
+ * @return the metadata value, or null if not present
+ */
+ protected abstract String extractUdtMetadata(UserType udt);
+
+ /**
+ * Extract metadata from a UDT field.
+ * @param udt the user-defined type
+ * @param fieldName the field name
+ * @return the metadata value, or null if not present
+ */
+ protected abstract String extractFieldMetadata(UserType udt, String fieldName);
+
+ @Override
+ public DataSet data()
+ {
+ SimpleDataSet result = new SimpleDataSet(metadata());
+ SchemaProvider schemaProvider = Schema.instance;
+
+ for (String keyspaceName : schemaProvider.getKeyspaces())
+ {
+ KeyspaceMetadata keyspace = schemaProvider.getKeyspaceMetadata(keyspaceName);
+ if (keyspace == null)
+ {
+ continue;
+ }
+ addKeyspaceRow(result, keyspace);
+
+ for (TableMetadata table : keyspace.tables)
+ {
+ addTableRow(result, keyspace, table);
+
+ for (ColumnMetadata column : table.columns())
+ addColumnRow(result, keyspace, table, column);
+ }
+
+ for (UserType udt : keyspace.types)
+ {
+ addUdtRow(result, keyspace, udt);
+
+ for (FieldIdentifier field : udt.fieldNames())
+ addFieldRow(result, keyspace, udt, field.toString());
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public DataSet data(DecoratedKey partitionKey)
+ {
+ SimpleDataSet result = new SimpleDataSet(metadata());
+
+ ByteBuffer key = partitionKey.getKey();
+ ByteBuffer[] components = ((CompositeType) metadata().partitionKeyType).split(key);
+ String objectType = UTF8Type.instance.compose(components[0]);
+ String keyspaceName = UTF8Type.instance.compose(components[1]);
+
+ KeyspaceMetadata keyspace = Schema.instance.getKeyspaceMetadata(keyspaceName);
+ if (keyspace == null)
+ throw new InvalidRequestException("Unknown keyspace: '" + keyspaceName + '\'');
+
+ ObjectType type = ObjectType.parse(objectType);
+ if (type == null)
+ throw new InvalidRequestException("Unknown object type: '" + objectType +
+ "'. Valid types are: " + Arrays.toString(ObjectType.values()));
+
+ switch (type)
+ {
+ case KEYSPACE:
+ addKeyspaceRow(result, keyspace);
+ break;
+ case TABLE:
+ for (TableMetadata table : keyspace.tables)
+ addTableRow(result, keyspace, table);
+ break;
+ case COLUMN:
+ for (TableMetadata table : keyspace.tables)
+ for (ColumnMetadata column : table.columns())
+ addColumnRow(result, keyspace, table, column);
+ break;
+ case UDT:
+ for (UserType udt : keyspace.types)
+ addUdtRow(result, keyspace, udt);
+ break;
+ case FIELD:
+ for (UserType udt : keyspace.types)
+ for (FieldIdentifier field : udt.fieldNames())
+ addFieldRow(result, keyspace, udt, field.toString());
+ break;
+ }
+ return result;
+ }
+
+ /**
+ * Add a row for keyspace metadata if present.
+ */
+ private void addKeyspaceRow(SimpleDataSet result, KeyspaceMetadata keyspace)
+ {
+ String metadata = Strings.emptyToNull(extractKeyspaceMetadata(keyspace));
+
+ if (metadata != null)
+ result.row(ObjectType.KEYSPACE.toString(), keyspace.name, EMPTY_VALUE, EMPTY_VALUE, EMPTY_VALUE, EMPTY_VALUE)
+ .column(schemaTableType.columnName, metadata);
+ }
+
+ /**
+ * Add a row for table metadata if present.
+ */
+ private void addTableRow(SimpleDataSet result, KeyspaceMetadata keyspace, TableMetadata table)
+ {
+ String metadata = Strings.emptyToNull(extractTableMetadata(table));
+
+ if (metadata != null)
+ result.row(ObjectType.TABLE.toString(), keyspace.name, table.name, EMPTY_VALUE, EMPTY_VALUE, EMPTY_VALUE)
+ .column(schemaTableType.columnName, metadata);
+ }
+
+ /**
+ * Add a row for column metadata if present.
+ */
+ private void addColumnRow(SimpleDataSet result, KeyspaceMetadata keyspace, TableMetadata table, ColumnMetadata column)
+ {
+ String metadata = Strings.emptyToNull(extractColumnMetadata(column));
+
+ if (metadata != null)
+ result.row(ObjectType.COLUMN.toString(), keyspace.name, table.name, column.name.toString(), EMPTY_VALUE, EMPTY_VALUE)
+ .column(schemaTableType.columnName, metadata);
+ }
+
+ /**
+ * Add a row for UDT metadata if present.
+ */
+ private void addUdtRow(SimpleDataSet result, KeyspaceMetadata keyspace, UserType udt)
+ {
+ String metadata = Strings.emptyToNull(extractUdtMetadata(udt));
+
+ if (metadata != null)
+ result.row(ObjectType.UDT.toString(), keyspace.name, EMPTY_VALUE, EMPTY_VALUE, udt.getNameAsString(), EMPTY_VALUE)
+ .column(schemaTableType.columnName, metadata);
+ }
+
+ /**
+ * Add a row for UDT field metadata if present.
+ */
+ private void addFieldRow(SimpleDataSet result, KeyspaceMetadata keyspace, UserType udt, String fieldName)
+ {
+ String metadata = Strings.emptyToNull(extractFieldMetadata(udt, fieldName));
+
+ if (metadata != null)
+ result.row(ObjectType.FIELD.toString(), keyspace.name, EMPTY_VALUE, EMPTY_VALUE, udt.getNameAsString(), fieldName)
+ .column(schemaTableType.columnName, metadata);
+ }
+}
diff --git a/src/java/org/apache/cassandra/db/virtual/SchemaCommentsTable.java b/src/java/org/apache/cassandra/db/virtual/SchemaCommentsTable.java
new file mode 100644
index 0000000000..c67395cb79
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/virtual/SchemaCommentsTable.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.db.virtual;
+
+import org.apache.cassandra.cql3.FieldIdentifier;
+import org.apache.cassandra.schema.ColumnMetadata;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.db.marshal.UserType;
+
+/**
+ * Virtual table that exposes all comments on schema elements.
+ * <p>
+ * This table provides a unified view of documentation metadata across:
+ * <ul>
+ * <li>Keyspaces - comments on keyspaces</li>
+ * <li>Tables - comments on tables</li>
+ * <li>Columns - comments on columns</li>
+ * <li>User-Defined Types (UDTs) - comments on UDTs</li>
+ * </ul>
+ * </p>
+ * <p>
+ * The table automatically reflects the current state of schema metadata without requiring
+ * explicit updates. Data is read directly from {@link org.apache.cassandra.schema.Schema#instance} on each query.
+ * </p>
+ * <p>
+ * Example queries:
+ * <pre>
+ * -- All comments in the system
+ * SELECT * FROM system_views.schema_comments;
+ *
+ * -- All table comments in a specific keyspace
+ * SELECT * FROM system_views.schema_comments
+ * WHERE object_type = 'TABLE' AND keyspace_name = 'my_keyspace';
+ *
+ * -- All column comments across all keyspaces
+ * SELECT * FROM system_views.schema_comments WHERE object_type = 'COLUMN';
+ * </pre>
+ * </p>
+ */
+final class SchemaCommentsTable extends AbstractSchemaMetadataTable
+{
+ SchemaCommentsTable(String keyspace)
+ {
+ super(keyspace, SchemaTableType.COMMENT);
+ }
+
+ @Override
+ protected String extractKeyspaceMetadata(KeyspaceMetadata keyspace)
+ {
+ return keyspace.params.comment;
+ }
+
+ @Override
+ protected String extractTableMetadata(TableMetadata table)
+ {
+ return table.params.comment;
+ }
+
+ @Override
+ protected String extractColumnMetadata(ColumnMetadata column)
+ {
+ return column.comment;
+ }
+
+ @Override
+ protected String extractUdtMetadata(UserType udt)
+ {
+ return udt.comment;
+ }
+
+ @Override
+ protected String extractFieldMetadata(UserType udt, String fieldName)
+ {
+ return udt.fieldComment(FieldIdentifier.forUnquoted(fieldName));
+ }
+}
diff --git a/src/java/org/apache/cassandra/db/virtual/SchemaSecurityLabelsTable.java b/src/java/org/apache/cassandra/db/virtual/SchemaSecurityLabelsTable.java
new file mode 100644
index 0000000000..1b589649d2
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/virtual/SchemaSecurityLabelsTable.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.db.virtual;
+
+import org.apache.cassandra.cql3.FieldIdentifier;
+import org.apache.cassandra.schema.ColumnMetadata;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.db.marshal.UserType;
+
+/**
+ * Virtual table that exposes all security labels on schema elements.
+ * <p>
+ * This table provides a unified view of security label metadata across:
+ * <ul>
+ * <li>Keyspaces - security labels on keyspaces</li>
+ * <li>Tables - security labels on tables</li>
+ * <li>Columns - security labels on columns</li>
+ * <li>User-Defined Types (UDTs) - security labels on UDTs</li>
+ * </ul>
+ * </p>
+ * <p>
+ * The table automatically reflects the current state of schema metadata without requiring
+ * explicit updates. Data is read directly from {@link org.apache.cassandra.schema.Schema#instance} on each query.
+ * </p>
+ * <p>
+ * Example queries:
+ * <pre>
+ * -- All security labels in the system
+ * SELECT * FROM system_views.schema_security_labels;
+ *
+ * -- All table security labels in a specific keyspace
+ * SELECT * FROM system_views.schema_security_labels
+ * WHERE object_type = 'TABLE' AND keyspace_name = 'my_keyspace';
+ *
+ * -- All column security labels across all keyspaces
+ * SELECT * FROM system_views.schema_security_labels WHERE object_type = 'COLUMN';
+ * </pre>
+ * </p>
+ */
+final class SchemaSecurityLabelsTable extends AbstractSchemaMetadataTable
+{
+ SchemaSecurityLabelsTable(String keyspace)
+ {
+ super(keyspace, SchemaTableType.SECURITY_LABEL);
+ }
+
+ @Override
+ protected String extractKeyspaceMetadata(KeyspaceMetadata keyspace)
+ {
+ return keyspace.params.securityLabel;
+ }
+
+ @Override
+ protected String extractTableMetadata(TableMetadata table)
+ {
+ return table.params.securityLabel;
+ }
+
+ @Override
+ protected String extractColumnMetadata(ColumnMetadata column)
+ {
+ return column.securityLabel;
+ }
+
+ @Override
+ protected String extractUdtMetadata(UserType udt)
+ {
+ return udt.securityLabel;
+ }
+
+ @Override
+ protected String extractFieldMetadata(UserType udt, String fieldName)
+ {
+ return udt.fieldSecurityLabel(FieldIdentifier.forUnquoted(fieldName));
+ }
+}
diff --git a/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java b/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java
index 7233bb4d53..9a80d1e9b9 100644
--- a/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java
+++ b/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java
@@ -37,6 +37,8 @@ public final class SystemViewsKeyspace extends VirtualKeyspace
.add(new ClientsTable(VIRTUAL_VIEWS))
.add(new SettingsTable(VIRTUAL_VIEWS))
.add(new SystemPropertiesTable(VIRTUAL_VIEWS))
+ .add(new SchemaCommentsTable(VIRTUAL_VIEWS))
+ .add(new SchemaSecurityLabelsTable(VIRTUAL_VIEWS))
.add(new SSTableTasksTable(VIRTUAL_VIEWS))
// Fully backward/forward compatible with the legace ThreadPoolsTable under the same "system_views.thread_pools" name.
.add(CollectionVirtualTableAdapter.create(VIRTUAL_VIEWS,
diff --git a/src/java/org/apache/cassandra/schema/ColumnMetadata.java b/src/java/org/apache/cassandra/schema/ColumnMetadata.java
index 817c99327b..69cb56bc59 100644
--- a/src/java/org/apache/cassandra/schema/ColumnMetadata.java
+++ b/src/java/org/apache/cassandra/schema/ColumnMetadata.java
@@ -251,7 +251,22 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta
@Nullable ColumnMask mask,
@Nonnull ColumnConstraints columnConstraints)
{
- super(ksName, cfName, name, type);
+ this(ksName, cfName, name, type, uniqueId, position, kind, mask, columnConstraints, EMPTY_COMMENT, EMPTY_SECURITY_LABEL);
+ }
+
+ public ColumnMetadata(String ksName,
+ String cfName,
+ ColumnIdentifier name,
+ AbstractType<?> type,
+ int uniqueId,
+ int position,
+ Kind kind,
+ @Nullable ColumnMask mask,
+ @Nonnull ColumnConstraints columnConstraints,
+ String comment,
+ String securityLabel)
+ {
+ super(ksName, cfName, name, type, comment, securityLabel);
this.uniqueId = uniqueId;
assert name != null && type != null && kind != null;
assert (position == NO_POSITION) == !kind.isPrimaryKeyKind(); // The position really only make sense for partition and clustering columns (and those must have one),
@@ -302,22 +317,32 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta
public ColumnMetadata copy()
{
- return new ColumnMetadata(ksName, cfName, name, type, uniqueId, position, kind, mask, columnConstraints);
+ return new ColumnMetadata(ksName, cfName, name, type, uniqueId, position, kind, mask, columnConstraints, comment, securityLabel);
}
public ColumnMetadata withNewName(ColumnIdentifier newName)
{
- return new ColumnMetadata(ksName, cfName, newName, type, uniqueId, position, kind, mask, columnConstraints);
+ return new ColumnMetadata(ksName, cfName, newName, type, uniqueId, position, kind, mask, columnConstraints, comment, securityLabel);
}
public ColumnMetadata withNewType(AbstractType<?> newType)
{
- return new ColumnMetadata(ksName, cfName, name, newType, uniqueId, position, kind, mask, columnConstraints);
+ return new ColumnMetadata(ksName, cfName, name, newType, uniqueId, position, kind, mask, columnConstraints, comment, securityLabel);
}
public ColumnMetadata withNewMask(@Nullable ColumnMask newMask)
{
- return new ColumnMetadata(ksName, cfName, name, type, uniqueId, position, kind, newMask, columnConstraints);
+ return new ColumnMetadata(ksName, cfName, name, type, uniqueId, position, kind, newMask, columnConstraints, comment, securityLabel);
+ }
+
+ public ColumnMetadata withNewComment(String newComment)
+ {
+ return new ColumnMetadata(ksName, cfName, name, type, uniqueId, position, kind, mask, columnConstraints, newComment, securityLabel);
+ }
+
+ public ColumnMetadata withNewSecurityLabel(String newSecurityLabel)
+ {
+ return new ColumnMetadata(ksName, cfName, name, type, uniqueId, position, kind, mask, columnConstraints, comment, newSecurityLabel);
}
public boolean isPartitionKey()
@@ -397,7 +422,13 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta
public ColumnMetadata withNewColumnConstraints(ColumnConstraints constraints)
{
constraints.validate(this);
- return new ColumnMetadata(ksName, cfName, name, type, uniqueId, position, kind, mask, constraints);
+ return new ColumnMetadata(ksName, cfName, name, type, uniqueId, position, kind, mask, constraints, comment, securityLabel);
+ }
+
+
+ public ColumnMetadata withSecurityLabel(String securityLabel)
+ {
+ return new ColumnMetadata(ksName, cfName, name, type, uniqueId, position, kind, mask, columnConstraints, comment, securityLabel);
}
public void removeColumnConstraints()
@@ -739,6 +770,11 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta
}
if (version.isAtLeast(Version.V7))
out.writeVInt32(t.uniqueId);
+ if (version.isAtLeast(Version.V8))
+ {
+ out.writeUTF(t.comment);
+ out.writeUTF(t.securityLabel);
+ }
}
public ColumnMetadata deserialize(DataInputPlus in, Types types, UserFunctions functions, Version version) throws IOException
@@ -765,7 +801,14 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta
int uniqueId = NO_UNIQUE_ID;
if (version.isAtLeast(Version.V7))
uniqueId = in.readVInt32();
- return new ColumnMetadata(ksName, tableName, new ColumnIdentifier(nameBB, name), type, uniqueId, position, kind, mask, constraints);
+ String comment = EMPTY_COMMENT;
+ String securityLabel = EMPTY_SECURITY_LABEL;
+ if (version.isAtLeast(Version.V8))
+ {
+ comment = in.readUTF();
+ securityLabel = in.readUTF();
+ }
+ return new ColumnMetadata(ksName, tableName, new ColumnIdentifier(nameBB, name), type, uniqueId, position, kind, mask, constraints, comment, securityLabel);
}
public long serializedSize(ColumnMetadata t, Version version)
@@ -777,6 +820,12 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta
if (t.hasConstraint())
constraintsSize += t.getColumnConstraints().serializer().serializedSize(t.columnConstraints, version);
}
+ long commentsAndSecurityLabelSize = 0;
+ if (version.isAtLeast(Version.V8))
+ {
+ commentsAndSecurityLabelSize += sizeof(t.comment);
+ commentsAndSecurityLabelSize += sizeof(t.securityLabel);
+ }
return sizeof(t.ksName) +
sizeof(t.cfName) +
sizeof(t.kind.name()) +
@@ -788,7 +837,8 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta
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) +
+ commentsAndSecurityLabelSize;
}
}
}
diff --git a/src/java/org/apache/cassandra/schema/KeyspaceMetadata.java b/src/java/org/apache/cassandra/schema/KeyspaceMetadata.java
index 703f1c0298..32e3eed97e 100644
--- a/src/java/org/apache/cassandra/schema/KeyspaceMetadata.java
+++ b/src/java/org/apache/cassandra/schema/KeyspaceMetadata.java
@@ -37,6 +37,7 @@ import org.apache.cassandra.cql3.SchemaElement;
import org.apache.cassandra.cql3.functions.Function;
import org.apache.cassandra.cql3.functions.UDAggregate;
import org.apache.cassandra.cql3.functions.UDFunction;
+import org.apache.cassandra.cql3.statements.SchemaDescriptionsUtil;
import org.apache.cassandra.db.marshal.UserType;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.RequestValidationException;
@@ -397,6 +398,16 @@ public final class KeyspaceMetadata implements SchemaElement
return builder.toString();
}
+ @Override
+ public String describe(boolean withWarnings, boolean withInternals, boolean ifNotExists)
+ {
+ String cqlString = toCqlString(withWarnings, withInternals, ifNotExists);
+ StringBuilder result = new StringBuilder(cqlString);
+ SchemaDescriptionsUtil.appendCommentOnKeyspace(result, this);
+ SchemaDescriptionsUtil.appendSecurityLabelOnKeyspace(result, this);
+ return result.toString();
+ }
+
public void validate(ClusterMetadata metadata)
{
validateKeyspaceName(name, ConfigurationException::new);
diff --git a/src/java/org/apache/cassandra/schema/KeyspaceParams.java b/src/java/org/apache/cassandra/schema/KeyspaceParams.java
index f683fabf49..ffc3167358 100644
--- a/src/java/org/apache/cassandra/schema/KeyspaceParams.java
+++ b/src/java/org/apache/cassandra/schema/KeyspaceParams.java
@@ -44,6 +44,8 @@ public final class KeyspaceParams
public static final Serializer serializer = new Serializer();
public static final boolean DEFAULT_DURABLE_WRITES = true;
+ private static final String EMPTY_COMMENT = "";
+ private static final String EMPTY_SECURITY_LABEL = "";
/**
* This determines durable writes for the {@link org.apache.cassandra.schema.SchemaConstants#SCHEMA_KEYSPACE_NAME}
@@ -57,7 +59,9 @@ public final class KeyspaceParams
{
DURABLE_WRITES,
REPLICATION,
- FAST_PATH;
+ FAST_PATH,
+ COMMENT,
+ SECURITY_LABEL;
@Override
public String toString()
@@ -69,17 +73,26 @@ public final class KeyspaceParams
public final boolean durableWrites;
public final ReplicationParams replication;
public final FastPathStrategy fastPath;
+ public final String comment;
+ public final String securityLabel;
public KeyspaceParams(boolean durableWrites, ReplicationParams replication, FastPathStrategy fastPath)
+ {
+ this(durableWrites, replication, fastPath, EMPTY_COMMENT, EMPTY_SECURITY_LABEL);
+ }
+
+ public KeyspaceParams(boolean durableWrites, ReplicationParams replication, FastPathStrategy fastPath, String comment, String securityLabel)
{
this.durableWrites = durableWrites;
this.replication = replication;
this.fastPath = fastPath;
+ this.comment = comment == null ? EMPTY_COMMENT : comment;
+ this.securityLabel = securityLabel == null ? EMPTY_SECURITY_LABEL : securityLabel;
}
public static KeyspaceParams create(boolean durableWrites, Map<String, String> replication, FastPathStrategy fastPath)
{
- return new KeyspaceParams(durableWrites, ReplicationParams.fromMap(replication), fastPath);
+ return new KeyspaceParams(durableWrites, ReplicationParams.fromMap(replication), fastPath, EMPTY_COMMENT, EMPTY_SECURITY_LABEL);
}
public static KeyspaceParams create(boolean durableWrites, Map<String, String> replication, Map<String, String> fastPath)
@@ -94,32 +107,42 @@ public final class KeyspaceParams
public static KeyspaceParams local()
{
- return new KeyspaceParams(DEFAULT_LOCAL_DURABLE_WRITES, ReplicationParams.local(), FastPathStrategy.simple());
+ return new KeyspaceParams(DEFAULT_LOCAL_DURABLE_WRITES, ReplicationParams.local(), FastPathStrategy.simple(), EMPTY_COMMENT, EMPTY_SECURITY_LABEL);
}
public static KeyspaceParams simple(int replicationFactor)
{
- return new KeyspaceParams(true, ReplicationParams.simple(replicationFactor), FastPathStrategy.simple());
+ return new KeyspaceParams(true, ReplicationParams.simple(replicationFactor), FastPathStrategy.simple(), EMPTY_COMMENT, EMPTY_SECURITY_LABEL);
}
public static KeyspaceParams simple(String replicationFactor)
{
- return new KeyspaceParams(true, ReplicationParams.simple(replicationFactor), FastPathStrategy.simple());
+ return new KeyspaceParams(true, ReplicationParams.simple(replicationFactor), FastPathStrategy.simple(), EMPTY_COMMENT, EMPTY_SECURITY_LABEL);
}
public static KeyspaceParams simpleTransient(int replicationFactor)
{
- return new KeyspaceParams(false, ReplicationParams.simple(replicationFactor), FastPathStrategy.simple());
+ return new KeyspaceParams(false, ReplicationParams.simple(replicationFactor), FastPathStrategy.simple(), EMPTY_COMMENT, EMPTY_SECURITY_LABEL);
}
public static KeyspaceParams nts(Object... args)
{
- return new KeyspaceParams(true, ReplicationParams.nts(args), FastPathStrategy.simple());
+ return new KeyspaceParams(true, ReplicationParams.nts(args), FastPathStrategy.simple(), EMPTY_COMMENT, EMPTY_SECURITY_LABEL);
}
public KeyspaceParams withSwapped(ReplicationParams params)
{
- return new KeyspaceParams(durableWrites, params, fastPath);
+ return new KeyspaceParams(durableWrites, params, fastPath, comment, securityLabel);
+ }
+
+ public KeyspaceParams withComment(String comment)
+ {
+ return new KeyspaceParams(durableWrites, replication, fastPath, comment, securityLabel);
+ }
+
+ public KeyspaceParams withSecurityLabel(String securityLabel)
+ {
+ return new KeyspaceParams(durableWrites, replication, fastPath, comment, securityLabel);
}
public void validate(String name, ClientState state, ClusterMetadata metadata)
@@ -154,6 +177,8 @@ public final class KeyspaceParams
.add(Option.DURABLE_WRITES.toString(), durableWrites)
.add(Option.REPLICATION.toString(), replication)
.add(Option.FAST_PATH.toString(), fastPath.toString())
+ .add(Option.COMMENT.toString(), comment)
+ .add(Option.SECURITY_LABEL.toString(), securityLabel)
.toString();
}
@@ -165,6 +190,11 @@ public final class KeyspaceParams
out.writeBoolean(t.durableWrites);
if (version.isAtLeast(MIN_ACCORD_VERSION))
FastPathStrategy.serializer.serialize(t.fastPath, out, version);
+ if (version.isAtLeast(Version.V8))
+ {
+ out.writeUTF(t.comment);
+ out.writeUTF(t.securityLabel);
+ }
}
public KeyspaceParams deserialize(DataInputPlus in, Version version) throws IOException
@@ -174,13 +204,22 @@ public final class KeyspaceParams
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) : 0) +
+ (version.isAtLeast(Version.V8) ? TypeSizes.sizeof(t.securityLabel) : 0) +
(version.isAtLeast(MIN_ACCORD_VERSION) ? FastPathStrategy.serializer.serializedSize(t.fastPath, version) : 0);
}
}
diff --git a/src/java/org/apache/cassandra/schema/TableMetadata.java b/src/java/org/apache/cassandra/schema/TableMetadata.java
index 1336747379..1c5b577793 100644
--- a/src/java/org/apache/cassandra/schema/TableMetadata.java
+++ b/src/java/org/apache/cassandra/schema/TableMetadata.java
@@ -56,6 +56,7 @@ import org.apache.cassandra.cql3.constraints.InvalidConstraintDefinitionExceptio
import org.apache.cassandra.cql3.constraints.NotNullConstraint;
import org.apache.cassandra.cql3.functions.Function;
import org.apache.cassandra.cql3.functions.masking.ColumnMask;
+import org.apache.cassandra.cql3.statements.SchemaDescriptionsUtil;
import org.apache.cassandra.db.Clustering;
import org.apache.cassandra.db.ClusteringComparator;
import org.apache.cassandra.db.Columns;
@@ -1070,7 +1071,7 @@ public class TableMetadata implements SchemaElement
static ColumnMetadata withUniqueId(ColumnMetadata prev, int uniqueId)
{
- return new ColumnMetadata(prev.ksName, prev.cfName, prev.name, prev.type, uniqueId, prev.position(), prev.kind, prev.getMask(), prev.getColumnConstraints());
+ return new ColumnMetadata(prev.ksName, prev.cfName, prev.name, prev.type, uniqueId, prev.position(), prev.kind, prev.getMask(), prev.getColumnConstraints(), prev.comment, prev.securityLabel);
}
public Builder id(TableId val)
@@ -1132,6 +1133,12 @@ public class TableMetadata implements SchemaElement
return this;
}
+ public Builder securityLabel(String val)
+ {
+ params.securityLabel(val);
+ return this;
+ }
+
public Builder compaction(CompactionParams val)
{
params.compaction(val);
@@ -1482,6 +1489,32 @@ public class TableMetadata implements SchemaElement
return this;
}
+ public Builder alterColumnComment(ColumnIdentifier name, String comment)
+ {
+ ColumnMetadata column = columns.get(name.bytes);
+ if (column == null)
+ throw new IllegalArgumentException("Column " + name + " doesn't exist");
+
+ ColumnMetadata newColumn = column.withNewComment(comment);
+
+ updateColumn(column, newColumn);
+
+ return this;
+ }
+
+ public Builder alterColumnSecurityLabel(ColumnIdentifier name, String securityLabel)
+ {
+ ColumnMetadata column = columns.get(name.bytes);
+ if (column == null)
+ throw new IllegalArgumentException("Column " + name + " doesn't exist" );
+
+ ColumnMetadata newColumn = column.withNewSecurityLabel(securityLabel);
+
+ updateColumn(column, newColumn);
+
+ return this;
+ }
+
public Builder alterColumnConstraints(ColumnIdentifier name, ColumnConstraints constraints)
{
ColumnMetadata column = columns.get(name.bytes);
@@ -1608,6 +1641,16 @@ public class TableMetadata implements SchemaElement
return builder.toString();
}
+ @Override
+ public String describe(boolean withWarnings, boolean withInternals, boolean ifNotExists)
+ {
+ String baseStatement = toCqlString(withWarnings, withInternals, ifNotExists);
+ StringBuilder result = new StringBuilder(baseStatement);
+ SchemaDescriptionsUtil.appendCommentOnTable(result, this);
+ SchemaDescriptionsUtil.appendSecurityLabelOnTable(result, this);
+ return result.toString();
+ }
+
public String toCqlString(boolean withWarnings,
boolean withDroppedColumns,
boolean withInternals,
diff --git a/src/java/org/apache/cassandra/schema/TableParams.java b/src/java/org/apache/cassandra/schema/TableParams.java
index fe0e13adf5..7f27e47b5a 100644
--- a/src/java/org/apache/cassandra/schema/TableParams.java
+++ b/src/java/org/apache/cassandra/schema/TableParams.java
@@ -70,6 +70,7 @@ import static org.apache.cassandra.schema.TableParams.Option.MEMTABLE_FLUSH_PERI
import static org.apache.cassandra.schema.TableParams.Option.MIN_INDEX_INTERVAL;
import static org.apache.cassandra.schema.TableParams.Option.PENDING_DROP;
import static org.apache.cassandra.schema.TableParams.Option.READ_REPAIR;
+import static org.apache.cassandra.schema.TableParams.Option.SECURITY_LABEL;
import static org.apache.cassandra.schema.TableParams.Option.SPECULATIVE_RETRY;
import static org.apache.cassandra.utils.LocalizeString.toLowerCaseLocalized;
@@ -101,7 +102,8 @@ public final class TableParams
TRANSACTIONAL_MODE,
TRANSACTIONAL_MIGRATION_FROM,
PENDING_DROP,
- AUTO_REPAIR;
+ AUTO_REPAIR,
+ SECURITY_LABEL;
@Override
public String toString()
@@ -111,6 +113,7 @@ public final class TableParams
}
public final String comment;
+ public final String securityLabel;
public final boolean allowAutoSnapshot;
public final double bloomFilterFpChance;
public final double crcCheckChance;
@@ -139,6 +142,7 @@ public final class TableParams
private TableParams(Builder builder)
{
comment = builder.comment;
+ securityLabel = builder.securityLabel;
allowAutoSnapshot = builder.allowAutoSnapshot;
bloomFilterFpChance = builder.bloomFilterFpChance == -1
? builder.compaction.defaultBloomFilterFbChance()
@@ -178,6 +182,7 @@ public final class TableParams
.bloomFilterFpChance(params.bloomFilterFpChance)
.caching(params.caching)
.comment(params.comment)
+ .securityLabel(params.securityLabel)
.compaction(params.compaction)
.compression(params.compression)
.memtable(params.memtable)
@@ -276,6 +281,7 @@ public final class TableParams
TableParams p = (TableParams) o;
return comment.equals(p.comment)
+ && securityLabel.equals(p.securityLabel)
&& additionalWritePolicy.equals(p.additionalWritePolicy)
&& allowAutoSnapshot == p.allowAutoSnapshot
&& bloomFilterFpChance == p.bloomFilterFpChance
@@ -305,6 +311,7 @@ public final class TableParams
public int hashCode()
{
return Objects.hashCode(comment,
+ securityLabel,
additionalWritePolicy,
allowAutoSnapshot,
bloomFilterFpChance,
@@ -335,6 +342,7 @@ public final class TableParams
{
return MoreObjects.toStringHelper(this)
.add(COMMENT.toString(), comment)
+ .add(SECURITY_LABEL.toString(), securityLabel)
.add(ADDITIONAL_WRITE_POLICY.toString(), additionalWritePolicy)
.add(ALLOW_AUTO_SNAPSHOT.toString(), allowAutoSnapshot)
.add(BLOOM_FILTER_FP_CHANCE.toString(), bloomFilterFpChance)
@@ -375,6 +383,8 @@ public final class TableParams
.newLine()
.append("AND cdc = ").append(cdc)
.newLine()
+ // TODO: AND comment should be deprecated in future releases in favor of
+ // JIRA CASSANDRA-20943 Introducing comments and security labels for schema elements
.append("AND comment = ").appendWithSingleQuotes(comment)
.newLine()
.append("AND compaction = ").append(compaction.asMap())
@@ -431,6 +441,7 @@ public final class TableParams
public static final class Builder
{
private String comment = "";
+ private String securityLabel = "";
private boolean allowAutoSnapshot = true;
private double bloomFilterFpChance = -1;
private double crcCheckChance = 1.0;
@@ -470,6 +481,12 @@ public final class TableParams
return this;
}
+ public Builder securityLabel(String val)
+ {
+ securityLabel = val;
+ return this;
+ }
+
public Builder allowAutoSnapshot(boolean val)
{
allowAutoSnapshot = val;
@@ -643,6 +660,8 @@ public final class TableParams
out.writeUnsignedVInt32(t.transactionalMigrationFrom.ordinal());
out.writeBoolean(t.pendingDrop);
}
+ if (version.isAtLeast(Version.V8))
+ out.writeUTF(t.securityLabel);
}
public TableParams deserialize(DataInputPlus in, Version version) throws IOException
@@ -674,6 +693,8 @@ public final class TableParams
.transactionalMigrationFrom(TransactionalMigrationFromMode.fromOrdinal(in.readUnsignedVInt32()))
.pendingDrop(in.readBoolean());
}
+ if (version.isAtLeast(Version.V8))
+ builder.securityLabel(in.readUTF());
return builder.build();
}
@@ -705,6 +726,10 @@ public final class TableParams
sizeofUnsignedVInt(t.transactionalMigrationFrom.ordinal()) +
sizeof(t.pendingDrop);
}
+ if (version.isAtLeast(Version.V8))
+ {
+ size += sizeof(t.securityLabel);
+ }
return size;
}
diff --git a/src/java/org/apache/cassandra/schema/Types.java b/src/java/org/apache/cassandra/schema/Types.java
index 9b6738e10f..d9c22b94ee 100644
--- a/src/java/org/apache/cassandra/schema/Types.java
+++ b/src/java/org/apache/cassandra/schema/Types.java
@@ -20,6 +20,7 @@ package org.apache.cassandra.schema;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.*;
+import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
@@ -44,6 +45,7 @@ import static com.google.common.collect.Iterables.any;
import static com.google.common.collect.Iterables.transform;
import static org.apache.cassandra.db.TypeSizes.sizeof;
+import static org.apache.cassandra.db.TypeSizes.sizeofUnsignedVInt;
import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
/**
@@ -54,6 +56,10 @@ public final class Types implements Iterable<UserType>
public static final Serializer serializer = new Serializer();
private static final Types NONE = new Types(ImmutableMap.of());
+ private static final String EMPTY_COMMENT = "";
+ private static final String EMPTY_SECURITY_LABEL = "";
+ private static final List<String> EMPTY_FIELD_COMMENTS = List.of();
+ private static final List<String> EMPTY_FIELD_SECURITY_LABELS = List.of();
private final Map<ByteBuffer, UserType> types;
@@ -362,13 +368,29 @@ public final class Types implements Iterable<UserType>
}
public void add(String name, List<String> fieldNames, List<String> fieldTypes)
+ {
+ add(name, fieldNames, fieldTypes, EMPTY_COMMENT, EMPTY_SECURITY_LABEL, EMPTY_FIELD_COMMENTS, EMPTY_FIELD_SECURITY_LABELS);
+ }
+
+ public void add(String name, List<String> fieldNames, List<String> fieldTypes, String comment, String securityLabel)
+ {
+ add(name, fieldNames, fieldTypes, comment, securityLabel, EMPTY_FIELD_COMMENTS, EMPTY_FIELD_SECURITY_LABELS);
+ }
+
+ public void add(String name,
+ List<String> fieldNames,
+ List<String> fieldTypes,
+ String comment,
+ String securityLabel,
+ List<String> fieldComments,
+ List<String> fieldSecurityLabels)
{
List<CQL3Type.Raw> rawFieldTypes =
fieldTypes.stream()
.map(CQLTypeParser::parseRaw)
.collect(toList());
- definitions.add(new RawUDT(name, fieldNames, rawFieldTypes));
+ definitions.add(new RawUDT(name, fieldNames, rawFieldTypes, comment, securityLabel, fieldComments, fieldSecurityLabels));
}
private static final class RawUDT
@@ -376,12 +398,36 @@ public final class Types implements Iterable<UserType>
final String name;
final List<String> fieldNames;
final List<CQL3Type.Raw> fieldTypes;
+ final String comment;
+ final String securityLabel;
+ final List<String> fieldComments;
+ final List<String> fieldSecurityLabels;
RawUDT(String name, List<String> fieldNames, List<CQL3Type.Raw> fieldTypes)
+ {
+ this(name, fieldNames, fieldTypes, EMPTY_COMMENT, EMPTY_SECURITY_LABEL, EMPTY_FIELD_COMMENTS, EMPTY_FIELD_SECURITY_LABELS);
+ }
+
+ RawUDT(String name, List<String> fieldNames, List<CQL3Type.Raw> fieldTypes, String comment, String securityLabel)
+ {
+ this(name, fieldNames, fieldTypes, comment, securityLabel, EMPTY_FIELD_COMMENTS, EMPTY_FIELD_SECURITY_LABELS);
+ }
+
+ RawUDT(String name,
+ List<String> fieldNames,
+ List<CQL3Type.Raw> fieldTypes,
+ String comment,
+ String securityLabel,
+ List<String> fieldComments,
+ List<String> fieldSecurityLabels)
{
this.name = name;
this.fieldNames = fieldNames;
this.fieldTypes = fieldTypes;
+ this.comment = comment;
+ this.securityLabel = securityLabel;
+ this.fieldComments = fieldComments;
+ this.fieldSecurityLabels = fieldSecurityLabels;
}
boolean referencesUserType(RawUDT other)
@@ -401,7 +447,18 @@ public final class Types implements Iterable<UserType>
.map(t -> t.prepareInternal(keyspace, types).getType())
.collect(toList());
- return new UserType(keyspace, bytes(name), preparedFieldNames, preparedFieldTypes, true);
+ Map<FieldIdentifier, String> preparedFieldComments = new HashMap<>();
+ Map<FieldIdentifier, String> preparedFieldSecurityLabels = new HashMap<>();
+
+ for (int i = 0; i < preparedFieldNames.size(); i++)
+ {
+ if (i < fieldComments.size() && !fieldComments.get(i).isEmpty())
+ preparedFieldComments.put(preparedFieldNames.get(i), fieldComments.get(i));
+ if (i < fieldSecurityLabels.size() && !fieldSecurityLabels.get(i).isEmpty())
+ preparedFieldSecurityLabels.put(preparedFieldNames.get(i), fieldSecurityLabels.get(i));
+ }
+
+ return new UserType(keyspace, bytes(name), preparedFieldNames, preparedFieldTypes, true, comment, securityLabel, preparedFieldComments, preparedFieldSecurityLabels);
}
@Override
@@ -461,7 +518,8 @@ public final class Types implements Iterable<UserType>
for (UserType type : t.types.values())
{
out.writeUTF(type.getNameAsString());
- List<String> fieldNames = type.fieldNames().stream().map(FieldIdentifier::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());
out.writeInt(fieldNames.size());
for (String s : fieldNames)
@@ -469,6 +527,12 @@ public final class Types implements Iterable<UserType>
out.writeInt(fieldTypes.size());
for (String s : fieldTypes)
out.writeUTF(s);
+ if (version.isAtLeast(Version.V8))
+ {
+ out.writeUTF(type.comment);
+ out.writeUTF(type.securityLabel);
+ serializeFieldMetadata(type, fieldIdentifiers, out);
+ }
}
}
@@ -487,7 +551,17 @@ public final class Types implements Iterable<UserType>
List<String> fieldTypes = new ArrayList<>(fieldTypeSize);
for (int x = 0; x < fieldTypeSize; x++)
fieldTypes.add(in.readUTF());
- builder.add(name, fieldNames, fieldTypes);
+ String comment = EMPTY_COMMENT;
+ String securityLabel = EMPTY_SECURITY_LABEL;
+ List<String> fieldComments = new ArrayList<>(Collections.nCopies(fieldNamesSize, ""));
+ List<String> fieldSecurityLabels = new ArrayList<>(Collections.nCopies(fieldNamesSize, ""));
+ if (version.isAtLeast(Version.V8))
+ {
+ comment = in.readUTF();
+ securityLabel = in.readUTF();
+ deserializeFieldMetadata(in, fieldComments, fieldSecurityLabels);
+ }
+ builder.add(name, fieldNames, fieldTypes, comment, securityLabel, fieldComments, fieldSecurityLabels);
}
return builder.build();
}
@@ -498,16 +572,95 @@ public final class Types implements Iterable<UserType>
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,
+ Function<FieldIdentifier, String> metadataGetter,
+ DataOutputPlus out) throws IOException
+ {
+ // Collect only non-empty indexes
+ List<Integer> nonEmptyIndexes = new ArrayList<>();
+ for (int i = 0; i < fieldIdentifiers.size(); i++)
+ {
+ String value = metadataGetter.apply(fieldIdentifiers.get(i));
+ if (!value.isEmpty())
+ nonEmptyIndexes.add(i);
+ }
+
+ // Write count followed by position-value pairs
+ out.writeUnsignedVInt32(nonEmptyIndexes.size());
+ for (int index : nonEmptyIndexes)
+ {
+ out.writeUnsignedVInt32(index);
+ out.writeUTF(metadataGetter.apply(fieldIdentifiers.get(index)));
+ }
+ }
+
+ 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.readUnsignedVInt32();
+ for (int i = 0; i < count; i++)
+ {
+ int position = in.readUnsignedVInt32();
+ String value = in.readUTF();
+ 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 (int i = 0; i < fieldIdentifiers.size(); i++)
+ {
+ String value = metadataGetter.apply(fieldIdentifiers.get(i));
+ if (!value.isEmpty())
+ {
+ nonEmptyCount++;
+ dataSize += sizeofUnsignedVInt(i); // position (int)
+ dataSize += sizeof(value); // value string
+ }
+ }
+
+ return sizeofUnsignedVInt(nonEmptyCount) + dataSize;
+ }
}
}
diff --git a/src/java/org/apache/cassandra/tcm/membership/NodeVersion.java b/src/java/org/apache/cassandra/tcm/membership/NodeVersion.java
index 64d1cc71c6..9a15951ed0 100644
--- a/src/java/org/apache/cassandra/tcm/membership/NodeVersion.java
+++ b/src/java/org/apache/cassandra/tcm/membership/NodeVersion.java
@@ -34,7 +34,7 @@ import static org.apache.cassandra.db.TypeSizes.sizeofUnsignedVInt;
public class NodeVersion implements Comparable<NodeVersion>
{
public static final Serializer serializer = new Serializer();
- public static final Version CURRENT_METADATA_VERSION = Version.V7;
+ public static final Version CURRENT_METADATA_VERSION = Version.V8;
public static final NodeVersion CURRENT = new NodeVersion(new CassandraVersion(FBUtilities.getReleaseVersionString()), CURRENT_METADATA_VERSION);
private static final CassandraVersion SINCE_VERSION = CassandraVersion.CASSANDRA_5_1;
diff --git a/src/java/org/apache/cassandra/tcm/serialization/Version.java b/src/java/org/apache/cassandra/tcm/serialization/Version.java
index 955c6996e0..c4fb39cd51 100644
--- a/src/java/org/apache/cassandra/tcm/serialization/Version.java
+++ b/src/java/org/apache/cassandra/tcm/serialization/Version.java
@@ -68,6 +68,11 @@ public enum Version
*/
V7(7),
+ /**
+ * - Comments and security labels for schema elements (keyspaces, tables, columns, UDTs, and UDT fields)
+ */
+ V8(8),
+
UNKNOWN(Integer.MAX_VALUE);
/**
diff --git a/test/unit/org/apache/cassandra/cql3/statements/DescribeStatementTest.java b/test/unit/org/apache/cassandra/cql3/statements/DescribeStatementTest.java
index eb5015da61..36355144a5 100644
--- a/test/unit/org/apache/cassandra/cql3/statements/DescribeStatementTest.java
+++ b/test/unit/org/apache/cassandra/cql3/statements/DescribeStatementTest.java
@@ -46,6 +46,7 @@ import org.apache.cassandra.transport.ProtocolVersion;
import static java.lang.String.format;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@@ -265,7 +266,8 @@ public class DescribeStatementTest extends CQLTester
" PRIMARY KEY (keyspace_name, table_name, column_name)\n" +
") WITH CLUSTERING ORDER BY (table_name ASC, column_name ASC)\n" +
" AND comment = 'virtual column definitions';\n" +
- "*/"));
+ "*/\n" +
+ "COMMENT ON TABLE system_virtual_schema.columns IS 'virtual column definitions';"));
}
@Test
@@ -845,7 +847,7 @@ public class DescribeStatementTest extends CQLTester
{
requireNetwork();
DatabaseDescriptor.setDynamicDataMaskingEnabled(true);
- String souceTable = createTable(KEYSPACE_PER_TEST,
+ String sourceTable = createTable(KEYSPACE_PER_TEST,
"CREATE TABLE %s (" +
" pk1 text, " +
" pk2 int MASKED WITH DEFAULT, " +
@@ -857,14 +859,14 @@ public class DescribeStatementTest extends CQLTester
"PRIMARY KEY ((pk1, pk2), ck1, ck2 ))");
TableMetadata source = getTableMetadata(KEYSPACE_PER_TEST, currentTable());
assertNotNull(source);
- String targetTable = createTableLike("CREATE TABLE %s LIKE %s", souceTable, KEYSPACE_PER_TEST, KEYSPACE_PER_TEST);
+ String targetTable = createTableLike("CREATE TABLE %s LIKE %s", sourceTable, KEYSPACE_PER_TEST, KEYSPACE_PER_TEST);
TableMetadata target = getTableMetadata(KEYSPACE_PER_TEST, currentTable());
assertNotNull(target);
assertTrue(equalsWithoutTableNameAndDropCns(source, target, true, true, true));
assertNotEquals(source.id, target.id);
assertNotEquals(source.name, target.name);
- String sourceTableCreateStatement = "CREATE TABLE " + KEYSPACE_PER_TEST + "." + souceTable + " (\n" +
+ String sourceTableCreateStatement = "CREATE TABLE " + KEYSPACE_PER_TEST + "." + sourceTable + " (\n" +
" pk1 text,\n" +
" pk2 int MASKED WITH system.mask_default(),\n" +
" ck1 int,\n" +
@@ -889,10 +891,10 @@ public class DescribeStatementTest extends CQLTester
" AND CLUSTERING ORDER BY (ck1 ASC, ck2 ASC)\n" +
" AND " + tableParametersCql();
- assertRowsNet(executeDescribeNet("DESCRIBE TABLE " + KEYSPACE_PER_TEST + "." + souceTable + " WITH INTERNALS"),
+ assertRowsNet(executeDescribeNet("DESCRIBE TABLE " + KEYSPACE_PER_TEST + "." + sourceTable + " WITH INTERNALS"),
row(KEYSPACE_PER_TEST,
"table",
- souceTable,
+ sourceTable,
sourceTableCreateStatement));
assertRowsNet(executeDescribeNet("DESCRIBE TABLE " + KEYSPACE_PER_TEST + "." + targetTable + " WITH INTERNALS"),
row(KEYSPACE_PER_TEST,
@@ -1050,6 +1052,174 @@ public class DescribeStatementTest extends CQLTester
}
}
+ @Test
+ public void testDescribeKeyspaceWithCommentsAndSecurityLabels() throws Throwable
+ {
+ try
+ {
+ execute("CREATE KEYSPACE test_comments WITH REPLICATION = {'class' : 'SimpleStrategy', 'replication_factor' : 1}");
+ String describeStatement = "DESCRIBE KEYSPACE test_comments";
+ String dropStatement = "DROP KEYSPACE IF EXISTS test_comments";
+ testDescribeOnSchemaElement("KEYSPACE", "test_comments", describeStatement, dropStatement);
+ }
+ finally
+ {
+ execute("DROP KEYSPACE IF EXISTS test_comments");
+ }
+ }
+
+ @Test
+ public void testDescribeTableWithCommentsAndSecurityLabels() throws Throwable
+ {
+ try
+ {
+ execute("CREATE TABLE " + KEYSPACE_PER_TEST + ".table_comment_test (id int PRIMARY KEY, name text)");
+ testDescribeOnSchemaElement("TABLE",
+ KEYSPACE_PER_TEST + ".table_comment_test",
+ "DESCRIBE TABLE " + KEYSPACE_PER_TEST + ".table_comment_test",
+ "DROP TABLE IF EXISTS " + KEYSPACE_PER_TEST + ".table_comment_test");
+ }
+ finally
+ {
+ execute("DROP TABLE IF EXISTS " + KEYSPACE_PER_TEST + ".table_comment_test");
+ }
+ }
+
+ @Test
+ public void testDescribeColumnWithCommentsAndSecurityLabels() throws Throwable
+ {
+ try
+ {
+ execute("CREATE TABLE " + KEYSPACE_PER_TEST + ".column_test (id int PRIMARY KEY, test_column text)");
+ testDescribeOnSchemaElement("COLUMN",
+ KEYSPACE_PER_TEST + ".column_test.test_column",
+ "DESCRIBE TABLE " + KEYSPACE_PER_TEST + ".column_test",
+ "ALTER TABLE " + KEYSPACE_PER_TEST + ".column_test DROP test_column");
+ }
+ finally
+ {
+ execute("DROP TABLE IF EXISTS " + KEYSPACE_PER_TEST + ".column_test");
+ }
+ }
+
+ @Test
+ public void testDescribeUserTypeWithCommentsAndSecurityLabels() throws Throwable
+ {
+ try
+ {
+ String type = createType(KEYSPACE_PER_TEST, "CREATE TYPE %s (name text, age int, address text)");
+ testDescribeOnSchemaElement("TYPE", KEYSPACE_PER_TEST + "." + type, "DESCRIBE TYPE " + KEYSPACE_PER_TEST + "." + type, "DROP TYPE IF EXISTS " + KEYSPACE_PER_TEST + "." + type);
+ }
+ finally
+ {
+ // Cleanup is handled by the base test class
+ }
+ }
+
+ @Test
+ public void testDescribeUserTypeWithFieldCommentsAndSecurityLabels() throws Throwable
+ {
+ try
+ {
+ String type = createType(KEYSPACE_PER_TEST, "CREATE TYPE %s (latitude double, longitude double)");
+ testDescribeOnSchemaElement("FIELD", KEYSPACE_PER_TEST + "." + type + ".latitude", "DESCRIBE TYPE " + KEYSPACE_PER_TEST + "." + type, "DROP TYPE " + KEYSPACE_PER_TEST + "." + type);
+
+ }
+ finally
+ {
+ // Cleanup is handled by the base test class
+ }
+ }
+
+
+ @Test
+ public void testDescribeEscapesSingleQuotesInKeyspaceComment() throws Throwable
+ {
+ String commentWithQuotes ="a''; DROP KEYSPACE " + KEYSPACE_PER_TEST + ";";
+ execute(String.format("COMMENT ON KEYSPACE %s IS '%s'", KEYSPACE_PER_TEST, commentWithQuotes));
+ String describeOutput = executeDescribeNet("DESCRIBE KEYSPACE " + KEYSPACE_PER_TEST).one().getString("create_statement");
+
+ // Verify that single quotes are properly escaped (doubled) in the output
+ assertTrue("DESCRIBE output should contain escaped single quotes in comment",
+ describeOutput.contains("COMMENT ON KEYSPACE " + KEYSPACE_PER_TEST + " IS 'a''; DROP KEYSPACE " + KEYSPACE_PER_TEST + ";';"));
+ }
+
+ /**
+ * Helper method to set comment and security label on a schema element and test DESCRIBE output.
+ * Handles special cases for FIELD and COLUMN which don't have their own DESCRIBE commands.
+ */
+ private void testDescribeOnSchemaElement(String objectType, String objectName, String describeCommand, String dropStatement) throws Throwable
+ {
+ String commentStatement = String.format("COMMENT ON %s %s IS 'testComment'", objectType, objectName);
+ String securitylabelStatement = String.format("SECURITY LABEL ON %s %s IS 'sensitive'", objectType, objectName);
+
+ testSetCommentAndLabel(describeCommand, commentStatement, securitylabelStatement);
+ testUnsetCommentAndLabel(describeCommand, objectType, objectName, commentStatement, securitylabelStatement);
+ testDropRemovesCommentAndLabel(objectType, objectName, commentStatement, securitylabelStatement, dropStatement);
+ }
+
+ private void testSetCommentAndLabel(String describeCommand, String commentStatement, String securitylabelStatement) throws Throwable
+ {
+ execute(commentStatement);
+ execute(securitylabelStatement);
+
+ String schema = describeSchemaElement(describeCommand);
+ assertTrue(schema.contains(commentStatement));
+ assertTrue(schema.contains(securitylabelStatement));
+ String fullSchema = describeFullSchema();
+ assertTrue(fullSchema.contains(commentStatement));
+ assertTrue(fullSchema.contains(securitylabelStatement));
+ }
+
+ private void testUnsetCommentAndLabel(String describeCommand, String objectType, String objectName,
+ String commentStatement, String securitylabelStatement) throws Throwable
+ {
+ String unsetCommentStatement = String.format("COMMENT ON %s %s IS NULL", objectType, objectName);
+ String unsetSecurityLabelStatement = String.format("SECURITY LABEL ON %s %s IS NULL", objectType, objectName);
+ execute(unsetCommentStatement);
+ execute(unsetSecurityLabelStatement);
+
+ String schema = describeSchemaElement(describeCommand);
+ String fullSchema = describeFullSchema();
+ assertFalse(schema.contains(unsetSecurityLabelStatement));
+ assertFalse(schema.contains(unsetCommentStatement));
+ assertFalse(fullSchema.contains(commentStatement));
+ assertFalse(fullSchema.contains(securitylabelStatement));
+ }
+
+ private void testDropRemovesCommentAndLabel(String objectType, String objectName,
+ String commentStatement, String securitylabelStatement,
+ String dropStatement) throws Throwable
+ {
+ execute(commentStatement);
+ execute(securitylabelStatement);
+ execute(dropStatement);
+
+ String unsetCommentStatement = String.format("COMMENT ON %s %s IS NULL", objectType, objectName);
+ String unsetSecurityLabelStatement = String.format("SECURITY LABEL ON %s %s IS NULL", objectType, objectName);
+ String fullSchema = describeFullSchema();
+ assertFalse(fullSchema.contains(unsetSecurityLabelStatement));
+ assertFalse(fullSchema.contains(unsetCommentStatement));
+ assertFalse(fullSchema.contains(commentStatement));
+ assertFalse(fullSchema.contains(securitylabelStatement));
+ }
+
+ private String describeSchemaElement(String describeCommand) throws Throwable
+ {
+ return executeDescribeNet(describeCommand).one().getString("create_statement");
+ }
+
+ private String describeFullSchema() throws Throwable
+ {
+ ResultSet result = executeDescribeNet("DESCRIBE SCHEMA");
+ StringBuilder fullSchema = new StringBuilder();
+ for (Row row : result.all())
+ {
+ fullSchema.append(row.getString("create_statement")).append("\n");
+ }
+ return fullSchema.toString();
+ }
+
private static String allTypesTable()
{
return "CREATE TABLE test.has_all_types (\n" +
@@ -1134,6 +1304,11 @@ public class DescribeStatementTest extends CQLTester
}
private static String tableParametersCql()
+ {
+ return tableParametersCql("");
+ }
+
+ private static String tableParametersCql(String comment)
{
if (!DatabaseDescriptor.getAutoRepairConfig().isAutoRepairSchedulingEnabled())
{
@@ -1142,7 +1317,7 @@ public class DescribeStatementTest extends CQLTester
" AND bloom_filter_fp_chance = 0.01\n" +
" AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'}\n" +
" AND cdc = false\n" +
- " AND comment = ''\n" +
+ " AND comment = '" + comment + "'\n" +
" AND compaction = " + cqlQuoted(CompactionParams.DEFAULT.asMap()) + "\n" +
" AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'}\n" +
" AND memtable = 'default'\n" +
diff --git a/test/unit/org/apache/cassandra/cql3/validation/miscellaneous/CommentAndSecurityLabelTest.java b/test/unit/org/apache/cassandra/cql3/validation/miscellaneous/CommentAndSecurityLabelTest.java
new file mode 100644
index 0000000000..fe404afa9d
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/validation/miscellaneous/CommentAndSecurityLabelTest.java
@@ -0,0 +1,558 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.validation.miscellaneous;
+
+import org.junit.Test;
+
+import com.datastax.driver.core.ResultSet;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.cql3.FieldIdentifier;
+import org.apache.cassandra.db.marshal.UserType;
+import org.apache.cassandra.schema.ColumnMetadata;
+import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.schema.Schema;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.schema.TableParams;
+
+import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
+import static org.junit.Assert.assertEquals;
+
+public class CommentAndSecurityLabelTest extends CQLTester
+{
+ public static final String KEYSPACE_NAME = "ks_comment";
+ public static final String SECURITY_KEYSPACE = "ks_security";
+ public static final String TABLE_NAME = "tbl_comment";
+ public static final String SECURITY_TABLE_NAME = "tbl_security";
+ public static final String TYPE_NAME = "address";
+
+ // Test data constants
+ private static final String TEST_COMMENT = "Test comment";
+ private static final String UPDATED_COMMENT = "Updated comment";
+ private static final String TEST_LABEL = "TEST_LABEL";
+ private static final String UPDATED_LABEL = "UPDATED_LABEL";
+
+ enum ObjectType
+ {KEYSPACE, TABLE, COLUMN, TYPE, FIELD}
+
+ @Test
+ public void testCommentOnKeyspace()
+ {
+ createKeyspaceWithName(KEYSPACE_NAME);
+ testCommentLifecycle(ObjectType.KEYSPACE, KEYSPACE_NAME, KEYSPACE_NAME);
+ }
+
+ @Test
+ public void testSecurityLabelOnKeyspace()
+ {
+ createKeyspaceWithName(SECURITY_KEYSPACE);
+ testSecurityLabelLifecycle(ObjectType.KEYSPACE, SECURITY_KEYSPACE, SECURITY_KEYSPACE);
+
+ // Test provider warning
+ ResultSet result = executeNet(String.format("SECURITY LABEL FOR test_provider ON KEYSPACE %s IS 'SENSITIVE'", SECURITY_KEYSPACE));
+ assertWarningsContain(result.getExecutionInfo().getWarnings(), "Provider functionality not implemented.");
+ assertSecurityLabel(ObjectType.KEYSPACE, SECURITY_KEYSPACE, SECURITY_KEYSPACE, "SENSITIVE");
+ }
+
+ @Test
+ public void testCommentOnTable()
+ {
+ createKeyspaceWithName(KEYSPACE_NAME);
+ createTableWithName(KEYSPACE_NAME, TABLE_NAME);
+ String tableRef = String.format("%s.%s", KEYSPACE_NAME, TABLE_NAME);
+ testCommentLifecycle(ObjectType.TABLE, KEYSPACE_NAME, tableRef);
+ }
+
+ @Test
+ public void testSecurityLabelOnTable()
+ {
+ createKeyspaceWithName(SECURITY_KEYSPACE);
+ createTableWithName(SECURITY_KEYSPACE, SECURITY_TABLE_NAME);
+ String tableRef = String.format("%s.%s", SECURITY_KEYSPACE, SECURITY_TABLE_NAME);
+ testSecurityLabelLifecycle(ObjectType.TABLE, SECURITY_KEYSPACE, tableRef);
+
+ // Test provider warning
+ ResultSet result = executeNet(String.format("SECURITY LABEL FOR my_provider ON TABLE %s IS 'CONFIDENTIAL'", tableRef));
+ assertWarningsContain(result.getExecutionInfo().getWarnings(), "Provider functionality not implemented.");
+ assertSecurityLabel(ObjectType.TABLE, SECURITY_KEYSPACE, tableRef, "CONFIDENTIAL");
+ }
+
+ @Test
+ public void testCommentOnColumn()
+ {
+ createKeyspaceWithName(KEYSPACE_NAME);
+ createTableWithName(KEYSPACE_NAME, TABLE_NAME);
+ String columnRef = String.format("%s.%s.name", KEYSPACE_NAME, TABLE_NAME);
+ testCommentLifecycle(ObjectType.COLUMN, KEYSPACE_NAME, columnRef);
+ }
+
+ @Test
+ public void testSecurityLabelOnColumn()
+ {
+ createKeyspaceWithName(SECURITY_KEYSPACE);
+ String createTableQuery = String.format("CREATE TABLE %s.%s (id int PRIMARY KEY, ssn text, name text)", SECURITY_KEYSPACE, SECURITY_TABLE_NAME);
+ createTable(createTableQuery);
+ String columnRef = String.format("%s.%s.ssn", SECURITY_KEYSPACE, SECURITY_TABLE_NAME);
+ testSecurityLabelLifecycle(ObjectType.COLUMN, SECURITY_KEYSPACE, columnRef);
+
+ // Test provider warning
+ ResultSet result = executeNet(String.format("SECURITY LABEL FOR data_classifier ON COLUMN %s IS 'PII'", columnRef));
+ assertWarningsContain(result.getExecutionInfo().getWarnings(), "Provider functionality not implemented.");
+ assertSecurityLabel(ObjectType.COLUMN, SECURITY_KEYSPACE, columnRef, "PII");
+ }
+
+ @Test
+ public void testCommentOnType()
+ {
+ createKeyspaceWithName(KEYSPACE_NAME);
+ execute(String.format("CREATE TYPE %s.%s (street text, city text, zip int)", KEYSPACE_NAME, TYPE_NAME));
+ String typeRef = String.format("%s.%s", KEYSPACE_NAME, TYPE_NAME);
+ testCommentLifecycle(ObjectType.TYPE, KEYSPACE_NAME, typeRef);
+ }
+
+ @Test
+ public void testSecurityLabelOnType()
+ {
+ createKeyspaceWithName(SECURITY_KEYSPACE);
+ String typeName = "personal_info";
+ execute(String.format("CREATE TYPE %s.%s (ssn text, dob date)", SECURITY_KEYSPACE, typeName));
+ String typeRef = String.format("%s.%s", SECURITY_KEYSPACE, typeName);
+ testSecurityLabelLifecycle(ObjectType.TYPE, SECURITY_KEYSPACE, typeRef);
+
+ // Test provider warning
+ ResultSet result = executeNet(String.format("SECURITY LABEL FOR security_provider ON TYPE %s IS 'RESTRICTED'", typeRef));
+ assertWarningsContain(result.getExecutionInfo().getWarnings(), "Provider functionality not implemented.");
+ assertSecurityLabel(ObjectType.TYPE, SECURITY_KEYSPACE, typeRef, "RESTRICTED");
+ }
+
+ @Test
+ public void testCommentOnField()
+ {
+ createKeyspaceWithName(KEYSPACE_NAME);
+ execute(String.format("CREATE TYPE %s.geo_position (latitude double, longitude double, altitude double)", KEYSPACE_NAME));
+ String fieldRef = String.format("%s.geo_position.latitude", KEYSPACE_NAME);
+ testCommentLifecycle(ObjectType.FIELD, KEYSPACE_NAME, fieldRef);
+ }
+
+ @Test
+ public void testSecurityLabelOnField()
+ {
+ createKeyspaceWithName(SECURITY_KEYSPACE);
+ execute(String.format("CREATE TYPE %s.patient_record (ssn text, diagnosis text, treatment text)", SECURITY_KEYSPACE));
+ String fieldRef = String.format("%s.patient_record.ssn", SECURITY_KEYSPACE);
+ testSecurityLabelLifecycle(ObjectType.FIELD, SECURITY_KEYSPACE, fieldRef);
+
+ // Test provider warning
+ ResultSet result = executeNet(String.format("SECURITY LABEL FOR healthcare_provider ON FIELD %s IS 'PHI'", fieldRef));
+ assertWarningsContain(result.getExecutionInfo().getWarnings(), "Provider functionality not implemented.");
+ assertSecurityLabel(ObjectType.FIELD, SECURITY_KEYSPACE, fieldRef, "PHI");
+ }
+
+ @Test
+ public void testFieldWithUseKeyspace()
+ {
+ createKeyspaceWithName(KEYSPACE_NAME);
+ execute(String.format("CREATE TYPE %s.address (street text, city text, zip int)", KEYSPACE_NAME));
+ execute(String.format("USE %s", KEYSPACE_NAME));
+
+ // Test unqualified field reference with USE KEYSPACE context
+ setComment(ObjectType.FIELD, "address.street", "Street address");
+ setSecurityLabel(ObjectType.FIELD, "address.street", "PUBLIC");
+ setComment(ObjectType.FIELD, "address.city", "City name");
+ setSecurityLabel(ObjectType.FIELD, "address.city", "PUBLIC");
+
+ // Verify
+ assertComment(ObjectType.FIELD, KEYSPACE_NAME, "address.street", "Street address");
+ assertSecurityLabel(ObjectType.FIELD, KEYSPACE_NAME, "address.street", "PUBLIC");
+ assertComment(ObjectType.FIELD, KEYSPACE_NAME, "address.city", "City name");
+ assertSecurityLabel(ObjectType.FIELD, KEYSPACE_NAME, "address.city", "PUBLIC");
+ }
+
+ @Test
+ public void testErrorCases()
+ {
+ createKeyspaceWithName(KEYSPACE_NAME);
+ createTableWithName(KEYSPACE_NAME, TABLE_NAME);
+ execute(String.format("CREATE TYPE %s.test_type (field1 text, field2 int)", KEYSPACE_NAME));
+
+ // Test non-existent keyspace
+ String commentOnKeyspace = "COMMENT ON KEYSPACE nonexistent IS 'comment'";
+ assertInvalidMessage("Keyspace 'nonexistent' doesn't exist", commentOnKeyspace);
+
+ // Test non-existent table
+ String commentOnTableQuery = String.format("COMMENT ON TABLE %s.nonexistent IS 'comment'", KEYSPACE_NAME);
+ assertInvalidMessage(String.format("Table '%s.nonexistent' doesn't exist", KEYSPACE_NAME), commentOnTableQuery);
+
+ // Test non-existent column
+ String commentOnColumnQuery = String.format("COMMENT ON COLUMN %s.%s.nonexistent IS 'comment'", KEYSPACE_NAME, TABLE_NAME);
+ assertInvalidMessage("Column 'nonexistent' doesn't exist", commentOnColumnQuery);
+
+ // Test non-existent type
+ String commentOnType = String.format("COMMENT ON TYPE %s.nonexistent IS 'comment'", KEYSPACE_NAME);
+ assertInvalidMessage("Type", commentOnType);
+
+ // Test non-existent type for field
+ String commentOnNonExistentType = String.format("COMMENT ON FIELD %s.nonexistent.somefield IS 'comment'", KEYSPACE_NAME);
+ assertInvalidMessage("doesn't exist", commentOnNonExistentType);
+
+ // Test non-existent field
+ String commentOnNonExistentField = String.format("COMMENT ON FIELD %s.test_type.nonexistent IS 'comment'", KEYSPACE_NAME);
+ assertInvalidMessage("doesn't exist", commentOnNonExistentField);
+ }
+
+ @Test
+ public void testMultipleOperations()
+ {
+ createKeyspaceWithName(KEYSPACE_NAME);
+ createTableWithName(KEYSPACE_NAME, TABLE_NAME);
+ execute(String.format("CREATE TYPE %s.contact_info (phone text, address text)", KEYSPACE_NAME));
+
+ // Set comments and labels on multiple objects
+ String tableRef = String.format("%s.%s", KEYSPACE_NAME, TABLE_NAME);
+ String columnRef = String.format("%s.%s.name", KEYSPACE_NAME, TABLE_NAME);
+ String typeRef = String.format("%s.contact_info", KEYSPACE_NAME);
+
+ setComment(ObjectType.TABLE, tableRef, "User table");
+ setSecurityLabel(ObjectType.TABLE, tableRef, "USER_DATA");
+ setComment(ObjectType.COLUMN, columnRef, "User name");
+ setSecurityLabel(ObjectType.COLUMN, columnRef, "PUBLIC");
+ setComment(ObjectType.TYPE, typeRef, "Contact information");
+ setSecurityLabel(ObjectType.TYPE, typeRef, "PERSONAL");
+
+ // Verify all are set correctly
+ assertComment(ObjectType.TABLE, KEYSPACE_NAME, tableRef, "User table");
+ assertSecurityLabel(ObjectType.TABLE, KEYSPACE_NAME, tableRef, "USER_DATA");
+ assertComment(ObjectType.COLUMN, KEYSPACE_NAME, columnRef, "User name");
+ assertSecurityLabel(ObjectType.COLUMN, KEYSPACE_NAME, columnRef, "PUBLIC");
+ assertComment(ObjectType.TYPE, KEYSPACE_NAME, typeRef, "Contact information");
+ assertSecurityLabel(ObjectType.TYPE, KEYSPACE_NAME, typeRef, "PERSONAL");
+ }
+
+ @Test
+ public void testEmptyAndSpecialCharacters()
+ {
+ createKeyspaceWithName(KEYSPACE_NAME);
+ createTableWithName(KEYSPACE_NAME, TABLE_NAME);
+ String tableRef = String.format("%s.%s", KEYSPACE_NAME, TABLE_NAME);
+
+ // Test empty string - should be rejected
+ assertInvalidMessage("Cannot set comment to empty string", buildCommentStatement(ObjectType.TABLE, tableRef, ""));
+ assertInvalidMessage("Cannot set security label to empty string", buildSecurityLabelStatement(ObjectType.TABLE, tableRef, ""));
+
+ // Test special characters
+ String specialComment = "Comment with \"quotes\" and '' and \nnewlines";
+ setComment(ObjectType.TABLE, tableRef, specialComment);
+ assertComment(ObjectType.TABLE, KEYSPACE_NAME, tableRef, "Comment with \"quotes\" and '' and \nnewlines");
+
+ // Test Unicode characters
+ String unicodeComment = "Unicode comment: 测试 ñoño 🚀";
+ setComment(ObjectType.TABLE, tableRef, unicodeComment);
+ assertComment(ObjectType.TABLE, KEYSPACE_NAME, tableRef, unicodeComment);
+ }
+
+ @Test
+ public void testUseKeyspaceContext()
+ {
+ createKeyspaceWithName(KEYSPACE_NAME);
+ createTableWithName(KEYSPACE_NAME, TABLE_NAME);
+ execute(String.format("CREATE TYPE %s.test_type (field1 text, field2 int)", KEYSPACE_NAME));
+
+ // Use the keyspace to set current context
+ execute(String.format("USE %s", KEYSPACE_NAME));
+
+ // Test unqualified names with USE KEYSPACE context
+ setComment(ObjectType.TABLE, TABLE_NAME, "Table comment via USE");
+ setSecurityLabel(ObjectType.TABLE, TABLE_NAME, "TABLE_LABEL");
+ setComment(ObjectType.COLUMN, TABLE_NAME + ".name", "Column comment via USE");
+ setSecurityLabel(ObjectType.COLUMN, TABLE_NAME + ".name", "COLUMN_LABEL");
+ setComment(ObjectType.TYPE, "test_type", "Type comment via USE");
+ setSecurityLabel(ObjectType.TYPE, "test_type", "TYPE_LABEL");
+
+ // Verify all are set correctly using the current keyspace context
+ assertComment(ObjectType.TABLE, KEYSPACE_NAME, TABLE_NAME, "Table comment via USE");
+ assertSecurityLabel(ObjectType.TABLE, KEYSPACE_NAME, TABLE_NAME, "TABLE_LABEL");
+ assertComment(ObjectType.COLUMN, KEYSPACE_NAME, TABLE_NAME + ".name", "Column comment via USE");
+ assertSecurityLabel(ObjectType.COLUMN, KEYSPACE_NAME, TABLE_NAME + ".name", "COLUMN_LABEL");
+ assertComment(ObjectType.TYPE, KEYSPACE_NAME, "test_type", "Type comment via USE");
+ assertSecurityLabel(ObjectType.TYPE, KEYSPACE_NAME, "test_type", "TYPE_LABEL");
+ }
+
+
+ @Test
+ public void testCommentAndSecurityLabelOnVirtualTableFails()
+ {
+ assertInvalidMessage("is not user-modifiable", "COMMENT ON TABLE system_views.settings IS 'fail'");
+ assertInvalidMessage("is not user-modifiable", "SECURITY LABEL ON TABLE system_views.settings IS 'fail'");
+ }
+
+ @Test
+ public void testCommentAndSecurityLabelOnSystemTableFails()
+ {
+ assertInvalidMessage("is not user-modifiable", "COMMENT ON TABLE system.local IS 'fail'");
+ assertInvalidMessage("is not user-modifiable", "SECURITY LABEL ON TABLE system.local IS 'fail'");
+ assertInvalidMessage("is not user-modifiable", "COMMENT ON COLUMN system.local.key IS 'fail'");
+ assertInvalidMessage("is not user-modifiable", "SECURITY LABEL ON COLUMN system.local.key IS 'fail'");
+ }
+
+ @Test
+ public void testMaterializedViewFails() throws Throwable
+ {
+ String tableName = createTable("CREATE TABLE %s (pk int, ck int, v int, PRIMARY KEY (pk, ck))");
+ String mvFullName = KEYSPACE + ".test_mv";
+
+ // Create view using executeNet with fully qualified names (like GrantAndRevokeTest does)
+ executeNet("CREATE MATERIALIZED VIEW " + mvFullName + " AS SELECT * FROM " + KEYSPACE + "." + tableName +
+ " WHERE pk IS NOT NULL AND ck IS NOT NULL PRIMARY KEY (ck, pk)");
+
+ // Wait for the view to be fully built before testing
+ waitForViewBuild("test_mv");
+
+ // Test that comment and security label statements fail
+ assertInvalidMessage("Cannot set comment on non-regular table",
+ "COMMENT ON TABLE " + mvFullName + " IS 'fail'");
+ assertInvalidMessage("Cannot set security label on non-regular table",
+ "SECURITY LABEL ON TABLE " + mvFullName + " IS 'fail'");
+ }
+
+ @Test
+ public void testCommentAndSecurityLabelOnSystemKeyspaceFails()
+ {
+ // Test comment and security label on system keyspaces
+ assertInvalidMessage("is not user-modifiable", "COMMENT ON KEYSPACE system IS 'fail'");
+ assertInvalidMessage("is not user-modifiable", "SECURITY LABEL ON KEYSPACE system IS 'fail'");
+ }
+
+ @Test
+ public void testCommentAndSecurityLabelOnVirtualKeyspaceFails()
+ {
+ // Test comment and security label on virtual keyspaces
+ assertInvalidMessage("is not user-modifiable", "COMMENT ON KEYSPACE system_views IS 'fail'");
+ assertInvalidMessage("is not user-modifiable", "SECURITY LABEL ON KEYSPACE system_views IS 'fail'");
+ }
+
+ @Test
+ public void testEmptyStringRejection()
+ {
+ createKeyspaceWithName(KEYSPACE_NAME);
+ createTableWithName(KEYSPACE_NAME, TABLE_NAME);
+ execute(String.format("CREATE TYPE %s.test_type (field1 text, field2 int)", KEYSPACE_NAME));
+
+ String tableRef = String.format("%s.%s", KEYSPACE_NAME, TABLE_NAME);
+ String columnRef = String.format("%s.%s.name", KEYSPACE_NAME, TABLE_NAME);
+ String typeRef = String.format("%s.test_type", KEYSPACE_NAME);
+ String fieldRef = String.format("%s.test_type.field1", KEYSPACE_NAME);
+
+ // Test that empty strings are rejected for comments on all schema elements
+ assertInvalidMessage("Cannot set comment to empty string", buildCommentStatement(ObjectType.KEYSPACE, KEYSPACE_NAME, ""));
+ assertInvalidMessage("Cannot set comment to empty string", buildCommentStatement(ObjectType.TABLE, tableRef, ""));
+ assertInvalidMessage("Cannot set comment to empty string", buildCommentStatement(ObjectType.COLUMN, columnRef, ""));
+ assertInvalidMessage("Cannot set comment to empty string", buildCommentStatement(ObjectType.TYPE, typeRef, ""));
+ assertInvalidMessage("Cannot set comment to empty string", buildCommentStatement(ObjectType.FIELD, fieldRef, ""));
+
+ // Test that empty strings are rejected for security labels on all schema elements
+ assertInvalidMessage("Cannot set security label to empty string", buildSecurityLabelStatement(ObjectType.KEYSPACE, KEYSPACE_NAME, ""));
+ assertInvalidMessage("Cannot set security label to empty string", buildSecurityLabelStatement(ObjectType.TABLE, tableRef, ""));
+ assertInvalidMessage("Cannot set security label to empty string", buildSecurityLabelStatement(ObjectType.COLUMN, columnRef, ""));
+ assertInvalidMessage("Cannot set security label to empty string", buildSecurityLabelStatement(ObjectType.TYPE, typeRef, ""));
+ assertInvalidMessage("Cannot set security label to empty string", buildSecurityLabelStatement(ObjectType.FIELD, fieldRef, ""));
+ }
+
+ // Helper methods for setting comments and security labels
+ private void setComment(ObjectType type, String objectName, String comment)
+ {
+ String statement = buildCommentStatement(type, objectName, comment);
+ execute(statement);
+ }
+
+ private void setSecurityLabel(ObjectType type, String objectName, String label)
+ {
+ String statement = buildSecurityLabelStatement(type, objectName, label);
+ execute(statement);
+ }
+
+ private String buildStatement(String statementType, ObjectType type, String objectName, String value)
+ {
+ String valueClause = value != null ? String.format("'%s'", value.replace("'", "''")) : "NULL";
+ String typeKeyword = type.name();
+ return String.format("%s ON %s %s IS %s", statementType, typeKeyword, objectName, valueClause);
+ }
+
+ private String buildCommentStatement(ObjectType type, String objectName, String comment)
+ {
+ return buildStatement("COMMENT", type, objectName, comment);
+ }
+
+ private String buildSecurityLabelStatement(ObjectType type, String objectName, String label)
+ {
+ return buildStatement("SECURITY LABEL", type, objectName, label);
+ }
+
+ // Helper methods for assertions
+ private void assertComment(ObjectType type, String keyspace, String objectName, String expected)
+ {
+ String actual = getComment(type, keyspace, objectName);
+ assertEquals(expected, actual);
+ }
+
+ private void assertSecurityLabel(ObjectType type, String keyspace, String objectName, String expected)
+ {
+ String actual = getSecurityLabel(type, keyspace, objectName);
+ assertEquals(expected, actual);
+ }
+
+ private String extractObjectName(ObjectType type, String objectName)
+ {
+ if (type == ObjectType.TABLE || type == ObjectType.TYPE)
+ {
+ return objectName.contains(".") ? objectName.split("\\.")[1] : objectName;
+ }
+ return objectName;
+ }
+
+ private String[] parseColumnReference(String objectName)
+ {
+ String[] parts = objectName.split("\\.");
+ if (parts.length == 2)
+ {
+ return new String[]{ parts[0], parts[1] }; // table/type, column/field
+ }
+ else if (parts.length == 3)
+ {
+ return new String[]{ parts[1], parts[2] }; // table/type, column/field (ignore keyspace part)
+ }
+ else
+ {
+ throw new IllegalArgumentException("Invalid reference format: " + objectName);
+ }
+ }
+
+ private String getMetadataValue(ObjectType type, String keyspace, String objectName, boolean isComment)
+ {
+ switch (type)
+ {
+ case KEYSPACE:
+ KeyspaceParams keyspaceMetadata = Schema.instance.getKeyspaceMetadata(keyspace).params;
+ return isComment ? keyspaceMetadata.comment : keyspaceMetadata.securityLabel;
+ case TABLE:
+ String tableName = extractObjectName(type, objectName);
+ TableParams tableParams = retrieveTableMetadata(keyspace, tableName).params;
+ return isComment ? tableParams.comment : tableParams.securityLabel;
+ case COLUMN:
+ String[] columnParts = parseColumnReference(objectName);
+ ColumnMetadata columnMetadata = getColumnMetadata(keyspace, columnParts[0], columnParts[1]);
+ return isComment ? columnMetadata.comment : columnMetadata.securityLabel;
+ case TYPE:
+ String typeName = extractObjectName(type, objectName);
+ UserType userType = getUserType(keyspace, typeName);
+ return isComment ? userType.comment : userType.securityLabel;
+ case FIELD:
+ String[] fieldParts = parseColumnReference(objectName);
+ UserType type1 = getUserType(keyspace, fieldParts[0]);
+ FieldIdentifier fieldId = FieldIdentifier.forUnquoted(fieldParts[1]);
+ return isComment ? type1.fieldComment(fieldId) : type1.fieldSecurityLabel(fieldId);
+ default:
+ throw new IllegalArgumentException("Unsupported object type: " + type);
+ }
+ }
+
+ private String getComment(ObjectType type, String keyspace, String objectName)
+ {
+ return getMetadataValue(type, keyspace, objectName, true);
+ }
+
+ private String getSecurityLabel(ObjectType type, String keyspace, String objectName)
+ {
+ return getMetadataValue(type, keyspace, objectName, false);
+ }
+
+ // Metadata retrieval helpers
+ private TableMetadata retrieveTableMetadata(String keyspace, String table)
+ {
+ return getColumnFamilyStore(keyspace, table).metadata();
+ }
+
+ private ColumnMetadata getColumnMetadata(String keyspace, String table, String column)
+ {
+ return retrieveTableMetadata(keyspace, table).getColumn(ColumnIdentifier.getInterned(column, false));
+ }
+
+ private UserType getUserType(String keyspace, String typeName)
+ {
+ return Schema.instance.getKeyspaceMetadata(keyspace).types.get(bytes(typeName)).get();
+ }
+
+ // Generic lifecycle test method
+ private void testMetadataLifecycle(ObjectType type, String keyspace, String objectName, boolean isComment)
+ {
+ String testValue = isComment ? TEST_COMMENT : TEST_LABEL;
+ String updatedValue = isComment ? UPDATED_COMMENT : UPDATED_LABEL;
+ if (isComment)
+ {
+ String emptyStringStatement = buildCommentStatement(type, objectName, "");
+ assertInvalidMessage("Cannot set comment to empty string", emptyStringStatement);
+ setComment(type, objectName, testValue);
+ assertComment(type, keyspace, objectName, testValue);
+ setComment(type, objectName, updatedValue);
+ assertComment(type, keyspace, objectName, updatedValue);
+ setComment(type, objectName, null);
+ assertComment(type, keyspace, objectName, "");
+ setComment(type, objectName, null);
+ assertComment(type, keyspace, objectName, "");
+ String longComment = buildCommentStatement(type, objectName, "a".repeat(129));
+ assertInvalidMessage("comment length (129) exceeds maximum allowed length (128)", longComment);
+ }
+ else
+ {
+ String emptyStringStatement = buildSecurityLabelStatement(type, objectName, "");
+ assertInvalidMessage("Cannot set security label to empty string", emptyStringStatement);
+ setSecurityLabel(type, objectName, testValue);
+ assertSecurityLabel(type, keyspace, objectName, testValue);
+ setSecurityLabel(type, objectName, updatedValue);
+ assertSecurityLabel(type, keyspace, objectName, updatedValue);
+ setSecurityLabel(type, objectName, null);
+ assertSecurityLabel(type, keyspace, objectName, "");
+ setSecurityLabel(type, objectName, null);
+ assertSecurityLabel(type, keyspace, objectName, "");
+ String longSecurityLabel = buildSecurityLabelStatement(type, objectName, "a".repeat(49));
+ assertInvalidMessage("security label length (49) exceeds maximum allowed length (48)", longSecurityLabel);
+ }
+ }
+
+ private void testCommentLifecycle(ObjectType type, String keyspace, String objectName)
+ {
+ testMetadataLifecycle(type, keyspace, objectName, true);
+ }
+
+ private void testSecurityLabelLifecycle(ObjectType type, String keyspace, String objectName)
+ {
+ testMetadataLifecycle(type, keyspace, objectName, false);
+ }
+
+ private void createKeyspaceWithName(String keyspace)
+ {
+ String query = String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}", keyspace);
+ createKeyspace(query);
+ }
+
+ private void createTableWithName(String keyspace, String taleName)
+ {
+ String query = String.format("CREATE TABLE %s.%s (id int PRIMARY KEY, name text)", keyspace, taleName);
+ createTable(query);
+ }
+}
diff --git a/test/unit/org/apache/cassandra/db/virtual/SchemaMetadataTableTest.java b/test/unit/org/apache/cassandra/db/virtual/SchemaMetadataTableTest.java
new file mode 100644
index 0000000000..b16c4078fb
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/virtual/SchemaMetadataTableTest.java
@@ -0,0 +1,336 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.db.virtual;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+/**
+ * Parameterized tests for schema metadata virtual tables.
+ * <p>
+ * This test class runs the same test suite for both {@link SchemaCommentsTable}
+ * and {@link SchemaSecurityLabelsTable}, ensuring consistent behavior across both tables.
+ * </p>
+ */
+@RunWith(Parameterized.class)
+public class SchemaMetadataTableTest extends CQLTester
+{
+ private static final String KS_NAME = "vts";
+ private static final String KS1 = "ks_metadata_test1";
+ private static final String KS2 = "ks_metadata_test2";
+ private static final String TABLE1 = "test_table";
+ private static final String TABLE2 = "user_data";
+
+ private static final String REPLICATION = "WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}";
+
+ @Parameterized.Parameters(name = "{0}")
+ public static Collection<Object[]> data()
+ {
+ return Arrays.asList(new Object[][]{
+ {"COMMENT", "schema_comments", "comment", "COMMENT ON", "IS"},
+ {"SECURITY_LABEL", "schema_security_labels", "security_label", "SECURITY LABEL ON", "IS"}
+ });
+ }
+
+ @Parameterized.Parameter(0)
+ public String metadataType;
+
+ @Parameterized.Parameter(1)
+ public String tableName;
+
+ @Parameterized.Parameter(2)
+ public String columnName;
+
+ @Parameterized.Parameter(3)
+ public String setStatementPrefix;
+
+ @Parameterized.Parameter(4)
+ public String setStatementSuffix;
+
+ @Before
+ public void setup()
+ {
+ if (metadataType.equals("COMMENT"))
+ {
+ SchemaCommentsTable table = new SchemaCommentsTable(KS_NAME);
+ VirtualKeyspaceRegistry.instance.register(new VirtualKeyspace(KS_NAME, ImmutableList.of(table)));
+ }
+ else
+ {
+ SchemaSecurityLabelsTable table = new SchemaSecurityLabelsTable(KS_NAME);
+ VirtualKeyspaceRegistry.instance.register(new VirtualKeyspace(KS_NAME, ImmutableList.of(table)));
+ }
+ }
+
+ @After
+ public void tearDown()
+ {
+ execute("DROP KEYSPACE IF EXISTS " + KS1);
+ execute("DROP KEYSPACE IF EXISTS " + KS2);
+ execute("DROP KEYSPACE IF EXISTS ks_no_metadata");
+ }
+
+ @Test
+ public void testEmptyResults() throws Throwable
+ {
+ String ks = "ks_no_metadata";
+ createTestKeyspace(ks);
+ execute("CREATE TABLE " + ks + ".test (id int PRIMARY KEY, value text)");
+ assertEmpty(execute("SELECT * FROM vts." + tableName + " WHERE keyspace_name = ?", ks));
+ }
+
+ @Test
+ public void testKeyspaceMetadata() throws Throwable
+ {
+ createTestKeyspace(KS1);
+
+ setMetadata("KEYSPACE", KS1, "Test keyspace metadata");
+ assertMetadata("Test keyspace metadata");
+
+ setMetadata("KEYSPACE", KS1, "Updated keyspace metadata");
+ assertMetadata("Updated keyspace metadata");
+
+ setMetadata("KEYSPACE", KS1, null);
+ assertEmpty(querySchemaMetadata("KEYSPACE"));
+ }
+
+ @Test
+ public void testTableMetadata()
+ {
+ createTestKeyspace(KS1);
+ execute("CREATE TABLE " + KS1 + '.' + TABLE1 + " (id int PRIMARY KEY, name text)");
+
+ setMetadata("TABLE", KS1 + '.' + TABLE1, "User information table");
+ assertTableMetadata(KS1, TABLE1, "User information table");
+
+ setMetadata("TABLE", KS1 + '.' + TABLE1, "Updated table metadata");
+ assertTableMetadata(KS1, TABLE1, "Updated table metadata");
+ }
+
+ @Test
+ public void testColumnMetadata()
+ {
+ createTestKeyspace(KS1);
+ execute("CREATE TABLE " + KS1 + '.' + TABLE1 + " (id int PRIMARY KEY, name text, email text)");
+
+ setMetadata("COLUMN", KS1 + '.' + TABLE1 + ".name", "User full name");
+ setMetadata("COLUMN", KS1 + '.' + TABLE1 + ".email", "User email address");
+
+ assertColumnMetadata("name", "User full name");
+ assertColumnMetadata("email", "User email address");
+
+ setMetadata("COLUMN", KS1 + '.' + TABLE1 + ".name", "Updated name metadata");
+ assertColumnMetadata("name", "Updated name metadata");
+ }
+
+ @Test
+ public void testUdtMetadata()
+ {
+ createTestKeyspace(KS1);
+
+ execute("CREATE TYPE " + KS1 + ".address (street text, city text, zip int)");
+ setMetadata("TYPE", KS1 + ".address", "User address information");
+ assertUdtMetadata("User address information");
+
+ setMetadata("TYPE", KS1 + ".address", "Postal address");
+ assertUdtMetadata("Postal address");
+ }
+
+ @Test
+ public void testUdtFieldMetadata()
+ {
+ createTestKeyspace(KS1);
+
+ execute("CREATE TYPE " + KS1 + ".address (street text, city text, zip int)");
+ setMetadata("FIELD", KS1 + ".address.street", "Street address");
+ setMetadata("FIELD", KS1 + ".address.city", "City name");
+ setMetadata("FIELD", KS1 + ".address.zip", "Postal code");
+
+ assertFieldMetadata("street", "Street address");
+ assertFieldMetadata("city", "City name");
+ assertFieldMetadata("zip", "Postal code");
+
+ setMetadata("FIELD", KS1 + ".address.street", "Updated street metadata");
+ assertFieldMetadata("street", "Updated street metadata");
+ }
+
+ @Test
+ public void testPartitionKeyFiltering()
+ {
+ createTestKeyspace(KS1);
+ createTestKeyspace(KS2);
+
+ execute("CREATE TABLE " + KS1 + '.' + TABLE1 + " (id int PRIMARY KEY)");
+ execute("CREATE TABLE " + KS2 + '.' + TABLE2 + " (id int PRIMARY KEY)");
+
+ setMetadata("KEYSPACE", KS1, "First keyspace");
+ setMetadata("KEYSPACE", KS2, "Second keyspace");
+ setMetadata("TABLE", KS1 + '.' + TABLE1, "Table in KS1");
+ setMetadata("TABLE", KS2 + '.' + TABLE2, "Table in KS2");
+
+ assertMetadata("First keyspace");
+ assertTableMetadata(KS1, TABLE1, "Table in KS1");
+ assertTableMetadata(KS2, TABLE2, "Table in KS2");
+ }
+
+ @Test
+ public void testMetadataRemoval() throws Throwable
+ {
+ createTestKeyspace(KS1);
+
+ setMetadata("KEYSPACE", KS1, "Test metadata");
+ assertRowCount(querySchemaMetadata("KEYSPACE"), 1);
+
+ setMetadata("KEYSPACE", KS1, null);
+ assertEmpty(querySchemaMetadata("KEYSPACE"));
+ }
+
+ @Test
+ public void testMultipleSchemaElements()
+ {
+ createTestKeyspace(KS1);
+ execute("CREATE TABLE " + KS1 + ".users (id int PRIMARY KEY, name text, email text)");
+ execute("CREATE TABLE " + KS1 + ".posts (id int PRIMARY KEY, title text)");
+ execute("CREATE TYPE " + KS1 + ".address (street text, city text)");
+
+ setMetadata("KEYSPACE", KS1, "Application database");
+ setMetadata("TABLE", KS1 + ".users", "User accounts");
+ setMetadata("TABLE", KS1 + ".posts", "Blog posts");
+ setMetadata("COLUMN", KS1 + ".users.name", "Full name");
+ setMetadata("COLUMN", KS1 + ".users.email", "Email address");
+ setMetadata("TYPE", KS1 + ".address", "Postal address");
+
+ assertRowCount(execute("SELECT * FROM vts." + tableName + " WHERE keyspace_name = ? ALLOW FILTERING", KS1), 6);
+
+ assertRowCount(querySchemaMetadata("KEYSPACE"), 1);
+ assertRowCount(querySchemaMetadata("TABLE"), 2);
+ assertRowCount(execute("SELECT * FROM vts." + tableName + " WHERE object_type = 'COLUMN' AND keyspace_name = ? ALLOW FILTERING", KS1), 2);
+ assertRowCount(querySchemaMetadata("UDT"), 1);
+ }
+
+ @Test
+ public void testScanAllMetadata()
+ {
+ createTestKeyspace(KS1);
+ execute("CREATE TABLE " + KS1 + '.' + TABLE1 + " (id int PRIMARY KEY)");
+
+ setMetadata("KEYSPACE", KS1, "Test");
+ setMetadata("TABLE", KS1 + '.' + TABLE1, "Test table");
+
+ Object[][] results = getRows(execute("SELECT * FROM vts." + tableName));
+
+ boolean foundKeyspace = false;
+ boolean foundTable = false;
+
+ for (Object[] row : results)
+ {
+ if (row[1].equals(KS1))
+ {
+ if (row[0].equals("KEYSPACE"))
+ foundKeyspace = true;
+ else if (row[0].equals("TABLE"))
+ foundTable = true;
+ }
+ }
+
+ assert foundKeyspace : "Keyspace metadata not found in full scan";
+ assert foundTable : "Table metadata not found in full scan";
+ }
+
+ private void createTestKeyspace(String keyspaceName)
+ {
+ createKeyspace("CREATE KEYSPACE " + keyspaceName + ' ' + REPLICATION);
+ }
+
+ private UntypedResultSet querySchemaMetadata(String objectType)
+ {
+ return execute("SELECT * FROM vts." + tableName + " WHERE object_type = ? AND keyspace_name = ?",
+ objectType, KS1);
+ }
+
+ private void setMetadata(String objectType, String objectName, String metadata)
+ {
+ String value = metadata != null ? '\'' + metadata.replace("'", "''") + '\'' : "NULL";
+ execute(setStatementPrefix + ' ' + objectType + ' ' + objectName + ' ' + setStatementSuffix + ' ' + value);
+ }
+
+ private void assertMetadata(String expectedMetadata)
+ {
+ assertRows(execute("SELECT " + columnName + " FROM vts." + tableName + " WHERE object_type = ? AND keyspace_name = ?",
+ "KEYSPACE", KS1),
+ row(expectedMetadata));
+ }
+
+ private void assertTableMetadata(String keyspaceName, String table, String expectedMetadata)
+ {
+ assertRows(execute("SELECT " + columnName + " FROM vts." + tableName + " WHERE object_type = 'TABLE' AND keyspace_name = ? AND table_name = ?",
+ keyspaceName, table),
+ row(expectedMetadata));
+ }
+
+ private void assertColumnMetadata(String column, String expectedMetadata)
+ {
+ assertRows(execute("SELECT " + columnName + " FROM vts." + tableName + " WHERE object_type = 'COLUMN' AND keyspace_name = ? AND table_name = ? AND column_name = ? ALLOW FILTERING",
+ KS1, TABLE1, column),
+ row(expectedMetadata));
+ }
+
+ private void assertUdtMetadata(String expectedMetadata)
+ {
+ assertRows(execute("SELECT " + columnName + " FROM vts." + tableName + " WHERE object_type = 'UDT' AND keyspace_name = ? AND udt_name = ?",
+ KS1, "address"),
+ row(expectedMetadata));
+ }
+
+ private void assertFieldMetadata(String field, String expectedMetadata)
+ {
+ assertRows(execute("SELECT " + columnName + " FROM vts." + tableName + " WHERE object_type = 'FIELD' AND keyspace_name = ? AND udt_name = ? AND field_name = ? ALLOW FILTERING",
+ KS1, "address", field),
+ row(expectedMetadata));
+ }
+
+ @Test
+ public void testInvalidKeyspaceName() throws Throwable
+ {
+ String invalidKeyspace = "nonexistent_keyspace";
+ assertInvalidThrowMessage("Unknown keyspace: '" + invalidKeyspace + "'",
+ InvalidRequestException.class,
+ "SELECT * FROM vts." + tableName + " WHERE object_type = 'KEYSPACE' AND keyspace_name = '" + invalidKeyspace + "'");
+ }
+
+ @Test
+ public void testInvalidObjectType() throws Throwable
+ {
+ createTestKeyspace(KS1);
+ String invalidObjectType = "INVALID_TYPE";
+ assertInvalidThrowMessage("Unknown object type: '" + invalidObjectType + "'. Valid types are: [KEYSPACE, TABLE, COLUMN, UDT, FIELD]",
+ InvalidRequestException.class,
+ "SELECT * FROM vts." + tableName + " WHERE object_type = '" + invalidObjectType + "' AND keyspace_name = '" + KS1 + "'");
+ }
+}
diff --git a/test/unit/org/apache/cassandra/schema/MockSchema.java b/test/unit/org/apache/cassandra/schema/MockSchema.java
index 8ca112ed18..a348a7dff3 100644
--- a/test/unit/org/apache/cassandra/schema/MockSchema.java
+++ b/test/unit/org/apache/cassandra/schema/MockSchema.java
@@ -324,7 +324,11 @@ public class MockSchema
? mockKS.tables.with(metadata)
: mockKS.tables.withSwapped(metadata);
mockKS = mockKS.withSwapped(tables);
- return new ColumnFamilyStore(new Keyspace(mockKS), metadata.name, Util.newSeqGen(), metadata, new Directories(metadata), false, false);
+ // Use Schema.instance to get or create the keyspace instance to avoid creating duplicate metrics
+ Keyspace keyspace = Schema.instance.getKeyspaceInstance(metadata.keyspace);
+ if (keyspace == null)
+ keyspace = new Keyspace(mockKS);
+ return new ColumnFamilyStore(keyspace, metadata.name, Util.newSeqGen(), metadata, new Directories(metadata), false, false);
}
private static TableMetadata newTableMetadata(String ksname)
@@ -381,6 +385,7 @@ public class MockSchema
public static class MockSchemaProvider implements SchemaProvider
{
private final SchemaProvider originalSchemaProvider = Schema.instance;
+ private volatile Keyspace mockKeyspaceInstance;
@Override
public Set<String> getKeyspaces()
@@ -464,7 +469,18 @@ public class MockSchema
public Keyspace getKeyspaceInstance(String keyspaceName)
{
if (isMockKS(keyspaceName))
- return new Keyspace(mockKS);
+ {
+ // Cache the keyspace instance to avoid creating duplicate metrics
+ if (mockKeyspaceInstance == null)
+ {
+ synchronized (this)
+ {
+ if (mockKeyspaceInstance == null)
+ mockKeyspaceInstance = new Keyspace(mockKS);
+ }
+ }
+ return mockKeyspaceInstance;
+ }
return originalSchemaProvider.getKeyspaceInstance(keyspaceName);
}
diff --git a/test/unit/org/apache/cassandra/schema/SchemaMetadataSerializationTest.java b/test/unit/org/apache/cassandra/schema/SchemaMetadataSerializationTest.java
new file mode 100644
index 0000000000..297b047e4f
--- /dev/null
+++ b/test/unit/org/apache/cassandra/schema/SchemaMetadataSerializationTest.java
@@ -0,0 +1,290 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.schema;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Test;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.cql3.FieldIdentifier;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.db.marshal.UserType;
+import org.apache.cassandra.io.util.DataInputBuffer;
+import org.apache.cassandra.io.util.DataOutputBuffer;
+import org.apache.cassandra.tcm.serialization.Version;
+
+import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * Tests serialization and deserialization of schema metadata (keyspace, table, type, field)
+ * to verify that comments and security labels persist correctly across serialization boundaries.
+ */
+public class SchemaMetadataSerializationTest
+{
+ static
+ {
+ DatabaseDescriptor.daemonInitialization();
+ }
+
+ private static final String KEYSPACE = "test_ks";
+
+ @Test
+ public void testKeyspaceMetadataSerialization() throws IOException
+ {
+ KeyspaceMetadata original = KeyspaceMetadata.create(KEYSPACE,
+ KeyspaceParams.simple(1)
+ .withComment("Test keyspace")
+ .withSecurityLabel("TEST"));
+
+ KeyspaceMetadata deserialized = serializeAndDeserializeKeyspace(original);
+
+ assertNotNull("Deserialized keyspace should not be null", deserialized);
+ assertEquals("Test keyspace", deserialized.params.comment);
+ assertEquals("TEST", deserialized.params.securityLabel);
+ }
+
+ @Test
+ public void testTableMetadataSerialization() throws IOException
+ {
+ TableMetadata original = createSimpleTable("users", "Users table", "SENSITIVE");
+ TableMetadata deserialized = serializeAndDeserializeTable(original);
+
+ assertNotNull("Deserialized table should not be null", deserialized);
+ assertEquals("Users table", deserialized.params.comment);
+ assertEquals("SENSITIVE", deserialized.params.securityLabel);
+ }
+
+ @Test
+ public void testColumnMetadataSerialization() throws IOException
+ {
+ ColumnIdentifier emailCol = ColumnIdentifier.getInterned("email", false);
+ TableMetadata original = TableMetadata.builder(KEYSPACE, "users")
+ .addPartitionKeyColumn("id", Int32Type.instance)
+ .addRegularColumn(emailCol, UTF8Type.instance)
+ .alterColumnComment(emailCol, "User email")
+ .alterColumnSecurityLabel(emailCol, "PII-EMAIL")
+ .build();
+
+ TableMetadata deserialized = serializeAndDeserializeTable(original);
+
+ ColumnMetadata emailColumn = deserialized.getColumn(bytes("email"));
+ assertNotNull("Email column should exist", emailColumn);
+ assertEquals("User email", emailColumn.comment);
+ assertEquals("PII-EMAIL", emailColumn.securityLabel);
+ }
+
+ @Test
+ public void testUserTypeSerialization() throws IOException
+ {
+ UserType original = createUserType("address", "Address type", "PUBLIC");
+ UserType deserialized = serializeAndDeserializeType(original);
+
+ assertNotNull("Deserialized type should not be null", deserialized);
+ assertEquals("Address type", deserialized.comment);
+ assertEquals("PUBLIC", deserialized.securityLabel);
+ }
+
+ @Test
+ public void testTypeFieldSerialization() throws IOException
+ {
+ FieldIdentifier streetField = FieldIdentifier.forUnquoted("street");
+ FieldIdentifier cityField = FieldIdentifier.forUnquoted("city");
+
+ Map<FieldIdentifier, String> fieldComments = new HashMap<>();
+ fieldComments.put(streetField, "Street address");
+ fieldComments.put(cityField, "City name");
+
+ Map<FieldIdentifier, String> fieldLabels = new HashMap<>();
+ fieldLabels.put(streetField, "PII");
+ fieldLabels.put(cityField, "PUBLIC");
+
+ UserType original = new UserType(KEYSPACE,
+ bytes("address"),
+ Arrays.asList(streetField, cityField),
+ Arrays.asList(UTF8Type.instance, UTF8Type.instance),
+ true,
+ "Address type",
+ "PUBLIC",
+ fieldComments,
+ fieldLabels);
+
+ UserType deserialized = serializeAndDeserializeType(original);
+
+ assertNotNull("Deserialized type should not be null", deserialized);
+ assertEquals("Street address", deserialized.fieldComment(streetField));
+ assertEquals("City name", deserialized.fieldComment(cityField));
+ assertEquals("PII", deserialized.fieldSecurityLabel(streetField));
+ assertEquals("PUBLIC", deserialized.fieldSecurityLabel(cityField));
+ }
+
+ @Test
+ public void testTypesSerialization() throws IOException
+ {
+ UserType type1 = createUserType("type1", "Type 1 comment", "Type 1 label");
+ UserType type2 = createUserType("type2", "Type 2 comment", "Type 2 label");
+ Types original = Types.of(type1, type2);
+
+ Types deserialized = serializeAndDeserializeTypes(original);
+
+ UserType deserializedType1 = deserialized.getNullable(bytes("type1"));
+ UserType deserializedType2 = deserialized.getNullable(bytes("type2"));
+
+ assertNotNull("Type1 should exist", deserializedType1);
+ assertNotNull("Type2 should exist", deserializedType2);
+ assertEquals("Type 1 comment", deserializedType1.comment);
+ assertEquals("Type 1 label", deserializedType1.securityLabel);
+ assertEquals("Type 2 comment", deserializedType2.comment);
+ assertEquals("Type 2 label", deserializedType2.securityLabel);
+ }
+
+ @Test
+ public void testCompleteKeyspaceMetadataSerialization() throws IOException
+ {
+ UserType addressType = createUserType("address", "Address type", "PUBLIC");
+
+ ColumnIdentifier nameCol = ColumnIdentifier.getInterned("name", false);
+ TableMetadata table = TableMetadata.builder(KEYSPACE, "users")
+ .addPartitionKeyColumn("id", Int32Type.instance)
+ .addRegularColumn(nameCol, UTF8Type.instance)
+ .alterColumnComment(nameCol, "User name")
+ .alterColumnSecurityLabel(nameCol, "PUBLIC")
+ .comment("Users table")
+ .securityLabel("SENSITIVE")
+ .build();
+
+ KeyspaceMetadata original = KeyspaceMetadata.create(KEYSPACE,
+ KeyspaceParams.simple(1)
+ .withComment("Test keyspace")
+ .withSecurityLabel("TEST"),
+ Tables.of(table),
+ Views.none(),
+ Types.of(addressType),
+ UserFunctions.none());
+
+ KeyspaceMetadata deserialized = serializeAndDeserializeKeyspace(original);
+
+ // Verify keyspace metadata
+ assertEquals("Test keyspace", deserialized.params.comment);
+ assertEquals("TEST", deserialized.params.securityLabel);
+
+ // Verify table metadata
+ TableMetadata deserializedTable = deserialized.tables.getNullable("users");
+ assertNotNull("Table should exist", deserializedTable);
+ assertEquals("Users table", deserializedTable.params.comment);
+ assertEquals("SENSITIVE", deserializedTable.params.securityLabel);
+
+ // Verify column metadata
+ ColumnMetadata nameColumn = deserializedTable.getColumn(bytes("name"));
+ assertNotNull("Name column should exist", nameColumn);
+ assertEquals("User name", nameColumn.comment);
+ assertEquals("PUBLIC", nameColumn.securityLabel);
+
+ // Verify type metadata
+ UserType deserializedType = deserialized.types.getNullable(bytes("address"));
+ assertNotNull("Type should exist", deserializedType);
+ assertEquals("Address type", deserializedType.comment);
+ assertEquals("PUBLIC", deserializedType.securityLabel);
+ }
+
+ @Test
+ public void testBackwardCompatibilityV7() throws IOException
+ {
+ UserType type = createUserType("test_type", "Test comment", "Test label");
+
+ // Serialize with V7 (should not include metadata)
+ DataOutputBuffer out = new DataOutputBuffer();
+ Types.serializer.serialize(Types.of(type), out, Version.V7);
+
+ // Deserialize with V7
+ DataInputBuffer in = new DataInputBuffer(out.toByteArray());
+ Types deserializedTypes = Types.serializer.deserialize(KEYSPACE, in, Version.V7);
+
+ // Verify metadata is empty for V7
+ UserType deserializedType = deserializedTypes.getNullable(bytes("test_type"));
+ assertNotNull("Type should exist", deserializedType);
+ assertEquals("", deserializedType.comment);
+ assertEquals("", deserializedType.securityLabel);
+ }
+
+ // Helper methods for creating test objects
+
+ private TableMetadata createSimpleTable(String name, String comment, String securityLabel)
+ {
+ return TableMetadata.builder(KEYSPACE, name)
+ .addPartitionKeyColumn("id", Int32Type.instance)
+ .addRegularColumn("email", UTF8Type.instance)
+ .comment(comment)
+ .securityLabel(securityLabel)
+ .build();
+ }
+
+ private UserType createUserType(String name, String comment, String securityLabel)
+ {
+ FieldIdentifier field = FieldIdentifier.forUnquoted("field1");
+ return new UserType(KEYSPACE,
+ bytes(name),
+ Arrays.asList(field),
+ Arrays.asList(UTF8Type.instance),
+ true,
+ comment,
+ securityLabel);
+ }
+
+ // Helper methods for serialization/deserialization
+
+ private KeyspaceMetadata serializeAndDeserializeKeyspace(KeyspaceMetadata original) throws IOException
+ {
+ DataOutputBuffer out = new DataOutputBuffer();
+ KeyspaceMetadata.serializer.serialize(original, out, Version.V8);
+
+ DataInputBuffer in = new DataInputBuffer(out.toByteArray());
+ return KeyspaceMetadata.serializer.deserialize(in, Version.V8);
+ }
+
+ private TableMetadata serializeAndDeserializeTable(TableMetadata original) throws IOException
+ {
+ DataOutputBuffer out = new DataOutputBuffer();
+ TableMetadata.serializer.serialize(original, out, Version.V8);
+
+ DataInputBuffer in = new DataInputBuffer(out.toByteArray());
+ return TableMetadata.serializer.deserialize(in, Types.none(), UserFunctions.none(), Version.V8);
+ }
+
+ private UserType serializeAndDeserializeType(UserType original) throws IOException
+ {
+ Types types = Types.of(original);
+ return serializeAndDeserializeTypes(types).getNullable(original.name);
+ }
+
+ private Types serializeAndDeserializeTypes(Types original) throws IOException
+ {
+ DataOutputBuffer out = new DataOutputBuffer();
+ Types.serializer.serialize(original, out, Version.V8);
+
+ DataInputBuffer in = new DataInputBuffer(out.toByteArray());
+ return Types.serializer.deserialize(KEYSPACE, in, Version.V8);
+ }
+}
diff --git a/test/unit/org/apache/cassandra/schema/createlike/CreateLikeTest.java b/test/unit/org/apache/cassandra/schema/createlike/CreateLikeTest.java
index 291a050464..e7b786cd14 100644
--- a/test/unit/org/apache/cassandra/schema/createlike/CreateLikeTest.java
+++ b/test/unit/org/apache/cassandra/schema/createlike/CreateLikeTest.java
@@ -42,6 +42,7 @@ import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.cql3.Duration;
import org.apache.cassandra.cql3.validation.operations.CreateTest;
import org.apache.cassandra.db.compaction.LeveledCompactionStrategy;
+import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.exceptions.AlreadyExistsException;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.InvalidRequestException;
@@ -581,7 +582,7 @@ public class CreateLikeTest extends CQLTester
{
createTable(sourceKs, "CREATE TABLE %s (a int PRIMARY KEY, b int, c text)", "tb");
String index = createIndex("CREATE INDEX ON " + sourceKs + ".tb (c)");
- assertInvalidThrowMessage("Souce Table '" + targetKs + "." + index + "' doesn't exist", InvalidRequestException.class,
+ assertInvalidThrowMessage("Source Table '" + targetKs + "." + index + "' doesn't exist", InvalidRequestException.class,
"CREATE TABLE " + sourceKs + ".newtb LIKE " + targetKs + "." + index + ";");
assertInvalidThrowMessage("System keyspace 'system' is not user-modifiable", InvalidRequestException.class,
"CREATE TABLE system.local_clone LIKE system.local ;");
@@ -700,6 +701,82 @@ public class CreateLikeTest extends CQLTester
"Source table " + sourceKs + "." + sourceTb + " to copy indexes from to " + targetKs + ".targettbb has custom indexes. These indexes were not copied: " + customIndexes);
}
+ @Test
+ public void testCreateTableLikeWithComments() throws Throwable
+ {
+ String sourceTb = createTable(sourceKs, "CREATE TABLE %s (a int PRIMARY KEY, b int, c text)", "source_tb_comments");
+ execute("COMMENT ON TABLE " + sourceKs + "." + sourceTb + " IS 'Test table comment'");
+ execute("COMMENT ON COLUMN " + sourceKs + "." + sourceTb + ".b IS 'Test column comment'");
+
+ // Test CREATE TABLE copy LIKE source WITH COMMENTS
+ String targetTb = createTableLike("CREATE TABLE %s LIKE %s WITH COMMENTS", sourceTb, sourceKs, targetKs);
+
+ // Verify that the table structure is copied
+ assertTableMetaEqualsWithoutKs(sourceKs, targetKs, sourceTb, targetTb, true, false, false);
+
+ // Verify that comments are copied
+ TableMetadata sourceTable = getTableMetadata(sourceKs, sourceTb);
+ TableMetadata targetTable = getTableMetadata(targetKs, targetTb);
+
+ assertEquals("Table comment should be copied", sourceTable.params.comment, targetTable.params.comment);
+ assertEquals("Column comment should be copied",
+ sourceTable.getColumn(UTF8Type.instance.decompose("b")).comment,
+ targetTable.getColumn(UTF8Type.instance.decompose("b")).comment);
+ }
+
+ @Test
+ public void testCreateTableLikeWithSecurityLabels() throws Throwable
+ {
+ String sourceTb = createTable(sourceKs, "CREATE TABLE %s (a int PRIMARY KEY, b int, c text)", "source_tb_labels");
+ execute("SECURITY LABEL ON TABLE " + sourceKs + "." + sourceTb + " IS 'confidential'");
+ execute("SECURITY LABEL ON COLUMN " + sourceKs + "." + sourceTb + ".b IS 'restricted'");
+
+ // Test CREATE TABLE copy LIKE source WITH SECURITY LABELS
+ String targetTb = createTableLike("CREATE TABLE %s LIKE %s WITH SECURITY LABELS", sourceTb, sourceKs, targetKs);
+
+ // Verify that the table structure is copied
+ assertTableMetaEqualsWithoutKs(sourceKs, targetKs, sourceTb, targetTb, true, false, false);
+
+ // Verify that security labels are copied
+ TableMetadata sourceTable = getTableMetadata(sourceKs, sourceTb);
+ TableMetadata targetTable = getTableMetadata(targetKs, targetTb);
+
+ assertEquals("Table security label should be copied", sourceTable.params.securityLabel, targetTable.params.securityLabel);
+ assertEquals("Column security label should be copied",
+ sourceTable.getColumn(UTF8Type.instance.decompose("b")).securityLabel,
+ targetTable.getColumn(UTF8Type.instance.decompose("b")).securityLabel);
+ }
+
+ @Test
+ public void testCreateTableLikeWithAllOptions() throws Throwable
+ {
+ String sourceTb = createTable(sourceKs, "CREATE TABLE %s (a int PRIMARY KEY, b int, c text)", "source_tb_all");
+ execute("COMMENT ON TABLE " + sourceKs + "." + sourceTb + " IS 'Test table comment'");
+ execute("COMMENT ON COLUMN " + sourceKs + "." + sourceTb + ".b IS 'Test column comment'");
+ execute("SECURITY LABEL ON TABLE " + sourceKs + "." + sourceTb + " IS 'confidential'");
+ execute("SECURITY LABEL ON COLUMN " + sourceKs + "." + sourceTb + ".b IS 'restricted'");
+ createIndex(sourceKs, "CREATE INDEX ON %s (b)");
+
+ // Test CREATE TABLE copy LIKE source WITH INDEXES AND COMMENTS AND SECURITY LABELS
+ String targetTb = createTableLike("CREATE TABLE %s LIKE %s WITH INDEXES AND COMMENTS AND SECURITY LABELS", sourceTb, sourceKs, targetKs);
+
+ // Verify that the table structure and indexes are copied
+ assertTableMetaEqualsWithoutKs(sourceKs, targetKs, sourceTb, targetTb, true, true, true);
+
+ // Verify that comments and security labels are copied
+ TableMetadata sourceTable = getTableMetadata(sourceKs, sourceTb);
+ TableMetadata targetTable = getTableMetadata(targetKs, targetTb);
+
+ assertEquals("Table comment should be copied", sourceTable.params.comment, targetTable.params.comment);
+ assertEquals("Table security label should be copied", sourceTable.params.securityLabel, targetTable.params.securityLabel);
+ assertEquals("Column comment should be copied",
+ sourceTable.getColumn(UTF8Type.instance.decompose("b")).comment,
+ targetTable.getColumn(UTF8Type.instance.decompose("b")).comment);
+ assertEquals("Column security label should be copied",
+ sourceTable.getColumn(UTF8Type.instance.decompose("b")).securityLabel,
+ targetTable.getColumn(UTF8Type.instance.decompose("b")).securityLabel);
+ }
+
private void assertTableMetaEqualsWithoutKs(String sourceKs, String targetKs, String sourceTb, String targetTb)
{
assertTableMetaEqualsWithoutKs(sourceKs, targetKs, sourceTb, targetTb, true, true, false);
diff --git a/test/unit/org/apache/cassandra/schema/createlike/CreateLikeWithSessionTest.java b/test/unit/org/apache/cassandra/schema/createlike/CreateLikeWithSessionTest.java
index e676961d18..2045868646 100644
--- a/test/unit/org/apache/cassandra/schema/createlike/CreateLikeWithSessionTest.java
+++ b/test/unit/org/apache/cassandra/schema/createlike/CreateLikeWithSessionTest.java
@@ -59,7 +59,7 @@ public class CreateLikeWithSessionTest extends CQLTester
assertThatExceptionOfType(com.datastax.driver.core.exceptions.InvalidQueryException.class).isThrownBy(() -> executeNet("CREATE TABLE tb7 LIKE " + ks2 + "." + tb1))
- .withMessage("Souce Table 'ks2.tb1' doesn't exist");
+ .withMessage("Source Table 'ks2.tb1' doesn't exist");
assertNotNull(getTableMetadata(ks1, tb1));
assertNotNull(getTableMetadata(ks1, "tb3"));
diff --git a/test/unit/org/apache/cassandra/tools/nodetool/ExportImportListCompressionDictionaryTest.java b/test/unit/org/apache/cassandra/tools/nodetool/ExportImportListCompressionDictionaryTest.java
index bd806ef953..fbb6f2a3aa 100644
--- a/test/unit/org/apache/cassandra/tools/nodetool/ExportImportListCompressionDictionaryTest.java
+++ b/test/unit/org/apache/cassandra/tools/nodetool/ExportImportListCompressionDictionaryTest.java
@@ -38,7 +38,6 @@ import static java.lang.String.format;
import static org.apache.cassandra.tools.ToolRunner.invokeNodetool;
import static org.apache.cassandra.utils.JsonUtils.serializeToJsonFile;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class ExportImportListCompressionDictionaryTest extends CQLTester
@@ -151,10 +150,9 @@ public class ExportImportListCompressionDictionaryTest extends CQLTester
ToolResult result = invokeNodetool("compressiondictionary", "import", pair.right.absolutePath());
Assert.assertEquals(1, result.getExitCode());
- assertEquals(format("Unable to import dictionary JSON: Table %s.%s does not exist or does not support dictionary compression\n",
- keyspace(),
- table),
- result.getStderr());
+ assertTrue(result.getStderr().contains(format("Unable to import dictionary JSON: Table %s.%s does not exist or does not support dictionary compression",
+ keyspace(),
+ table)));
}
private void importDictionary(File dictFile)
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org