CASSANDRA-20943 Introducing comments and security labels for schema elements

This commit is contained in:
jkonisa 2025-10-03 15:15:42 -07:00
parent dc89b8c802
commit b11633be4f
47 changed files with 4749 additions and 118 deletions

View File

@ -1,4 +1,5 @@
5.1 5.1
* Introducing comments and security labels for schema elements (CASSANDRA-20943)
* Extend nodetool tablestats for dictionary memory usage (CASSANDRA-20940) * Extend nodetool tablestats for dictionary memory usage (CASSANDRA-20940)
* Introduce separate GCInspector thresholds for concurrent GC events (CASSANDRA-20980) * Introduce separate GCInspector thresholds for concurrent GC events (CASSANDRA-20980)
* Reduce contention in MemtableAllocator.allocate (CASSANDRA-20226) * Reduce contention in MemtableAllocator.allocate (CASSANDRA-20226)

View File

@ -2610,6 +2610,14 @@ drop_compact_storage_enabled: false
# Defaults to true, which means that reconfiguration of password validator via JMX is possible. # Defaults to true, which means that reconfiguration of password validator via JMX is possible.
#password_policy_reconfiguration_enabled: true #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 # 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. # 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. # It is suspicious to use default_time_to_live set to 0 with such compaction strategy.

View File

@ -803,3 +803,192 @@ statements.
However, tables are the only object that can be truncated currently, and the `TABLE` keyword can be omitted. 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. 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.

View File

@ -299,6 +299,14 @@ JUNK ::= /([ \t\r\f\v]+|(--|[/][/])[^\n\r]*([\n\r]|$)|[/][*].*?[*][/])/ ;
| <alterTableStatement> | <alterTableStatement>
| <alterKeyspaceStatement> | <alterKeyspaceStatement>
| <alterUserTypeStatement> | <alterUserTypeStatement>
| <commentOnKeyspaceStatement>
| <commentOnTableStatement>
| <commentOnColumnStatement>
| <commentOnTypeStatement>
| <securityLabelOnKeyspaceStatement>
| <securityLabelOnTableStatement>
| <securityLabelOnColumnStatement>
| <securityLabelOnTypeStatement>
; ;
<authenticationStatement> ::= <createUserStatement> <authenticationStatement> ::= <createUserStatement>
@ -402,6 +410,8 @@ JUNK ::= /([ \t\r\f\v]+|(--|[/][/])[^\n\r]*([\n\r]|$)|[/][*].*?[*][/])/ ;
; ;
<propertyOrOption> ::= <property> <propertyOrOption> ::= <property>
| "INDEXES" | "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''' syntax_rules += r'''
<username> ::= name=( <identifier> | <stringLiteral> ) <username> ::= name=( <identifier> | <stringLiteral> )
; ;

View File

@ -166,10 +166,10 @@ class TestCqlshCompletion(CqlshCompletionCase):
cqlver = '3.1.6' cqlver = '3.1.6'
def test_complete_on_empty_string(self): 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', 'COPY', 'CREATE', 'DEBUG', 'DELETE', 'DESC', 'DESCRIBE',
'DROP', 'GRANT', 'HELP', 'INSERT', 'LIST', 'LOGIN', 'PAGING', 'REVOKE', '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')) 'UPDATE', 'USE', 'exit', 'quit', 'CLEAR', 'CLS', 'history'))
def test_complete_command_words(self): def test_complete_command_words(self):
@ -288,10 +288,10 @@ class TestCqlshCompletion(CqlshCompletionCase):
self.trycompletions( self.trycompletions(
("INSERT INTO twenty_rows_composite_table (a, b, c) " ("INSERT INTO twenty_rows_composite_table (a, b, c) "
"VALUES ( 'eggs', 'sausage', 'spam');"), "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', 'CREATE', 'DEBUG', 'DELETE', 'DESC', 'DESCRIBE', 'DROP',
'ELAPSED', 'EXPAND', 'GRANT', 'HELP', 'INSERT', 'LIST', 'LOGIN', 'PAGING', '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', 'TRUNCATE', 'UPDATE', 'USE', 'exit', 'history', 'quit',
'CLEAR', 'CLS']) 'CLEAR', 'CLS'])
@ -856,7 +856,7 @@ class TestCqlshCompletion(CqlshCompletionCase):
'min_index_interval', 'min_index_interval',
'speculative_retry', 'additional_write_policy', 'speculative_retry', 'additional_write_policy',
'cdc', 'read_repair', 'cdc', 'read_repair',
'INDEXES']) 'INDEXES', 'COMMENTS', 'SECURITY'])
self.trycompletions('CREATE TABLE new_table LIKE old_table WITH INDEXES ', self.trycompletions('CREATE TABLE new_table LIKE old_table WITH INDEXES ',
choices=[';' , '=', 'AND']) choices=[';' , '=', 'AND'])
self.trycompletions('CREATE TABLE ' + 'new_table LIKE old_table WITH bloom_filter_fp_chance ', self.trycompletions('CREATE TABLE ' + 'new_table LIKE old_table WITH bloom_filter_fp_chance ',
@ -909,7 +909,7 @@ class TestCqlshCompletion(CqlshCompletionCase):
'min_index_interval', 'min_index_interval',
'speculative_retry', 'additional_write_policy', 'speculative_retry', 'additional_write_policy',
'cdc', 'read_repair', 'cdc', 'read_repair',
'INDEXES']) 'INDEXES', 'COMMENTS', 'SECURITY'])
self.trycompletions('CREATE TABLE ' + "new_table LIKE old_table WITH compaction = " self.trycompletions('CREATE TABLE ' + "new_table LIKE old_table WITH compaction = "
+ "{'class': 'TimeWindowCompactionStrategy', '", + "{'class': 'TimeWindowCompactionStrategy', '",
choices=['compaction_window_unit', 'compaction_window_size', choices=['compaction_window_unit', 'compaction_window_size',

View File

@ -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_KEYSPACES: K E Y S P A C E S;
K_COLUMNFAMILY:( C O L U M N F A M I L Y K_COLUMNFAMILY:( C O L U M N F A M I L Y
| T A B L E ); | 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 K_TABLES: ( C O L U M N F A M I L I E S
| T A B L E S ); | T A B L E S );
K_MATERIALIZED:M A T E R I A L I Z E D; 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_TRIGGER: T R I G G E R;
K_STATIC: S T A T I C; K_STATIC: S T A T I C;
K_FROZEN: F R O Z E N; 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_FUNCTION: F U N C T I O N;
K_FUNCTIONS: F U N C T I O N S; 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_VECTOR: V E C T O R;
K_ANN: A N N; 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 // Case-insensitive alpha characters
fragment A: ('a'|'A'); fragment A: ('a'|'A');
fragment B: ('b'|'B'); fragment B: ('b'|'B');

View File

@ -284,6 +284,16 @@ cqlStatement returns [CQLStatement.Raw stmt]
| st45=copyTableStatement { $stmt = st45; } | st45=copyTableStatement { $stmt = st45; }
| st46=batchTxnStatement { $stmt = st46; } | st46=batchTxnStatement { $stmt = st46; }
| st47=letStatement { $stmt = st47; } | 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] propertyOrOption[CopyTableStatement.Raw stmt]
: tableLikeSingleOption[stmt] : likeOption[stmt]
| property[stmt.attrs] | property[stmt.attrs]
; ;
tableLikeSingleOption[CopyTableStatement.Raw stmt] likeOption[CopyTableStatement.Raw stmt]
: K_INDEXES {$stmt.withLikeOption(CopyTableStatement.CreateLikeOption.INDEXES);} : 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); } : 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>; * DROP TABLE [IF EXISTS] <table>;
*/ */
@ -1784,6 +1879,22 @@ columnFamilyName returns [QualifiedName name]
: (ksName[name] '.')? cfName[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] userTypeName returns [UTName name]
: (ks=noncol_ident '.')? ut=non_type_ident { $name = new UTName(ks, ut); } : (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_ONLY
| K_STATIC | K_STATIC
| K_FROZEN | K_FROZEN
| K_FOR
| K_TUPLE | K_TUPLE
| K_FUNCTION | K_FUNCTION
| K_FUNCTIONS | K_FUNCTIONS
@ -2362,5 +2474,12 @@ basic_unreserved_keyword returns [String str]
| K_LET | K_LET
| K_THEN | K_THEN
| K_TRANSACTION | K_TRANSACTION
| K_COMMENT
| K_COMMENTS
| K_SECURITY
| K_LABEL
| K_LABELS
| K_FIELD
| K_COLUMN
) { $str = $k.text; } ) { $str = $k.text; }
; ;

View File

@ -64,6 +64,14 @@ public enum AuditLogEntryType
USE_KEYSPACE(AuditLogEntryCategory.OTHER), USE_KEYSPACE(AuditLogEntryCategory.OTHER),
DESCRIBE(AuditLogEntryCategory.OTHER), DESCRIBE(AuditLogEntryCategory.OTHER),
TRANSACTION(AuditLogEntryCategory.TRANSACTION), 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 * Common Audit Log Entry Types

View File

@ -108,6 +108,9 @@ public class Config
public volatile DurationSpec.IntMillisecondsBound credentials_update_interval = null; public volatile DurationSpec.IntMillisecondsBound credentials_update_interval = null;
public volatile boolean credentials_cache_active_update = false; 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 */ /* Hashing strategy Random or OPHF */
public String partitioner; public String partitioner;

View File

@ -2212,6 +2212,16 @@ public class DatabaseDescriptor
conf.credentials_cache_active_update = update; 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() public static int getMaxValueSize()
{ {
return Ints.saturatedCast(conf.max_value_size.toMebibytes() * 1024L * 1024); return Ints.saturatedCast(conf.max_value_size.toMebibytes() * 1024L * 1024);

View File

@ -28,17 +28,28 @@ import java.util.Iterator;
public class ColumnSpecification public class ColumnSpecification
{ {
protected static final String EMPTY_COMMENT = "";
protected static final String EMPTY_SECURITY_LABEL = "";
public final String ksName; public final String ksName;
public final String cfName; public final String cfName;
public final ColumnIdentifier name; public final ColumnIdentifier name;
public final AbstractType<?> type; public final AbstractType<?> type;
public final String comment;
public final String securityLabel;
public ColumnSpecification(String ksName, String cfName, ColumnIdentifier name, AbstractType<?> type) 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.ksName = ksName;
this.cfName = cfName; this.cfName = cfName;
this.name = name; this.name = name;
this.type = type; 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) public ColumnSpecification withAlias(ColumnIdentifier alias)
{ {
return new ColumnSpecification(ksName, cfName, alias, type); return new ColumnSpecification(ksName, cfName, alias, type, comment, securityLabel);
} }
public boolean isReversedType() public boolean isReversedType()
@ -100,6 +111,8 @@ public class ColumnSpecification
return MoreObjects.toStringHelper(this) return MoreObjects.toStringHelper(this)
.add("name", name) .add("name", name)
.add("type", type) .add("type", type)
.add("comment", comment)
.add("securityLabel", securityLabel)
.toString(); .toString();
} }
} }

View File

@ -96,4 +96,23 @@ public interface SchemaElement
* @return a CQL representation of this element * @return a CQL representation of this element
*/ */
String toCqlString(boolean withWarnings, boolean withInternals, boolean ifNotExists); 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);
}
} }

View File

@ -374,7 +374,7 @@ public abstract class DescribeStatement<T> extends CQLStatement.Raw implements C
return ImmutableList.of(bytes(element.elementKeyspaceQuotedIfNeeded()), return ImmutableList.of(bytes(element.elementKeyspaceQuotedIfNeeded()),
bytes(element.elementType().toString()), bytes(element.elementType().toString()),
bytes(element.elementNameQuotedIfNeeded()), 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()), return ImmutableList.of(bytes(element.elementKeyspaceQuotedIfNeeded()),
bytes(element.elementType().toString()), bytes(element.elementType().toString()),
bytes(element.elementNameQuotedIfNeeded()), bytes(element.elementNameQuotedIfNeeded()),
bytes(element.toCqlString(true, withInternals, false))); bytes(element.describe(true, withInternals, false)));
} }
} }

View File

@ -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(';');
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -20,6 +20,7 @@ package org.apache.cassandra.cql3.statements.schema;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@ -27,6 +28,8 @@ import java.util.stream.Collectors;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import org.apache.commons.lang3.StringUtils;
import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogContext;
import org.apache.cassandra.audit.AuditLogEntryType; import org.apache.cassandra.audit.AuditLogEntryType;
import org.apache.cassandra.auth.Permission; import org.apache.cassandra.auth.Permission;
@ -67,14 +70,14 @@ public final class CopyTableStatement extends AlterSchemaStatement
private final String targetTableName; private final String targetTableName;
private final boolean ifNotExists; private final boolean ifNotExists;
private final TableAttributes attrs; private final TableAttributes attrs;
private final CreateLikeOption createLikeOption; private final Set<CreateLikeOption> createLikeOptions;
public CopyTableStatement(String sourceKeyspace, public CopyTableStatement(String sourceKeyspace,
String targetKeyspace, String targetKeyspace,
String sourceTableName, String sourceTableName,
String targetTableName, String targetTableName,
boolean ifNotExists, boolean ifNotExists,
CreateLikeOption createLikeOption, Set<CreateLikeOption> createLikeOptions,
TableAttributes attrs) TableAttributes attrs)
{ {
super(targetKeyspace); super(targetKeyspace);
@ -83,7 +86,9 @@ public final class CopyTableStatement extends AlterSchemaStatement
this.sourceTableName = sourceTableName; this.sourceTableName = sourceTableName;
this.targetTableName = targetTableName; this.targetTableName = targetTableName;
this.ifNotExists = ifNotExists; this.ifNotExists = ifNotExists;
this.createLikeOption = createLikeOption; this.createLikeOptions = createLikeOptions != null
? EnumSet.copyOf(createLikeOptions)
: EnumSet.noneOf(CreateLikeOption.class);
this.attrs = attrs; this.attrs = attrs;
} }
@ -115,10 +120,10 @@ public final class CopyTableStatement extends AlterSchemaStatement
if (null == sourceKeyspaceMeta) if (null == sourceKeyspaceMeta)
throw ire("Source Keyspace '%s' doesn't exist", sourceKeyspace); throw ire("Source Keyspace '%s' doesn't exist", sourceKeyspace);
TableMetadata sourceTableMeta = sourceKeyspaceMeta.getTableNullable(sourceTableName); TableMetadata sourceTableMeta = sourceKeyspaceMeta.getTableOrViewNullable(sourceTableName);
if (null == sourceTableMeta) 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()) if (sourceTableMeta.isIndex())
throw ire("Cannot use CREATE TABLE LIKE on an index table '%s.%s'.", sourceKeyspace, sourceTableName); 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()) if (!sourceTableMeta.params.compression.isEnabled())
Guardrails.uncompressedTablesEnabled.ensureEnabled(state); 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 // id can be set through create table like cql using WITH ID
String sourceCQLString = sourceTableMeta.toCqlString(false, false, false, false); String sourceCQLString = sourceTableMeta.toCqlString(false, false, false, false);
@ -203,9 +208,12 @@ public final class CopyTableStatement extends AlterSchemaStatement
.indexes(Indexes.none()) .indexes(Indexes.none())
.triggers(Triggers.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 originalParams = targetBuilder.build().params;
TableParams newTableParams = attrs.asAlteredTableParams(originalParams); TableParams newTableParams = attrs.asAlteredTableParams(originalParams);
maybeCopyIndexes(targetBuilder, sourceTableMeta, targetKeyspaceMeta);
TableMetadata table = targetBuilder.params(newTableParams) TableMetadata table = targetBuilder.params(newTableParams)
.id(TableId.get(metadata)) .id(TableId.get(metadata))
@ -239,59 +247,13 @@ public final class CopyTableStatement extends AlterSchemaStatement
validateDefaultTimeToLive(attrs.asNewTableParams(keyspaceName)); 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 public final static class Raw extends CQLStatement.Raw
{ {
private final QualifiedName oldName; private final QualifiedName oldName;
private final QualifiedName newName; private final QualifiedName newName;
private final boolean ifNotExists; private final boolean ifNotExists;
public final TableAttributes attrs = new TableAttributes(); 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) 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 oldKeyspace = oldName.hasKeyspace() ? oldName.getKeyspace() : state.getKeyspace();
String newKeyspace = newName.hasKeyspace() ? newName.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);
}
}
} }
} }

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -31,6 +31,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.apache.cassandra.cql3.*; 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.Constants;
import org.apache.cassandra.cql3.terms.MultiElements; import org.apache.cassandra.cql3.terms.MultiElements;
import org.apache.cassandra.cql3.terms.Term; 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 Logger logger = LoggerFactory.getLogger(UserType.class);
private static final ConflictBehavior CONFLICT_BEHAVIOR = ConflictBehavior.get(); 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 String keyspace;
public final ByteBuffer name; public final ByteBuffer name;
public final String comment;
public final String securityLabel;
private final List<FieldIdentifier> fieldNames; private final List<FieldIdentifier> fieldNames;
private final List<String> stringFieldNames; private final List<String> stringFieldNames;
private final Map<FieldIdentifier, String> fieldComments;
private final Map<FieldIdentifier, String> fieldSecurityLabels;
private final boolean isMultiCell; private final boolean isMultiCell;
private final UserTypeSerializer serializer; private final UserTypeSerializer serializer;
public UserType(String keyspace, ByteBuffer name, List<FieldIdentifier> fieldNames, List<AbstractType<?>> fieldTypes, boolean isMultiCell) 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); super(fieldTypes, false);
assert fieldNames.size() == fieldTypes.size(); assert fieldNames.size() == fieldTypes.size();
this.keyspace = keyspace; this.keyspace = keyspace;
this.name = name; this.name = name;
this.comment = comment == null ? EMPTY_COMMENT : comment;
this.securityLabel = securityLabel == null ? EMPTY_SECURITY_LABEL : securityLabel;
this.fieldNames = fieldNames; this.fieldNames = fieldNames;
this.stringFieldNames = new ArrayList<>(fieldNames.size()); this.stringFieldNames = new ArrayList<>(fieldNames.size());
this.fieldComments = Collections.unmodifiableMap(new HashMap<>(fieldComments));
this.fieldSecurityLabels = Collections.unmodifiableMap(new HashMap<>(fieldSecurityLabels));
this.isMultiCell = isMultiCell; this.isMultiCell = isMultiCell;
LinkedHashMap<String , TypeSerializer<?>> fieldSerializers = new LinkedHashMap<>(fieldTypes.size()); LinkedHashMap<String , TypeSerializer<?>> fieldSerializers = new LinkedHashMap<>(fieldTypes.size());
@ -106,7 +135,7 @@ public class UserType extends TupleType implements SchemaElement
columnTypes.add(p.right); 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 @Override
@ -311,13 +340,13 @@ public class UserType extends TupleType implements SchemaElement
@Override @Override
public UserType freeze() 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 @Override
public UserType unfreeze() 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 @Override
@ -442,14 +471,66 @@ public class UserType extends TupleType implements SchemaElement
{ {
return isMultiCell == udt.isMultiCell return isMultiCell == udt.isMultiCell
? udt ? 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, return new UserType(keyspace,
name, name,
fieldNames, fieldNames,
Lists.newArrayList(transform(fieldTypes(), t -> t.withUpdatedUserType(udt))), 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 @Override
@ -629,6 +710,16 @@ public class UserType extends TupleType implements SchemaElement
return builder.toString(); 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 @Override
protected String componentOrFieldName(int i) protected String componentOrFieldName(int i)
{ {

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -37,6 +37,8 @@ public final class SystemViewsKeyspace extends VirtualKeyspace
.add(new ClientsTable(VIRTUAL_VIEWS)) .add(new ClientsTable(VIRTUAL_VIEWS))
.add(new SettingsTable(VIRTUAL_VIEWS)) .add(new SettingsTable(VIRTUAL_VIEWS))
.add(new SystemPropertiesTable(VIRTUAL_VIEWS)) .add(new SystemPropertiesTable(VIRTUAL_VIEWS))
.add(new SchemaCommentsTable(VIRTUAL_VIEWS))
.add(new SchemaSecurityLabelsTable(VIRTUAL_VIEWS))
.add(new SSTableTasksTable(VIRTUAL_VIEWS)) .add(new SSTableTasksTable(VIRTUAL_VIEWS))
// Fully backward/forward compatible with the legace ThreadPoolsTable under the same "system_views.thread_pools" name. // Fully backward/forward compatible with the legace ThreadPoolsTable under the same "system_views.thread_pools" name.
.add(CollectionVirtualTableAdapter.create(VIRTUAL_VIEWS, .add(CollectionVirtualTableAdapter.create(VIRTUAL_VIEWS,

View File

@ -251,7 +251,22 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta
@Nullable ColumnMask mask, @Nullable ColumnMask mask,
@Nonnull ColumnConstraints columnConstraints) @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; this.uniqueId = uniqueId;
assert name != null && type != null && kind != null; 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), 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() 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) 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) 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) 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() public boolean isPartitionKey()
@ -397,7 +422,13 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta
public ColumnMetadata withNewColumnConstraints(ColumnConstraints constraints) public ColumnMetadata withNewColumnConstraints(ColumnConstraints constraints)
{ {
constraints.validate(this); 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() public void removeColumnConstraints()
@ -739,6 +770,11 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta
} }
if (version.isAtLeast(Version.V7)) if (version.isAtLeast(Version.V7))
out.writeVInt32(t.uniqueId); 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 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; int uniqueId = NO_UNIQUE_ID;
if (version.isAtLeast(Version.V7)) if (version.isAtLeast(Version.V7))
uniqueId = in.readVInt32(); 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) public long serializedSize(ColumnMetadata t, Version version)
@ -777,6 +820,12 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta
if (t.hasConstraint()) if (t.hasConstraint())
constraintsSize += t.getColumnConstraints().serializer().serializedSize(t.columnConstraints, version); 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) + return sizeof(t.ksName) +
sizeof(t.cfName) + sizeof(t.cfName) +
sizeof(t.kind.name()) + sizeof(t.kind.name()) +
@ -788,7 +837,8 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta
BOOL_SIZE + BOOL_SIZE +
((t.mask == null) ? 0 : ColumnMask.serializer.serializedSize(t.mask, version)) + ((t.mask == null) ? 0 : ColumnMask.serializer.serializedSize(t.mask, version)) +
constraintsSize + constraintsSize +
(version.isAtLeast(Version.V7) ? sizeofVInt(t.uniqueId) : 0); (version.isAtLeast(Version.V7) ? sizeofVInt(t.uniqueId) : 0) +
commentsAndSecurityLabelSize;
} }
} }
} }

View File

@ -37,6 +37,7 @@ import org.apache.cassandra.cql3.SchemaElement;
import org.apache.cassandra.cql3.functions.Function; import org.apache.cassandra.cql3.functions.Function;
import org.apache.cassandra.cql3.functions.UDAggregate; import org.apache.cassandra.cql3.functions.UDAggregate;
import org.apache.cassandra.cql3.functions.UDFunction; 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.db.marshal.UserType;
import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.RequestValidationException; import org.apache.cassandra.exceptions.RequestValidationException;
@ -397,6 +398,16 @@ public final class KeyspaceMetadata implements SchemaElement
return builder.toString(); 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) public void validate(ClusterMetadata metadata)
{ {
validateKeyspaceName(name, ConfigurationException::new); validateKeyspaceName(name, ConfigurationException::new);

View File

@ -44,6 +44,8 @@ public final class KeyspaceParams
public static final Serializer serializer = new Serializer(); public static final Serializer serializer = new Serializer();
public static final boolean DEFAULT_DURABLE_WRITES = true; 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} * 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, DURABLE_WRITES,
REPLICATION, REPLICATION,
FAST_PATH; FAST_PATH,
COMMENT,
SECURITY_LABEL;
@Override @Override
public String toString() public String toString()
@ -69,17 +73,26 @@ public final class KeyspaceParams
public final boolean durableWrites; public final boolean durableWrites;
public final ReplicationParams replication; public final ReplicationParams replication;
public final FastPathStrategy fastPath; public final FastPathStrategy fastPath;
public final String comment;
public final String securityLabel;
public KeyspaceParams(boolean durableWrites, ReplicationParams replication, FastPathStrategy fastPath) 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.durableWrites = durableWrites;
this.replication = replication; this.replication = replication;
this.fastPath = fastPath; 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) 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) 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() 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) 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) 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) 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) 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) 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) 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.DURABLE_WRITES.toString(), durableWrites)
.add(Option.REPLICATION.toString(), replication) .add(Option.REPLICATION.toString(), replication)
.add(Option.FAST_PATH.toString(), fastPath.toString()) .add(Option.FAST_PATH.toString(), fastPath.toString())
.add(Option.COMMENT.toString(), comment)
.add(Option.SECURITY_LABEL.toString(), securityLabel)
.toString(); .toString();
} }
@ -165,6 +190,11 @@ public final class KeyspaceParams
out.writeBoolean(t.durableWrites); out.writeBoolean(t.durableWrites);
if (version.isAtLeast(MIN_ACCORD_VERSION)) if (version.isAtLeast(MIN_ACCORD_VERSION))
FastPathStrategy.serializer.serialize(t.fastPath, out, 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 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 fastPath = version.isAtLeast(MIN_ACCORD_VERSION)
? FastPathStrategy.serializer.deserialize(in, version) ? FastPathStrategy.serializer.deserialize(in, version)
: FastPathStrategy.simple(); : 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) public long serializedSize(KeyspaceParams t, Version version)
{ {
return ReplicationParams.serializer.serializedSize(t.replication, version) + return ReplicationParams.serializer.serializedSize(t.replication, version) +
TypeSizes.sizeof(t.durableWrites) + 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); (version.isAtLeast(MIN_ACCORD_VERSION) ? FastPathStrategy.serializer.serializedSize(t.fastPath, version) : 0);
} }
} }

View File

@ -56,6 +56,7 @@ import org.apache.cassandra.cql3.constraints.InvalidConstraintDefinitionExceptio
import org.apache.cassandra.cql3.constraints.NotNullConstraint; import org.apache.cassandra.cql3.constraints.NotNullConstraint;
import org.apache.cassandra.cql3.functions.Function; import org.apache.cassandra.cql3.functions.Function;
import org.apache.cassandra.cql3.functions.masking.ColumnMask; 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.Clustering;
import org.apache.cassandra.db.ClusteringComparator; import org.apache.cassandra.db.ClusteringComparator;
import org.apache.cassandra.db.Columns; import org.apache.cassandra.db.Columns;
@ -1070,7 +1071,7 @@ public class TableMetadata implements SchemaElement
static ColumnMetadata withUniqueId(ColumnMetadata prev, int uniqueId) 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) public Builder id(TableId val)
@ -1132,6 +1133,12 @@ public class TableMetadata implements SchemaElement
return this; return this;
} }
public Builder securityLabel(String val)
{
params.securityLabel(val);
return this;
}
public Builder compaction(CompactionParams val) public Builder compaction(CompactionParams val)
{ {
params.compaction(val); params.compaction(val);
@ -1482,6 +1489,32 @@ public class TableMetadata implements SchemaElement
return this; 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) public Builder alterColumnConstraints(ColumnIdentifier name, ColumnConstraints constraints)
{ {
ColumnMetadata column = columns.get(name.bytes); ColumnMetadata column = columns.get(name.bytes);
@ -1608,6 +1641,16 @@ public class TableMetadata implements SchemaElement
return builder.toString(); 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, public String toCqlString(boolean withWarnings,
boolean withDroppedColumns, boolean withDroppedColumns,
boolean withInternals, boolean withInternals,

View File

@ -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.MIN_INDEX_INTERVAL;
import static org.apache.cassandra.schema.TableParams.Option.PENDING_DROP; 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.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.schema.TableParams.Option.SPECULATIVE_RETRY;
import static org.apache.cassandra.utils.LocalizeString.toLowerCaseLocalized; import static org.apache.cassandra.utils.LocalizeString.toLowerCaseLocalized;
@ -101,7 +102,8 @@ public final class TableParams
TRANSACTIONAL_MODE, TRANSACTIONAL_MODE,
TRANSACTIONAL_MIGRATION_FROM, TRANSACTIONAL_MIGRATION_FROM,
PENDING_DROP, PENDING_DROP,
AUTO_REPAIR; AUTO_REPAIR,
SECURITY_LABEL;
@Override @Override
public String toString() public String toString()
@ -111,6 +113,7 @@ public final class TableParams
} }
public final String comment; public final String comment;
public final String securityLabel;
public final boolean allowAutoSnapshot; public final boolean allowAutoSnapshot;
public final double bloomFilterFpChance; public final double bloomFilterFpChance;
public final double crcCheckChance; public final double crcCheckChance;
@ -139,6 +142,7 @@ public final class TableParams
private TableParams(Builder builder) private TableParams(Builder builder)
{ {
comment = builder.comment; comment = builder.comment;
securityLabel = builder.securityLabel;
allowAutoSnapshot = builder.allowAutoSnapshot; allowAutoSnapshot = builder.allowAutoSnapshot;
bloomFilterFpChance = builder.bloomFilterFpChance == -1 bloomFilterFpChance = builder.bloomFilterFpChance == -1
? builder.compaction.defaultBloomFilterFbChance() ? builder.compaction.defaultBloomFilterFbChance()
@ -178,6 +182,7 @@ public final class TableParams
.bloomFilterFpChance(params.bloomFilterFpChance) .bloomFilterFpChance(params.bloomFilterFpChance)
.caching(params.caching) .caching(params.caching)
.comment(params.comment) .comment(params.comment)
.securityLabel(params.securityLabel)
.compaction(params.compaction) .compaction(params.compaction)
.compression(params.compression) .compression(params.compression)
.memtable(params.memtable) .memtable(params.memtable)
@ -276,6 +281,7 @@ public final class TableParams
TableParams p = (TableParams) o; TableParams p = (TableParams) o;
return comment.equals(p.comment) return comment.equals(p.comment)
&& securityLabel.equals(p.securityLabel)
&& additionalWritePolicy.equals(p.additionalWritePolicy) && additionalWritePolicy.equals(p.additionalWritePolicy)
&& allowAutoSnapshot == p.allowAutoSnapshot && allowAutoSnapshot == p.allowAutoSnapshot
&& bloomFilterFpChance == p.bloomFilterFpChance && bloomFilterFpChance == p.bloomFilterFpChance
@ -305,6 +311,7 @@ public final class TableParams
public int hashCode() public int hashCode()
{ {
return Objects.hashCode(comment, return Objects.hashCode(comment,
securityLabel,
additionalWritePolicy, additionalWritePolicy,
allowAutoSnapshot, allowAutoSnapshot,
bloomFilterFpChance, bloomFilterFpChance,
@ -335,6 +342,7 @@ public final class TableParams
{ {
return MoreObjects.toStringHelper(this) return MoreObjects.toStringHelper(this)
.add(COMMENT.toString(), comment) .add(COMMENT.toString(), comment)
.add(SECURITY_LABEL.toString(), securityLabel)
.add(ADDITIONAL_WRITE_POLICY.toString(), additionalWritePolicy) .add(ADDITIONAL_WRITE_POLICY.toString(), additionalWritePolicy)
.add(ALLOW_AUTO_SNAPSHOT.toString(), allowAutoSnapshot) .add(ALLOW_AUTO_SNAPSHOT.toString(), allowAutoSnapshot)
.add(BLOOM_FILTER_FP_CHANCE.toString(), bloomFilterFpChance) .add(BLOOM_FILTER_FP_CHANCE.toString(), bloomFilterFpChance)
@ -375,6 +383,8 @@ public final class TableParams
.newLine() .newLine()
.append("AND cdc = ").append(cdc) .append("AND cdc = ").append(cdc)
.newLine() .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) .append("AND comment = ").appendWithSingleQuotes(comment)
.newLine() .newLine()
.append("AND compaction = ").append(compaction.asMap()) .append("AND compaction = ").append(compaction.asMap())
@ -431,6 +441,7 @@ public final class TableParams
public static final class Builder public static final class Builder
{ {
private String comment = ""; private String comment = "";
private String securityLabel = "";
private boolean allowAutoSnapshot = true; private boolean allowAutoSnapshot = true;
private double bloomFilterFpChance = -1; private double bloomFilterFpChance = -1;
private double crcCheckChance = 1.0; private double crcCheckChance = 1.0;
@ -470,6 +481,12 @@ public final class TableParams
return this; return this;
} }
public Builder securityLabel(String val)
{
securityLabel = val;
return this;
}
public Builder allowAutoSnapshot(boolean val) public Builder allowAutoSnapshot(boolean val)
{ {
allowAutoSnapshot = val; allowAutoSnapshot = val;
@ -643,6 +660,8 @@ public final class TableParams
out.writeUnsignedVInt32(t.transactionalMigrationFrom.ordinal()); out.writeUnsignedVInt32(t.transactionalMigrationFrom.ordinal());
out.writeBoolean(t.pendingDrop); out.writeBoolean(t.pendingDrop);
} }
if (version.isAtLeast(Version.V8))
out.writeUTF(t.securityLabel);
} }
public TableParams deserialize(DataInputPlus in, Version version) throws IOException public TableParams deserialize(DataInputPlus in, Version version) throws IOException
@ -674,6 +693,8 @@ public final class TableParams
.transactionalMigrationFrom(TransactionalMigrationFromMode.fromOrdinal(in.readUnsignedVInt32())) .transactionalMigrationFrom(TransactionalMigrationFromMode.fromOrdinal(in.readUnsignedVInt32()))
.pendingDrop(in.readBoolean()); .pendingDrop(in.readBoolean());
} }
if (version.isAtLeast(Version.V8))
builder.securityLabel(in.readUTF());
return builder.build(); return builder.build();
} }
@ -705,6 +726,10 @@ public final class TableParams
sizeofUnsignedVInt(t.transactionalMigrationFrom.ordinal()) + sizeofUnsignedVInt(t.transactionalMigrationFrom.ordinal()) +
sizeof(t.pendingDrop); sizeof(t.pendingDrop);
} }
if (version.isAtLeast(Version.V8))
{
size += sizeof(t.securityLabel);
}
return size; return size;
} }

View File

@ -20,6 +20,7 @@ package org.apache.cassandra.schema;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.*; import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Stream; import java.util.stream.Stream;
import java.util.stream.StreamSupport; 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 com.google.common.collect.Iterables.transform;
import static org.apache.cassandra.db.TypeSizes.sizeof; import static org.apache.cassandra.db.TypeSizes.sizeof;
import static org.apache.cassandra.db.TypeSizes.sizeofUnsignedVInt;
import static org.apache.cassandra.utils.ByteBufferUtil.bytes; 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(); public static final Serializer serializer = new Serializer();
private static final Types NONE = new Types(ImmutableMap.of()); 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; 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) 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 = List<CQL3Type.Raw> rawFieldTypes =
fieldTypes.stream() fieldTypes.stream()
.map(CQLTypeParser::parseRaw) .map(CQLTypeParser::parseRaw)
.collect(toList()); .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 private static final class RawUDT
@ -376,12 +398,36 @@ public final class Types implements Iterable<UserType>
final String name; final String name;
final List<String> fieldNames; final List<String> fieldNames;
final List<CQL3Type.Raw> fieldTypes; 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) 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.name = name;
this.fieldNames = fieldNames; this.fieldNames = fieldNames;
this.fieldTypes = fieldTypes; this.fieldTypes = fieldTypes;
this.comment = comment;
this.securityLabel = securityLabel;
this.fieldComments = fieldComments;
this.fieldSecurityLabels = fieldSecurityLabels;
} }
boolean referencesUserType(RawUDT other) boolean referencesUserType(RawUDT other)
@ -401,7 +447,18 @@ public final class Types implements Iterable<UserType>
.map(t -> t.prepareInternal(keyspace, types).getType()) .map(t -> t.prepareInternal(keyspace, types).getType())
.collect(toList()); .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 @Override
@ -461,7 +518,8 @@ public final class Types implements Iterable<UserType>
for (UserType type : t.types.values()) for (UserType type : t.types.values())
{ {
out.writeUTF(type.getNameAsString()); 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()); List<String> fieldTypes = type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList());
out.writeInt(fieldNames.size()); out.writeInt(fieldNames.size());
for (String s : fieldNames) for (String s : fieldNames)
@ -469,6 +527,12 @@ public final class Types implements Iterable<UserType>
out.writeInt(fieldTypes.size()); out.writeInt(fieldTypes.size());
for (String s : fieldTypes) for (String s : fieldTypes)
out.writeUTF(s); 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); List<String> fieldTypes = new ArrayList<>(fieldTypeSize);
for (int x = 0; x < fieldTypeSize; x++) for (int x = 0; x < fieldTypeSize; x++)
fieldTypes.add(in.readUTF()); 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(); return builder.build();
} }
@ -498,16 +572,95 @@ public final class Types implements Iterable<UserType>
for (UserType type : t.types.values()) for (UserType type : t.types.values())
{ {
size += sizeof(type.getNameAsString()); size += sizeof(type.getNameAsString());
List<String> fieldNames = type.fieldNames().stream().map(FieldIdentifier::toString).collect(toList()); List<FieldIdentifier> fieldIdentifiers = type.fieldNames();
List<String> fieldTypes = type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList()); 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()); size += sizeof(fieldNames.size());
for (String s : fieldNames) for (String s : fieldNames)
size += sizeof(s); size += sizeof(s);
size += sizeof(fieldTypes.size()); size += sizeof(fieldTypes.size());
for (String s : fieldTypes) for (String s : fieldTypes)
size += sizeof(s); size += sizeof(s);
if (version.isAtLeast(Version.V8))
{
size += sizeof(type.comment);
size += sizeof(type.securityLabel);
size += fieldMetadataSize(type, fieldIdentifiers);
}
} }
return size; 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;
}
} }
} }

View File

@ -34,7 +34,7 @@ import static org.apache.cassandra.db.TypeSizes.sizeofUnsignedVInt;
public class NodeVersion implements Comparable<NodeVersion> public class NodeVersion implements Comparable<NodeVersion>
{ {
public static final Serializer serializer = new Serializer(); 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); public static final NodeVersion CURRENT = new NodeVersion(new CassandraVersion(FBUtilities.getReleaseVersionString()), CURRENT_METADATA_VERSION);
private static final CassandraVersion SINCE_VERSION = CassandraVersion.CASSANDRA_5_1; private static final CassandraVersion SINCE_VERSION = CassandraVersion.CASSANDRA_5_1;

View File

@ -68,6 +68,11 @@ public enum Version
*/ */
V7(7), V7(7),
/**
* - Comments and security labels for schema elements (keyspaces, tables, columns, UDTs, and UDT fields)
*/
V8(8),
UNKNOWN(Integer.MAX_VALUE); UNKNOWN(Integer.MAX_VALUE);
/** /**

View File

@ -46,6 +46,7 @@ import org.apache.cassandra.transport.ProtocolVersion;
import static java.lang.String.format; import static java.lang.String.format;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@ -265,7 +266,8 @@ public class DescribeStatementTest extends CQLTester
" PRIMARY KEY (keyspace_name, table_name, column_name)\n" + " PRIMARY KEY (keyspace_name, table_name, column_name)\n" +
") WITH CLUSTERING ORDER BY (table_name ASC, column_name ASC)\n" + ") WITH CLUSTERING ORDER BY (table_name ASC, column_name ASC)\n" +
" AND comment = 'virtual column definitions';\n" + " AND comment = 'virtual column definitions';\n" +
"*/")); "*/\n" +
"COMMENT ON TABLE system_virtual_schema.columns IS 'virtual column definitions';"));
} }
@Test @Test
@ -845,7 +847,7 @@ public class DescribeStatementTest extends CQLTester
{ {
requireNetwork(); requireNetwork();
DatabaseDescriptor.setDynamicDataMaskingEnabled(true); DatabaseDescriptor.setDynamicDataMaskingEnabled(true);
String souceTable = createTable(KEYSPACE_PER_TEST, String sourceTable = createTable(KEYSPACE_PER_TEST,
"CREATE TABLE %s (" + "CREATE TABLE %s (" +
" pk1 text, " + " pk1 text, " +
" pk2 int MASKED WITH DEFAULT, " + " pk2 int MASKED WITH DEFAULT, " +
@ -857,14 +859,14 @@ public class DescribeStatementTest extends CQLTester
"PRIMARY KEY ((pk1, pk2), ck1, ck2 ))"); "PRIMARY KEY ((pk1, pk2), ck1, ck2 ))");
TableMetadata source = getTableMetadata(KEYSPACE_PER_TEST, currentTable()); TableMetadata source = getTableMetadata(KEYSPACE_PER_TEST, currentTable());
assertNotNull(source); 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()); TableMetadata target = getTableMetadata(KEYSPACE_PER_TEST, currentTable());
assertNotNull(target); assertNotNull(target);
assertTrue(equalsWithoutTableNameAndDropCns(source, target, true, true, true)); assertTrue(equalsWithoutTableNameAndDropCns(source, target, true, true, true));
assertNotEquals(source.id, target.id); assertNotEquals(source.id, target.id);
assertNotEquals(source.name, target.name); 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" + " pk1 text,\n" +
" pk2 int MASKED WITH system.mask_default(),\n" + " pk2 int MASKED WITH system.mask_default(),\n" +
" ck1 int,\n" + " ck1 int,\n" +
@ -889,10 +891,10 @@ public class DescribeStatementTest extends CQLTester
" AND CLUSTERING ORDER BY (ck1 ASC, ck2 ASC)\n" + " AND CLUSTERING ORDER BY (ck1 ASC, ck2 ASC)\n" +
" AND " + tableParametersCql(); " 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, row(KEYSPACE_PER_TEST,
"table", "table",
souceTable, sourceTable,
sourceTableCreateStatement)); sourceTableCreateStatement));
assertRowsNet(executeDescribeNet("DESCRIBE TABLE " + KEYSPACE_PER_TEST + "." + targetTable + " WITH INTERNALS"), assertRowsNet(executeDescribeNet("DESCRIBE TABLE " + KEYSPACE_PER_TEST + "." + targetTable + " WITH INTERNALS"),
row(KEYSPACE_PER_TEST, 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() private static String allTypesTable()
{ {
return "CREATE TABLE test.has_all_types (\n" + return "CREATE TABLE test.has_all_types (\n" +
@ -1134,6 +1304,11 @@ public class DescribeStatementTest extends CQLTester
} }
private static String tableParametersCql() private static String tableParametersCql()
{
return tableParametersCql("");
}
private static String tableParametersCql(String comment)
{ {
if (!DatabaseDescriptor.getAutoRepairConfig().isAutoRepairSchedulingEnabled()) if (!DatabaseDescriptor.getAutoRepairConfig().isAutoRepairSchedulingEnabled())
{ {
@ -1142,7 +1317,7 @@ public class DescribeStatementTest extends CQLTester
" AND bloom_filter_fp_chance = 0.01\n" + " AND bloom_filter_fp_chance = 0.01\n" +
" AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'}\n" + " AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'}\n" +
" AND cdc = false\n" + " AND cdc = false\n" +
" AND comment = ''\n" + " AND comment = '" + comment + "'\n" +
" AND compaction = " + cqlQuoted(CompactionParams.DEFAULT.asMap()) + "\n" + " AND compaction = " + cqlQuoted(CompactionParams.DEFAULT.asMap()) + "\n" +
" AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'}\n" + " AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'}\n" +
" AND memtable = 'default'\n" + " AND memtable = 'default'\n" +

View File

@ -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);
}
}

View File

@ -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 + "'");
}
}

View File

@ -324,7 +324,11 @@ public class MockSchema
? mockKS.tables.with(metadata) ? mockKS.tables.with(metadata)
: mockKS.tables.withSwapped(metadata); : mockKS.tables.withSwapped(metadata);
mockKS = mockKS.withSwapped(tables); 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) private static TableMetadata newTableMetadata(String ksname)
@ -381,6 +385,7 @@ public class MockSchema
public static class MockSchemaProvider implements SchemaProvider public static class MockSchemaProvider implements SchemaProvider
{ {
private final SchemaProvider originalSchemaProvider = Schema.instance; private final SchemaProvider originalSchemaProvider = Schema.instance;
private volatile Keyspace mockKeyspaceInstance;
@Override @Override
public Set<String> getKeyspaces() public Set<String> getKeyspaces()
@ -464,7 +469,18 @@ public class MockSchema
public Keyspace getKeyspaceInstance(String keyspaceName) public Keyspace getKeyspaceInstance(String keyspaceName)
{ {
if (isMockKS(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); return originalSchemaProvider.getKeyspaceInstance(keyspaceName);
} }

View File

@ -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);
}
}

View File

@ -42,6 +42,7 @@ import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.cql3.Duration; import org.apache.cassandra.cql3.Duration;
import org.apache.cassandra.cql3.validation.operations.CreateTest; import org.apache.cassandra.cql3.validation.operations.CreateTest;
import org.apache.cassandra.db.compaction.LeveledCompactionStrategy; 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.AlreadyExistsException;
import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.InvalidRequestException; 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"); createTable(sourceKs, "CREATE TABLE %s (a int PRIMARY KEY, b int, c text)", "tb");
String index = createIndex("CREATE INDEX ON " + sourceKs + ".tb (c)"); 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 + ";"); "CREATE TABLE " + sourceKs + ".newtb LIKE " + targetKs + "." + index + ";");
assertInvalidThrowMessage("System keyspace 'system' is not user-modifiable", InvalidRequestException.class, assertInvalidThrowMessage("System keyspace 'system' is not user-modifiable", InvalidRequestException.class,
"CREATE TABLE system.local_clone LIKE system.local ;"); "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); "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) private void assertTableMetaEqualsWithoutKs(String sourceKs, String targetKs, String sourceTb, String targetTb)
{ {
assertTableMetaEqualsWithoutKs(sourceKs, targetKs, sourceTb, targetTb, true, true, false); assertTableMetaEqualsWithoutKs(sourceKs, targetKs, sourceTb, targetTb, true, true, false);

View File

@ -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)) 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, tb1));
assertNotNull(getTableMetadata(ks1, "tb3")); assertNotNull(getTableMetadata(ks1, "tb3"));

View File

@ -38,7 +38,6 @@ import static java.lang.String.format;
import static org.apache.cassandra.tools.ToolRunner.invokeNodetool; import static org.apache.cassandra.tools.ToolRunner.invokeNodetool;
import static org.apache.cassandra.utils.JsonUtils.serializeToJsonFile; import static org.apache.cassandra.utils.JsonUtils.serializeToJsonFile;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
public class ExportImportListCompressionDictionaryTest extends CQLTester public class ExportImportListCompressionDictionaryTest extends CQLTester
@ -151,10 +150,9 @@ public class ExportImportListCompressionDictionaryTest extends CQLTester
ToolResult result = invokeNodetool("compressiondictionary", "import", pair.right.absolutePath()); ToolResult result = invokeNodetool("compressiondictionary", "import", pair.right.absolutePath());
Assert.assertEquals(1, result.getExitCode()); 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", assertTrue(result.getStderr().contains(format("Unable to import dictionary JSON: Table %s.%s does not exist or does not support dictionary compression",
keyspace(), keyspace(),
table), table)));
result.getStderr());
} }
private void importDictionary(File dictFile) private void importDictionary(File dictFile)