Repository: cassandra Updated Branches: refs/heads/cassandra-2.2 6e1bdeb14 -> cfab9dac1 refs/heads/trunk ddf6c0b45 -> 15b1f9bc4
Update cqlsh for UDFs patch by Robert Stupp; reviewed by Aleksey Yeschenko for CASSANDRA-7556 Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/cfab9dac Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/cfab9dac Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/cfab9dac Branch: refs/heads/cassandra-2.2 Commit: cfab9dac19e85d304810ad47470d995e5b383d77 Parents: 6e1bdeb Author: Robert Stupp <[email protected]> Authored: Tue Jun 23 19:19:57 2015 +0200 Committer: Robert Stupp <[email protected]> Committed: Tue Jun 23 19:19:57 2015 +0200 ---------------------------------------------------------------------- CHANGES.txt | 1 + bin/cqlsh | 116 ++++++++++++++++- lib/cassandra-driver-2.6.0c1.zip | Bin 0 -> 198898 bytes ...driver-internal-only-2.5.1.post0-074650b.zip | Bin 195907 -> 0 bytes pylib/cqlshlib/cql3handling.py | 93 ++++++++++++-- pylib/cqlshlib/helptopics.py | 125 +++++++++++++++++++ src/java/org/apache/cassandra/cql3/Cql.g | 2 + 7 files changed, 326 insertions(+), 11 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cassandra/blob/cfab9dac/CHANGES.txt ---------------------------------------------------------------------- diff --git a/CHANGES.txt b/CHANGES.txt index 16fe569..c0480d7 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 2.2 + * Update cqlsh for UDFs (CASSANDRA-7556) * Change Windows kernel default timer resolution (CASSANDRA-9634) * Deprected sstable2json and json2sstable (CASSANDRA-9618) * Allow native functions in user-defined aggregates (CASSANDRA-9542) http://git-wip-us.apache.org/repos/asf/cassandra/blob/cfab9dac/bin/cqlsh ---------------------------------------------------------------------- diff --git a/bin/cqlsh b/bin/cqlsh index b2a729c..a556a92 100755 --- a/bin/cqlsh +++ b/bin/cqlsh @@ -265,7 +265,11 @@ cqlsh_extra_syntax_rules = r''' ; <describeCommand> ::= ( "DESCRIBE" | "DESC" ) - ( "KEYSPACES" + ( "FUNCTIONS" ksname=<keyspaceName>? + | "FUNCTION" udf=<anyFunctionName> + | "AGGREGATES" ksname=<keyspaceName>? + | "AGGREGATE" uda=<userAggregateName> + | "KEYSPACES" | "KEYSPACE" ksname=<keyspaceName>? | ( "COLUMNFAMILY" | "TABLE" ) cf=<columnFamilyName> | ( "COLUMNFAMILIES" | "TABLES" ) @@ -437,6 +441,12 @@ class VersionNotSupported(Exception): class UserTypeNotFound(Exception): pass +class FunctionNotFound(Exception): + pass + +class AggregateNotFound(Exception): + pass + class DecodeError(Exception): verb = 'decode' @@ -784,6 +794,18 @@ class Shell(cmd.Cmd): return [(field_name, field_type.cql_parameterized_type()) for field_name, field_type in zip(user_type.field_names, user_type.field_types)] + def get_userfunction_names(self, ksname=None): + if ksname is None: + ksname = self.current_keyspace + + return map(lambda f: f.name, self.get_keyspace_meta(ksname).functions.values()) + + def get_useraggregate_names(self, ksname=None): + if ksname is None: + ksname = self.current_keyspace + + return map(lambda f: f.name, self.get_keyspace_meta(ksname).aggregates.values()) + def get_cluster_name(self): return self.conn.metadata.cluster_name @@ -1284,6 +1306,8 @@ class Shell(cmd.Cmd): def describe_columnfamily(self, ksname, cfname): if ksname is None: ksname = self.current_keyspace + if ksname is None: + raise NoKeyspaceError("No keyspace specified and no current keyspace") print self.print_recreate_columnfamily(ksname, cfname, sys.stdout) print @@ -1301,6 +1325,60 @@ class Shell(cmd.Cmd): cmd.Cmd.columnize(self, protect_names(self.get_columnfamily_names(ksname))) print + def describe_functions(self, ksname=None): + print + if ksname is None: + for ksmeta in self.get_keyspaces(): + name = protect_name(ksmeta.name) + print 'Keyspace %s' % (name,) + print '---------%s' % ('-' * len(name)) + cmd.Cmd.columnize(self, protect_names(ksmeta.functions.keys())) + print + else: + ksmeta = self.get_keyspace_meta(ksname) + cmd.Cmd.columnize(self, protect_names(ksmeta.functions.keys())) + print + + def describe_function(self, ksname, functionname): + if ksname is None: + ksname = self.current_keyspace + if ksname is None: + raise NoKeyspaceError("No keyspace specified and no current keyspace") + print + ksmeta = self.get_keyspace_meta(ksname) + functions = filter(lambda f: f.name == functionname, ksmeta.functions.values()) + if len(functions) == 0: + raise FunctionNotFound("User defined function %r not found" % functionname) + print "\n\n".join(func.as_cql_query(formatted=True) for func in functions) + print + + def describe_aggregates(self, ksname=None): + print + if ksname is None: + for ksmeta in self.get_keyspaces(): + name = protect_name(ksmeta.name) + print 'Keyspace %s' % (name,) + print '---------%s' % ('-' * len(name)) + cmd.Cmd.columnize(self, protect_names(ksmeta.aggregates.keys())) + print + else: + ksmeta = self.get_keyspace_meta(ksname) + cmd.Cmd.columnize(self, protect_names(ksmeta.aggregates.keys())) + print + + def describe_aggregate(self, ksname, aggregatename): + if ksname is None: + ksname = self.current_keyspace + if ksname is None: + raise NoKeyspaceError("No keyspace specified and no current keyspace") + print + ksmeta = self.get_keyspace_meta(ksname) + aggregates = filter(lambda f: f.name == aggregatename, ksmeta.aggregates.values()) + if len(aggregates) == 0: + raise FunctionNotFound("User defined aggregate %r not found" % aggregatename) + print "\n\n".join(aggr.as_cql_query(formatted=True) for aggr in aggregates) + print + def describe_usertypes(self, ksname): print if ksname is None: @@ -1318,6 +1396,8 @@ class Shell(cmd.Cmd): def describe_usertype(self, ksname, typename): if ksname is None: ksname = self.current_keyspace + if ksname is None: + raise NoKeyspaceError("No keyspace specified and no current keyspace") print ksmeta = self.get_keyspace_meta(ksname) try: @@ -1395,11 +1475,41 @@ class Shell(cmd.Cmd): Output CQL commands that could be used to recreate the entire (non-system) schema. Works as though "DESCRIBE KEYSPACE k" was invoked for each non-system keyspace k. Use DESCRIBE FULL SCHEMA to include the system keyspaces. + + DESCRIBE FUNCTIONS <keyspace> + + Output the names of all user defined functions in the given keyspace. + + DESCRIBE FUNCTION [<keyspace>.]<function> + + Describe the given user defined function. + + DESCRIBE AGGREGATES <keyspace> + + Output the names of all user defined aggregates in the given keyspace. + + DESCRIBE AGGREGATE [<keyspace>.]<aggregate> + + Describe the given user defined aggregate. """ what = parsed.matched[1][1].lower() - if what == 'keyspaces': + if what == 'functions': + ksname = self.cql_unprotect_name(parsed.get_binding('ksname', None)) + self.describe_functions(ksname) + elif what == 'function': + ksname = self.cql_unprotect_name(parsed.get_binding('ksname', None)) + functionname = self.cql_unprotect_name(parsed.get_binding('udfname')) + self.describe_function(ksname, functionname) + elif what == 'aggregates': + ksname = self.cql_unprotect_name(parsed.get_binding('ksname', None)) + self.describe_aggregates(ksname) + elif what == 'aggregate': + ksname = self.cql_unprotect_name(parsed.get_binding('ksname', None)) + aggregatename = self.cql_unprotect_name(parsed.get_binding('udaname')) + self.describe_aggregate(ksname, aggregatename) + elif what == 'keyspaces': self.describe_keyspaces() - if what == 'keyspace': + elif what == 'keyspace': ksname = self.cql_unprotect_name(parsed.get_binding('ksname', '')) if not ksname: ksname = self.current_keyspace http://git-wip-us.apache.org/repos/asf/cassandra/blob/cfab9dac/lib/cassandra-driver-2.6.0c1.zip ---------------------------------------------------------------------- diff --git a/lib/cassandra-driver-2.6.0c1.zip b/lib/cassandra-driver-2.6.0c1.zip new file mode 100644 index 0000000..0e77468 Binary files /dev/null and b/lib/cassandra-driver-2.6.0c1.zip differ http://git-wip-us.apache.org/repos/asf/cassandra/blob/cfab9dac/lib/cassandra-driver-internal-only-2.5.1.post0-074650b.zip ---------------------------------------------------------------------- diff --git a/lib/cassandra-driver-internal-only-2.5.1.post0-074650b.zip b/lib/cassandra-driver-internal-only-2.5.1.post0-074650b.zip deleted file mode 100644 index ce21a7a..0000000 Binary files a/lib/cassandra-driver-internal-only-2.5.1.post0-074650b.zip and /dev/null differ http://git-wip-us.apache.org/repos/asf/cassandra/blob/cfab9dac/pylib/cqlshlib/cql3handling.py ---------------------------------------------------------------------- diff --git a/pylib/cqlshlib/cql3handling.py b/pylib/cqlshlib/cql3handling.py index ae66a4e..75b2871 100644 --- a/pylib/cqlshlib/cql3handling.py +++ b/pylib/cqlshlib/cql3handling.py @@ -209,10 +209,20 @@ JUNK ::= /([ \t\r\f\v]+|(--|[/][/])[^\n\r]*([\n\r]|$)|[/][*].*?[*][/])/ ; <mapLiteral> ::= "{" <term> ":" <term> ( "," <term> ":" <term> )* "}" ; -<userFunctionName> ::= <identifier> ( "." <identifier> )? - ; +<anyFunctionName> ::= ( ksname=<cfOrKsName> dot="." )? udfname=<cfOrKsName> ; + +<userFunctionName> ::= ( ksname=<nonSystemKeyspaceName> dot="." )? udfname=<cfOrKsName> ; + +<refUserFunctionName> ::= udfname=<cfOrKsName> ; -<functionName> ::= <userFunctionName> +<userAggregateName> ::= ( ksname=<nonSystemKeyspaceName> dot="." )? udaname=<cfOrKsName> ; + +<functionAggregateName> ::= ( ksname=<nonSystemKeyspaceName> dot="." )? functionname=<cfOrKsName> ; + +<aggregateName> ::= <userAggregateName> + ; + +<functionName> ::= <functionAggregateName> | "TOKEN" ; @@ -645,6 +655,69 @@ syntax_rules += r''' ; ''' + + +def udf_name_completer(ctxt, cass): + ks = ctxt.get_binding('ksname', None) + if ks is not None: + ks = dequote_name(ks) + try: + udfnames = cass.get_userfunction_names(ks) + except Exception: + if ks is None: + return () + raise + return map(maybe_escape_name, udfnames) + + +def uda_name_completer(ctxt, cass): + ks = ctxt.get_binding('ksname', None) + if ks is not None: + ks = dequote_name(ks) + try: + udanames = cass.get_useraggregate_names(ks) + except Exception: + if ks is None: + return () + raise + return map(maybe_escape_name, udanames) + + +def udf_uda_name_completer(ctxt, cass): + ks = ctxt.get_binding('ksname', None) + if ks is not None: + ks = dequote_name(ks) + try: + functionnames = cass.get_userfunction_names(ks) + cass.get_useraggregate_names(ks) + except Exception: + if ks is None: + return () + raise + return map(maybe_escape_name, functionnames) + + +def ref_udf_name_completer(ctxt, cass): + try: + udanames = cass.get_userfunction_names(None) + except Exception: + return () + return map(maybe_escape_name, udanames) + + +completer_for('functionAggregateName', 'ksname')(cf_ks_name_completer) +completer_for('functionAggregateName', 'dot')(cf_ks_dot_completer) +completer_for('functionAggregateName', 'functionname')(udf_uda_name_completer) +completer_for('anyFunctionName', 'ksname')(cf_ks_name_completer) +completer_for('anyFunctionName', 'dot')(cf_ks_dot_completer) +completer_for('anyFunctionName', 'udfname')(udf_name_completer) +completer_for('userFunctionName', 'ksname')(cf_ks_name_completer) +completer_for('userFunctionName', 'dot')(cf_ks_dot_completer) +completer_for('userFunctionName', 'udfname')(udf_name_completer) +completer_for('refUserFunctionName', 'udfname')(ref_udf_name_completer) +completer_for('userAggregateName', 'ksname')(cf_ks_dot_completer) +completer_for('userAggregateName', 'dot')(cf_ks_dot_completer) +completer_for('userAggregateName', 'udaname')(uda_name_completer) + @completer_for('orderByClause', 'ordercol') def select_order_column_completer(ctxt, cass): prev_order_cols = ctxt.get_binding('ordercol', ()) @@ -1034,13 +1107,13 @@ syntax_rules += r''' <createAggregateStatement> ::= "CREATE" ("OR" "REPLACE")? "AGGREGATE" ("IF" "NOT" "EXISTS")? - <userFunctionName> + <userAggregateName> ( "(" ( <storageType> ( "," <storageType> )* )? ")" )? - "SFUNC" <identifier> + "SFUNC" <refUserFunctionName> "STYPE" <storageType> - ( "FINALFUNC" <identifier> )? + ( "FINALFUNC" <refUserFunctionName> )? ( "INITCOND" <term> )? ; @@ -1078,7 +1151,7 @@ syntax_rules += r''' <dropFunctionStatement> ::= "DROP" "FUNCTION" ( "IF" "EXISTS" )? <userFunctionName> ; -<dropAggregateStatement> ::= "DROP" "AGGREGATE" ( "IF" "EXISTS" )? <userFunctionName> +<dropAggregateStatement> ::= "DROP" "AGGREGATE" ( "IF" "EXISTS" )? <userAggregateName> ; ''' @@ -1246,7 +1319,11 @@ syntax_rules += r''' ; <functionResource> ::= ( "ALL" "FUNCTIONS" ("IN KEYSPACE" <keyspaceName>)? ) - | ("FUNCTION" <userFunctionName>) + | ( "FUNCTION" <functionAggregateName> + ( "(" ( newcol=<cident> <storageType> + ( "," [newcolname]=<cident> <storageType> )* )? + ")" ) + ) ; ''' http://git-wip-us.apache.org/repos/asf/cassandra/blob/cfab9dac/pylib/cqlshlib/helptopics.py ---------------------------------------------------------------------- diff --git a/pylib/cqlshlib/helptopics.py b/pylib/cqlshlib/helptopics.py index b38b235..860a582 100644 --- a/pylib/cqlshlib/helptopics.py +++ b/pylib/cqlshlib/helptopics.py @@ -183,6 +183,8 @@ class CQLHelpTopics(object): HELP DROP_KEYSPACE; HELP DROP_TABLE; HELP DROP_INDEX; + HELP DROP_FUNCTION; + HELP DROP_AGGREGATE; """ def help_drop_keyspace(self): @@ -211,6 +213,37 @@ class CQLHelpTopics(object): A DROP INDEX statement is used to drop an existing secondary index. """ + def help_drop_function(self): + print """ + DROP FUNCTION ( IF EXISTS )? + ( <keyspace> '.' )? <function-name> + ( '(' <arg-type> ( ',' <arg-type> )* ')' )? + + DROP FUNCTION statement removes a function created using CREATE FUNCTION. + You must specify the argument types (signature) of the function to drop if there + are multiple functions with the same name but a different signature + (overloaded functions). + + DROP FUNCTION with the optional IF EXISTS keywords drops a function if it exists. + """ + + def help_drop_aggregate(self): + print """ + DROP AGGREGATE ( IF EXISTS )? + ( <keyspace> '.' )? <aggregate-name> + ( '(' <arg-type> ( ',' <arg-type> )* ')' )? + + The DROP AGGREGATE statement removes an aggregate created using CREATE AGGREGATE. + You must specify the argument types of the aggregate to drop if there are multiple + aggregates with the same name but a different signature (overloaded aggregates). + + DROP AGGREGATE with the optional IF EXISTS keywords drops an aggregate if it exists, + and does nothing if a function with the signature does not exist. + + Signatures for user-defined aggregates follow the same rules as for + user-defined functions. + """ + def help_truncate(self): print """ TRUNCATE <tablename>; @@ -227,6 +260,8 @@ class CQLHelpTopics(object): HELP CREATE_KEYSPACE; HELP CREATE_TABLE; HELP CREATE_INDEX; + HELP CREATE_FUNCTION; + HELP CREATE_AGGREGATE; """ def help_use(self): @@ -243,6 +278,96 @@ class CQLHelpTopics(object): number, it can be quoted using double quotes. """ + def help_create_aggregate(self): + print """ + CREATE ( OR REPLACE )? AGGREGATE ( IF NOT EXISTS )? + ( <keyspace> '.' )? <aggregate-name> + '(' <arg-type> ( ',' <arg-type> )* ')' + SFUNC ( <keyspace> '.' )? <state-functionname> + STYPE <state-type> + ( FINALFUNC ( <keyspace> '.' )? <final-functionname> )? + ( INITCOND <init-cond> )? + + CREATE AGGREGATE creates or replaces a user-defined aggregate. + + CREATE AGGREGATE with the optional OR REPLACE keywords either creates an aggregate + or replaces an existing one with the same signature. A CREATE AGGREGATE without + OR REPLACE fails if an aggregate with the same signature already exists. + + CREATE AGGREGATE with the optional IF NOT EXISTS keywords either creates an aggregate + if it does not already exist. + + OR REPLACE and IF NOT EXIST cannot be used together. + + Aggregates belong to a keyspace. If no keyspace is specified in <aggregate-name>, the + current keyspace is used (i.e. the keyspace specified using the USE statement). It is + not possible to create a user-defined aggregate in one of the system keyspaces. + + Signatures for user-defined aggregates follow the same rules as for + user-defined functions. + + STYPE defines the type of the state value and must be specified. + + The optional INITCOND defines the initial state value for the aggregate. It defaults + to null. A non-null INITCOND must be specified for state functions that are declared + with RETURNS NULL ON NULL INPUT. + + SFUNC references an existing function to be used as the state modifying function. The + type of first argument of the state function must match STYPE. The remaining argument + types of the state function must match the argument types of the aggregate function. + State is not updated for state functions declared with RETURNS NULL ON NULL INPUT and + called with null. + + The optional FINALFUNC is called just before the aggregate result is returned. It must + take only one argument with type STYPE. The return type of the FINALFUNC may be a + different type. A final function declared with RETURNS NULL ON NULL INPUT means that + the aggregate's return value will be null, if the last state is null. + + If no FINALFUNC is defined, the overall return type of the aggregate function is STYPE. + If a FINALFUNC is defined, it is the return type of that function. + """ + + def help_create_function(self): + print """ + CREATE ( OR REPLACE )? FUNCTION ( IF NOT EXISTS )? + ( <keyspace> '.' )? <function-name> + '(' <arg-name> <arg-type> ( ',' <arg-name> <arg-type> )* ')' + ( CALLED | RETURNS NULL ) ON NULL INPUT + RETURNS <type> + LANGUAGE <language> + AS <body> + + CREATE FUNCTION creates or replaces a user-defined function. + + Signatures are used to distinguish individual functions. The signature consists of: + + The fully qualified function name - i.e keyspace plus function-name + The concatenated list of all argument types + + Note that keyspace names, function names and argument types are subject to the default + naming conventions and case-sensitivity rules. + + CREATE FUNCTION with the optional OR REPLACE keywords either creates a function or + replaces an existing one with the same signature. A CREATE FUNCTION without OR REPLACE + fails if a function with the same signature already exists. + + Behavior on invocation with null values must be defined for each function. There are + two options: + + RETURNS NULL ON NULL INPUT declares that the function will always return null if any + of the input arguments is null. CALLED ON NULL INPUT declares that the function will + always be executed. + + If the optional IF NOT EXISTS keywords are used, the function will only be created if + another function with the same signature does not exist. + + OR REPLACE and IF NOT EXIST cannot be used together. + + Functions belong to a keyspace. If no keyspace is specified in <function-name>, the + current keyspace is used (i.e. the keyspace specified using the USE statement). + It is not possible to create a user-defined function in one of the system keyspaces. + """ + def help_create_table(self): print """ CREATE TABLE <cfname> ( <colname> <type> PRIMARY KEY [, http://git-wip-us.apache.org/repos/asf/cassandra/blob/cfab9dac/src/java/org/apache/cassandra/cql3/Cql.g ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/Cql.g b/src/java/org/apache/cassandra/cql3/Cql.g index d49dbd3..094b72e 100644 --- a/src/java/org/apache/cassandra/cql3/Cql.g +++ b/src/java/org/apache/cassandra/cql3/Cql.g @@ -171,6 +171,8 @@ options { public Set<Permission> filterPermissions(Set<Permission> permissions, IResource resource) { + if (resource == null) + return Collections.emptySet(); Set<Permission> filtered = new HashSet<>(permissions); filtered.retainAll(resource.applicablePermissions()); if (filtered.isEmpty())
