Update cqlsh for UDFs

patch by Robert Stupp; reviewed by Aleksey Yeschenko for CASSANDRA-7556
This commit is contained in:
Robert Stupp 2015-06-23 19:19:57 +02:00
parent 6e1bdeb142
commit cfab9dac19
7 changed files with 326 additions and 11 deletions

View File

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

116
bin/cqlsh
View File

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

Binary file not shown.

View File

@ -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> ;
<functionName> ::= <userFunctionName>
<userFunctionName> ::= ( ksname=<nonSystemKeyspaceName> dot="." )? udfname=<cfOrKsName> ;
<refUserFunctionName> ::= udfname=<cfOrKsName> ;
<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> )* )?
")" )
)
;
'''

View File

@ -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 [,

View File

@ -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())