diff --git a/CHANGES.txt b/CHANGES.txt index 16fe5697cb..c0480d7ad5 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) diff --git a/bin/cqlsh b/bin/cqlsh index b2a729ca02..a556a92a41 100755 --- a/bin/cqlsh +++ b/bin/cqlsh @@ -265,7 +265,11 @@ cqlsh_extra_syntax_rules = r''' ; ::= ( "DESCRIBE" | "DESC" ) - ( "KEYSPACES" + ( "FUNCTIONS" ksname=? + | "FUNCTION" udf= + | "AGGREGATES" ksname=? + | "AGGREGATE" uda= + | "KEYSPACES" | "KEYSPACE" ksname=? | ( "COLUMNFAMILY" | "TABLE" ) cf= | ( "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 + + Output the names of all user defined functions in the given keyspace. + + DESCRIBE FUNCTION [.] + + Describe the given user defined function. + + DESCRIBE AGGREGATES + + Output the names of all user defined aggregates in the given keyspace. + + DESCRIBE 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 diff --git a/lib/cassandra-driver-2.6.0c1.zip b/lib/cassandra-driver-2.6.0c1.zip new file mode 100644 index 0000000000..0e77468cc6 Binary files /dev/null and b/lib/cassandra-driver-2.6.0c1.zip differ 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 ce21a7a761..0000000000 Binary files a/lib/cassandra-driver-internal-only-2.5.1.post0-074650b.zip and /dev/null differ diff --git a/pylib/cqlshlib/cql3handling.py b/pylib/cqlshlib/cql3handling.py index ae66a4e9eb..75b2871bbc 100644 --- a/pylib/cqlshlib/cql3handling.py +++ b/pylib/cqlshlib/cql3handling.py @@ -209,10 +209,20 @@ JUNK ::= /([ \t\r\f\v]+|(--|[/][/])[^\n\r]*([\n\r]|$)|[/][*].*?[*][/])/ ; ::= "{" ":" ( "," ":" )* "}" ; - ::= ( "." )? - ; + ::= ( ksname= dot="." )? udfname= ; - ::= + ::= ( ksname= dot="." )? udfname= ; + + ::= udfname= ; + + ::= ( ksname= dot="." )? udaname= ; + + ::= ( ksname= dot="." )? functionname= ; + + ::= + ; + + ::= | "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''' ::= "CREATE" ("OR" "REPLACE")? "AGGREGATE" ("IF" "NOT" "EXISTS")? - + ( "(" ( ( "," )* )? ")" )? - "SFUNC" + "SFUNC" "STYPE" - ( "FINALFUNC" )? + ( "FINALFUNC" )? ( "INITCOND" )? ; @@ -1078,7 +1151,7 @@ syntax_rules += r''' ::= "DROP" "FUNCTION" ( "IF" "EXISTS" )? ; - ::= "DROP" "AGGREGATE" ( "IF" "EXISTS" )? + ::= "DROP" "AGGREGATE" ( "IF" "EXISTS" )? ; ''' @@ -1246,7 +1319,11 @@ syntax_rules += r''' ; ::= ( "ALL" "FUNCTIONS" ("IN KEYSPACE" )? ) - | ("FUNCTION" ) + | ( "FUNCTION" + ( "(" ( newcol= + ( "," [newcolname]= )* )? + ")" ) + ) ; ''' diff --git a/pylib/cqlshlib/helptopics.py b/pylib/cqlshlib/helptopics.py index b38b235cda..860a58215b 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 )? + ( '.' )? + ( '(' ( ',' )* ')' )? + + 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 )? + ( '.' )? + ( '(' ( ',' )* ')' )? + + 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 ; @@ -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 )? + ( '.' )? + '(' ( ',' )* ')' + SFUNC ( '.' )? + STYPE + ( FINALFUNC ( '.' )? )? + ( INITCOND )? + + 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 , 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 )? + ( '.' )? + '(' ( ',' )* ')' + ( CALLED | RETURNS NULL ) ON NULL INPUT + RETURNS + LANGUAGE + AS + + 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 , 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 ( PRIMARY KEY [, diff --git a/src/java/org/apache/cassandra/cql3/Cql.g b/src/java/org/apache/cassandra/cql3/Cql.g index d49dbd3c24..094b72ec8a 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 filterPermissions(Set permissions, IResource resource) { + if (resource == null) + return Collections.emptySet(); Set filtered = new HashSet<>(permissions); filtered.retainAll(resource.applicablePermissions()); if (filtered.isEmpty())