mirror of https://github.com/apache/cassandra
CASSANDRA-20943 Introducing comments and security labels for schema elements
This commit is contained in:
parent
dc89b8c802
commit
b11633be4f
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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> )
|
||||||
;
|
;
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
;
|
;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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" +
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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"));
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue