# 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. from cassandra.metadata import maybe_escape_name from cqlshlib import helptopics from cqlshlib.cqlhandling import CqlParsingRuleSet, Hint simple_cql_types = {'ascii', 'bigint', 'blob', 'boolean', 'counter', 'date', 'decimal', 'double', 'duration', 'float', 'inet', 'int', 'smallint', 'text', 'time', 'timestamp', 'timeuuid', 'tinyint', 'uuid', 'varchar', 'varint'} simple_cql_types.difference_update(('set', 'map', 'list')) cqldocs = helptopics.CQL3HelpTopics() class UnexpectedTableStructure(UserWarning): def __init__(self, msg): self.msg = msg def __str__(self): return 'Unexpected table structure; may not translate correctly to CQL. ' + self.msg SYSTEM_KEYSPACES = ('system', 'system_schema', 'system_traces', 'system_auth', 'system_distributed', 'system_views', 'system_virtual_schema') NONALTERBALE_KEYSPACES = ('system', 'system_schema', 'system_views', 'system_virtual_schema') class Cql3ParsingRuleSet(CqlParsingRuleSet): columnfamily_layout_options = ( ('bloom_filter_fp_chance', None), ('comment', None), ('gc_grace_seconds', None), ('min_index_interval', None), ('max_index_interval', None), ('default_time_to_live', None), ('speculative_retry', None), ('additional_write_policy', None), ('memtable_flush_period_in_ms', None), ('cdc', None), ('read_repair', None), ) columnfamily_layout_map_options = ( # (CQL3 option name, schema_columnfamilies column name (or None if same), # list of known map keys) ('compaction', 'compaction_strategy_options', ('class', 'max_threshold', 'tombstone_compaction_interval', 'tombstone_threshold', 'enabled', 'unchecked_tombstone_compaction', 'only_purge_repaired_tombstones', 'provide_overlapping_tombstones')), ('compression', 'compression_parameters', ('sstable_compression', 'chunk_length_kb', 'crc_check_chance')), ('caching', None, ('rows_per_partition', 'keys')), ) obsolete_cf_options = () consistency_levels = ( 'ANY', 'ONE', 'TWO', 'THREE', 'QUORUM', 'ALL', 'LOCAL_QUORUM', 'EACH_QUORUM', 'SERIAL' ) size_tiered_compaction_strategy_options = ( 'min_sstable_size', 'min_threshold', 'bucket_high', 'bucket_low' ) leveled_compaction_strategy_options = ( 'sstable_size_in_mb', 'fanout_size' ) date_tiered_compaction_strategy_options = ( 'base_time_seconds', 'max_sstable_age_days', 'min_threshold', 'max_window_size_seconds', 'timestamp_resolution' ) time_window_compaction_strategy_options = ( 'compaction_window_unit', 'compaction_window_size', 'min_threshold', 'timestamp_resolution' ) @classmethod def escape_value(cls, value): if value is None: return 'NULL' # this totally won't work if isinstance(value, bool): value = str(value).lower() elif isinstance(value, float): return '%f' % value elif isinstance(value, int): return str(value) return "'%s'" % value.replace("'", "''") @classmethod def escape_name(cls, name): if name is None: return 'NULL' return "'%s'" % name.replace("'", "''") @staticmethod def dequote_name(name): name = name.strip() if name == '': return name if name[0] == '"' and name[-1] == '"': return name[1:-1].replace('""', '"') else: return name.lower() @staticmethod def dequote_value(cqlword): cqlword = cqlword.strip() if cqlword == '': return cqlword if cqlword[0] == "'" and cqlword[-1] == "'": cqlword = cqlword[1:-1].replace("''", "'") return cqlword CqlRuleSet = Cql3ParsingRuleSet() # convenience for remainder of module completer_for = CqlRuleSet.completer_for explain_completion = CqlRuleSet.explain_completion dequote_value = CqlRuleSet.dequote_value dequote_name = CqlRuleSet.dequote_name escape_value = CqlRuleSet.escape_value # BEGIN SYNTAX/COMPLETION RULE DEFINITIONS syntax_rules = r''' ::= * ; ::= [statements]= ";" ; # the order of these terminal productions is significant: ::= /\n/ ; JUNK ::= /([ \t\r\f\v]+|(--|[/][/])[^\n\r]*([\n\r]|$)|[/][*].*?[*][/])/ ; ::= | ; ::= /'([^']|'')*'/ ; ::= /\$\$(?:(?!\$\$).)*\$\$/; ::= /"([^"]|"")*"/ ; ::= /-?[0-9]+\.[0-9]+/ ; ::= /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/ ; ::= /0x[0-9a-f]+/ ; ::= /[0-9]+/ ; ::= /[a-z][a-z0-9_]*/ ; ::= ":" ; ::= "*" ; ::= ";" ; ::= /[-+=%/,().]/ ; ::= /[<>!]=?/ ; ::= /[][{}]/ ; ::= "-"? ; ::= "true" | "false" ; ::= /\$\$(?:(?!\$\$).)*/ ; ::= /'([^']|'')*/ ; ::= /"([^"]|"")*/ ; ::= /[/][*].*$/ ; ::= | | | | | | | | "NULL" ; ::= ( ( "." )?) | "TOKEN" ; ::= "(" ( ( "," )* )? ")" ; ::= token="TOKEN" "(" ( "," )* ")" | ; ::= | | ; ::= ; # just an alias ::= | | ; ::= "[" ( ( "," )* )? "]" ; ::= "{" ( ( "," )* )? "}" ; ::= "{" ":" ( "," ":" )* "}" ; ::= ( ksname= dot="." )? udfname= ; ::= ( ksname= dot="." )? udfname= ; ::= udfname= ; ::= ( ksname= dot="." )? udaname= ; ::= ( ksname= dot="." )? functionname= ; ::= ; ::= | "TOKEN" ; ::= | | | | | ; ::= | | | | ; ::= | | | | | | | | | | | | | | | | | | ; ::= | | | | | | | ; ::= | | | | ; # timestamp is included here, since it's also a keyword ::= typename=( | | "timestamp" ) ; ::= utname= ; ::= | | | ; # Note: autocomplete for frozen collection types does not handle nesting past depth 1 properly, # but that's a lot of work to fix for little benefit. ::= "map" "<" "," ( | ) ">" | "list" "<" ( | ) ">" | "set" "<" ( | ) ">" ; ::= "frozen" "<" "map" "<" "," ">" ">" | "frozen" "<" "list" "<" ">" ">" | "frozen" "<" "set" "<" ">" ">" ; ::= ( ksname= dot="." )? cfname= ; ::= ( ksname= dot="." )? mvname= ; ::= ( ksname= dot="." )? utname= ; ::= ksname= ; ::= ksname= ; ::= ksname= ; ::= | | ; ::= nocomplete= ( "key" | "clustering" # | "count" -- to get count(*) completion, treat count as reserved | "ttl" | "compact" | "storage" | "type" | "values" ) ; ::= [propname]= propeq="=" [propval]= ; ::= propsimpleval=( | | | | ) # we don't use here so we can get more targeted # completions: | propsimpleval="{" [propmapkey]= ":" [propmapval]= ( ender="," [propmapkey]= ":" [propmapval]= )* ender="}" ; ''' def prop_equals_completer(ctxt, cass): if not working_on_keyspace(ctxt): # we know if the thing in the property name position is "compact" or # "clustering" that there won't actually be an equals sign, because # there are no properties by those names. there are, on the other hand, # table properties that start with those keywords which don't have # equals signs at all. curprop = ctxt.get_binding('propname')[-1].upper() if curprop in ('COMPACT', 'CLUSTERING'): return () return ['='] completer_for('property', 'propeq')(prop_equals_completer) @completer_for('property', 'propname') def prop_name_completer(ctxt, cass): if working_on_keyspace(ctxt): return ks_prop_name_completer(ctxt, cass) elif 'MATERIALIZED' == ctxt.get_binding('wat', '').upper(): props = cf_prop_name_completer(ctxt, cass) props.remove('default_time_to_live') props.remove('gc_grace_seconds') return props else: return cf_prop_name_completer(ctxt, cass) @completer_for('propertyValue', 'propsimpleval') def prop_val_completer(ctxt, cass): if working_on_keyspace(ctxt): return ks_prop_val_completer(ctxt, cass) else: return cf_prop_val_completer(ctxt, cass) @completer_for('propertyValue', 'propmapkey') def prop_val_mapkey_completer(ctxt, cass): if working_on_keyspace(ctxt): return ks_prop_val_mapkey_completer(ctxt, cass) else: return cf_prop_val_mapkey_completer(ctxt, cass) @completer_for('propertyValue', 'propmapval') def prop_val_mapval_completer(ctxt, cass): if working_on_keyspace(ctxt): return ks_prop_val_mapval_completer(ctxt, cass) else: return cf_prop_val_mapval_completer(ctxt, cass) @completer_for('propertyValue', 'ender') def prop_val_mapender_completer(ctxt, cass): if working_on_keyspace(ctxt): return ks_prop_val_mapender_completer(ctxt, cass) else: return cf_prop_val_mapender_completer(ctxt, cass) def ks_prop_name_completer(ctxt, cass): optsseen = ctxt.get_binding('propname', ()) if 'replication' not in optsseen: return ['replication'] return ["durable_writes"] def ks_prop_val_completer(ctxt, cass): optname = ctxt.get_binding('propname')[-1] if optname == 'durable_writes': return ["'true'", "'false'"] if optname == 'replication': return ["{'class': '"] return () def ks_prop_val_mapkey_completer(ctxt, cass): optname = ctxt.get_binding('propname')[-1] if optname != 'replication': return () keysseen = list(map(dequote_value, ctxt.get_binding('propmapkey', ()))) valsseen = list(map(dequote_value, ctxt.get_binding('propmapval', ()))) for k, v in zip(keysseen, valsseen): if k == 'class': repclass = v break else: return ["'class'"] if repclass == 'SimpleStrategy': opts = {'replication_factor'} elif repclass == 'NetworkTopologyStrategy': return [Hint('')] return list(map(escape_value, opts.difference(keysseen))) def ks_prop_val_mapval_completer(ctxt, cass): optname = ctxt.get_binding('propname')[-1] if optname != 'replication': return () currentkey = dequote_value(ctxt.get_binding('propmapkey')[-1]) if currentkey == 'class': return list(map(escape_value, CqlRuleSet.replication_strategies)) return [Hint('')] def ks_prop_val_mapender_completer(ctxt, cass): optname = ctxt.get_binding('propname')[-1] if optname != 'replication': return [','] keysseen = list(map(dequote_value, ctxt.get_binding('propmapkey', ()))) valsseen = list(map(dequote_value, ctxt.get_binding('propmapval', ()))) for k, v in zip(keysseen, valsseen): if k == 'class': repclass = v break else: return [','] if repclass == 'SimpleStrategy': if 'replication_factor' not in keysseen: return [','] if repclass == 'NetworkTopologyStrategy' and len(keysseen) == 1: return [','] return ['}'] def cf_prop_name_completer(ctxt, cass): return [c[0] for c in (CqlRuleSet.columnfamily_layout_options + CqlRuleSet.columnfamily_layout_map_options)] def cf_prop_val_completer(ctxt, cass): exist_opts = ctxt.get_binding('propname') this_opt = exist_opts[-1] if this_opt == 'compression': return ["{'sstable_compression': '"] if this_opt == 'compaction': return ["{'class': '"] if this_opt == 'caching': return ["{'keys': '"] if any(this_opt == opt[0] for opt in CqlRuleSet.obsolete_cf_options): return ["''"] if this_opt == 'bloom_filter_fp_chance': return [Hint('')] if this_opt in ('min_compaction_threshold', 'max_compaction_threshold', 'gc_grace_seconds', 'min_index_interval', 'max_index_interval'): return [Hint('')] if this_opt in ('cdc'): return [Hint('')] if this_opt in ('read_repair'): return [Hint('<\'none\'|\'blocking\'>')] return [Hint('')] def cf_prop_val_mapkey_completer(ctxt, cass): optname = ctxt.get_binding('propname')[-1] for cql3option, _, subopts in CqlRuleSet.columnfamily_layout_map_options: if optname == cql3option: break else: return () keysseen = list(map(dequote_value, ctxt.get_binding('propmapkey', ()))) valsseen = list(map(dequote_value, ctxt.get_binding('propmapval', ()))) pairsseen = dict(list(zip(keysseen, valsseen))) if optname == 'compression': return list(map(escape_value, set(subopts).difference(keysseen))) if optname == 'caching': return list(map(escape_value, set(subopts).difference(keysseen))) if optname == 'compaction': opts = set(subopts) try: csc = pairsseen['class'] except KeyError: return ["'class'"] csc = csc.split('.')[-1] if csc == 'SizeTieredCompactionStrategy': opts = opts.union(set(CqlRuleSet.size_tiered_compaction_strategy_options)) elif csc == 'LeveledCompactionStrategy': opts = opts.union(set(CqlRuleSet.leveled_compaction_strategy_options)) elif csc == 'DateTieredCompactionStrategy': opts = opts.union(set(CqlRuleSet.date_tiered_compaction_strategy_options)) elif csc == 'TimeWindowCompactionStrategy': opts = opts.union(set(CqlRuleSet.time_window_compaction_strategy_options)) return list(map(escape_value, opts)) return () def cf_prop_val_mapval_completer(ctxt, cass): opt = ctxt.get_binding('propname')[-1] key = dequote_value(ctxt.get_binding('propmapkey')[-1]) if opt == 'compaction': if key == 'class': return list(map(escape_value, CqlRuleSet.available_compaction_classes)) if key == 'provide_overlapping_tombstones': return [Hint('')] return [Hint('')] elif opt == 'compression': if key == 'sstable_compression': return list(map(escape_value, CqlRuleSet.available_compression_classes)) return [Hint('')] elif opt == 'caching': if key == 'rows_per_partition': return ["'ALL'", "'NONE'", Hint('#rows_per_partition')] elif key == 'keys': return ["'ALL'", "'NONE'"] return () def cf_prop_val_mapender_completer(ctxt, cass): return [',', '}'] @completer_for('tokenDefinition', 'token') def token_word_completer(ctxt, cass): return ['token('] @completer_for('simpleStorageType', 'typename') def storagetype_completer(ctxt, cass): return simple_cql_types @completer_for('keyspaceName', 'ksname') def ks_name_completer(ctxt, cass): return list(map(maybe_escape_name, cass.get_keyspace_names())) @completer_for('nonSystemKeyspaceName', 'ksname') def non_system_ks_name_completer(ctxt, cass): ksnames = [n for n in cass.get_keyspace_names() if n not in SYSTEM_KEYSPACES] return list(map(maybe_escape_name, ksnames)) @completer_for('alterableKeyspaceName', 'ksname') def alterable_ks_name_completer(ctxt, cass): ksnames = [n for n in cass.get_keyspace_names() if n not in NONALTERBALE_KEYSPACES] return list(map(maybe_escape_name, ksnames)) def cf_ks_name_completer(ctxt, cass): return [maybe_escape_name(ks) + '.' for ks in cass.get_keyspace_names()] completer_for('columnFamilyName', 'ksname')(cf_ks_name_completer) completer_for('materializedViewName', 'ksname')(cf_ks_name_completer) def cf_ks_dot_completer(ctxt, cass): name = dequote_name(ctxt.get_binding('ksname')) if name in cass.get_keyspace_names(): return ['.'] return [] completer_for('columnFamilyName', 'dot')(cf_ks_dot_completer) completer_for('materializedViewName', 'dot')(cf_ks_dot_completer) @completer_for('columnFamilyName', 'cfname') def cf_name_completer(ctxt, cass): ks = ctxt.get_binding('ksname', None) if ks is not None: ks = dequote_name(ks) try: cfnames = cass.get_columnfamily_names(ks) except Exception: if ks is None: return () raise return list(map(maybe_escape_name, cfnames)) @completer_for('materializedViewName', 'mvname') def mv_name_completer(ctxt, cass): ks = ctxt.get_binding('ksname', None) if ks is not None: ks = dequote_name(ks) try: mvnames = cass.get_materialized_view_names(ks) except Exception: if ks is None: return () raise return list(map(maybe_escape_name, mvnames)) completer_for('userTypeName', 'ksname')(cf_ks_name_completer) completer_for('userTypeName', 'dot')(cf_ks_dot_completer) def ut_name_completer(ctxt, cass): ks = ctxt.get_binding('ksname', None) if ks is not None: ks = dequote_name(ks) try: utnames = cass.get_usertype_names(ks) except Exception: if ks is None: return () raise return list(map(maybe_escape_name, utnames)) completer_for('userTypeName', 'utname')(ut_name_completer) completer_for('userType', 'utname')(ut_name_completer) @completer_for('unreservedKeyword', 'nocomplete') def unreserved_keyword_completer(ctxt, cass): # we never want to provide completions through this production; # this is always just to allow use of some keywords as column # names, CF names, property values, etc. return () def get_table_meta(ctxt, cass): ks = ctxt.get_binding('ksname', None) if ks is not None: ks = dequote_name(ks) cf = dequote_name(ctxt.get_binding('cfname')) return cass.get_table_meta(ks, cf) def get_ut_layout(ctxt, cass): ks = ctxt.get_binding('ksname', None) if ks is not None: ks = dequote_name(ks) ut = dequote_name(ctxt.get_binding('utname')) return cass.get_usertype_layout(ks, ut) def working_on_keyspace(ctxt): wat = ctxt.get_binding('wat', '').upper() if wat in ('KEYSPACE', 'SCHEMA'): return True return False syntax_rules += r''' ::= "USE" ; ::= "SELECT" ( "JSON" )? "FROM" (cf= | mv=) ( "WHERE" )? ( "GROUP" "BY" ( "," )* )? ( "ORDER" "BY" ( "," )* )? ( "PER" "PARTITION" "LIMIT" perPartitionLimit= )? ( "LIMIT" limit= )? ( "ALLOW" "FILTERING" )? ; ::= ( "AND" )* ; ::= [rel_lhs]= ( "[" "]" )? ( "=" | "<" | ">" | "<=" | ">=" | "CONTAINS" ( "KEY" )? ) | token="TOKEN" "(" [rel_tokname]= ( "," [rel_tokname]= )* ")" ("=" | "<" | ">" | "<=" | ">=") | [rel_lhs]= "IN" "(" ( "," )* ")" ; ::= "DISTINCT"? ("AS" )? ("," ("AS" )?)* | "*" ; ::= "." ; ::= [colname]= ( "[" ( ( ".." "]" )? | ".." ) )? | | "WRITETIME" "(" [colname]= ")" | "TTL" "(" [colname]= ")" | "COUNT" "(" star=( "*" | "1" ) ")" | "CAST" "(" "AS" ")" | | ; ::= "(" ( ( "," )* )? ")" ; ::= [ordercol]= ( "ASC" | "DESC" )? ; ::= [groupcol]= | ; ::= "(" ( ( "," )* )? ")" ; ::= [groupcol]= | ; ''' 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 list(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 list(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 list(map(maybe_escape_name, functionnames)) def ref_udf_name_completer(ctxt, cass): try: udanames = cass.get_userfunction_names(None) except Exception: return () return list(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_name_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', ()) keyname = ctxt.get_binding('keyname') if keyname is None: keyname = ctxt.get_binding('rel_lhs', ()) if not keyname: return [Hint("Can't ORDER BY here: need to specify partition key in WHERE clause")] layout = get_table_meta(ctxt, cass) order_by_candidates = [col.name for col in layout.clustering_key] if len(order_by_candidates) > len(prev_order_cols): return [maybe_escape_name(order_by_candidates[len(prev_order_cols)])] return [Hint('No more orderable columns here.')] @completer_for('groupByClause', 'groupcol') def select_group_column_completer(ctxt, cass): prev_group_cols = ctxt.get_binding('groupcol', ()) layout = get_table_meta(ctxt, cass) group_by_candidates = [col.name for col in layout.primary_key] if len(group_by_candidates) > len(prev_group_cols): return [maybe_escape_name(group_by_candidates[len(prev_group_cols)])] return [Hint('No more columns here.')] @completer_for('relation', 'token') def relation_token_word_completer(ctxt, cass): return ['TOKEN('] @completer_for('relation', 'rel_tokname') def relation_token_subject_completer(ctxt, cass): layout = get_table_meta(ctxt, cass) return [key.name for key in layout.partition_key] @completer_for('relation', 'rel_lhs') def select_relation_lhs_completer(ctxt, cass): layout = get_table_meta(ctxt, cass) filterable = set() already_filtered_on = list(map(dequote_name, ctxt.get_binding('rel_lhs', ()))) for num in range(0, len(layout.partition_key)): if num == 0 or layout.partition_key[num - 1].name in already_filtered_on: filterable.add(layout.partition_key[num].name) else: break for num in range(0, len(layout.clustering_key)): if num == 0 or layout.clustering_key[num - 1].name in already_filtered_on: filterable.add(layout.clustering_key[num].name) else: break for idx in layout.indexes.values(): filterable.add(idx.index_options["target"]) return list(map(maybe_escape_name, filterable)) explain_completion('selector', 'colname') syntax_rules += r''' ::= "INSERT" "INTO" cf= ( ( "(" [colname]= ( "," [colname]= )* ")" "VALUES" "(" [newval]= ( valcomma="," [newval]= )* valcomma=")") | ("JSON" )) ( "IF" "NOT" "EXISTS")? ( "USING" [insertopt]= ( "AND" [insertopt]= )* )? ; ::= "TIMESTAMP" | "TTL" ; ''' def regular_column_names(table_meta): if not table_meta or not table_meta.columns: return [] regular_columns = list(set(table_meta.columns.keys()) - set([key.name for key in table_meta.partition_key]) - set([key.name for key in table_meta.clustering_key])) return regular_columns @completer_for('insertStatement', 'colname') def insert_colname_completer(ctxt, cass): layout = get_table_meta(ctxt, cass) colnames = set(map(dequote_name, ctxt.get_binding('colname', ()))) keycols = layout.primary_key for k in keycols: if k.name not in colnames: return [maybe_escape_name(k.name)] normalcols = set(regular_column_names(layout)) - colnames return list(map(maybe_escape_name, normalcols)) @completer_for('insertStatement', 'newval') def insert_newval_completer(ctxt, cass): layout = get_table_meta(ctxt, cass) insertcols = list(map(dequote_name, ctxt.get_binding('colname'))) valuesdone = ctxt.get_binding('newval', ()) if len(valuesdone) >= len(insertcols): return [] curcol = insertcols[len(valuesdone)] coltype = layout.columns[curcol].cql_type if coltype in ('map', 'set'): return ['{'] if coltype == 'list': return ['['] if coltype == 'boolean': return ['true', 'false'] return [Hint('' % (maybe_escape_name(curcol), coltype))] @completer_for('insertStatement', 'valcomma') def insert_valcomma_completer(ctxt, cass): numcols = len(ctxt.get_binding('colname', ())) numvals = len(ctxt.get_binding('newval', ())) if numcols > numvals: return [','] return [')'] @completer_for('insertStatement', 'insertopt') def insert_option_completer(ctxt, cass): opts = set('TIMESTAMP TTL'.split()) for opt in ctxt.get_binding('insertopt', ()): opts.discard(opt.split()[0]) return opts syntax_rules += r''' ::= "UPDATE" cf= ( "USING" [updateopt]= ( "AND" [updateopt]= )* )? "SET" ( "," )* "WHERE" ( "IF" ( "EXISTS" | ))? ; ::= updatecol= (( "=" update_rhs=( | ) ( counterop=( "+" | "-" ) inc= | listadder="+" listcol= )? ) | ( indexbracket="[" "]" "=" ) | ( udt_field_dot="." udt_field= "=" )) ; ::= ( "AND" )* ; ::= (("=" | "<" | ">" | "<=" | ">=" | "!=" | "CONTAINS" ( "KEY" )? ) ) | ("IN" "(" ( "," )* ")" ) ; ::= conditioncol= ( (( indexbracket="[" "]" ) |( udt_field_dot="." udt_field= )) )? ; ''' @completer_for('updateStatement', 'updateopt') def update_option_completer(ctxt, cass): opts = set('TIMESTAMP TTL'.split()) for opt in ctxt.get_binding('updateopt', ()): opts.discard(opt.split()[0]) return opts @completer_for('assignment', 'updatecol') def update_col_completer(ctxt, cass): layout = get_table_meta(ctxt, cass) return list(map(maybe_escape_name, regular_column_names(layout))) @completer_for('assignment', 'update_rhs') def update_countername_completer(ctxt, cass): layout = get_table_meta(ctxt, cass) curcol = dequote_name(ctxt.get_binding('updatecol', '')) coltype = layout.columns[curcol].cql_type if coltype == 'counter': return [maybe_escape_name(curcol)] if coltype in ('map', 'set'): return ["{"] if coltype == 'list': return ["["] return [Hint('' % coltype)] @completer_for('assignment', 'counterop') def update_counterop_completer(ctxt, cass): layout = get_table_meta(ctxt, cass) curcol = dequote_name(ctxt.get_binding('updatecol', '')) return ['+', '-'] if layout.columns[curcol].cql_type == 'counter' else [] @completer_for('assignment', 'inc') def update_counter_inc_completer(ctxt, cass): layout = get_table_meta(ctxt, cass) curcol = dequote_name(ctxt.get_binding('updatecol', '')) if layout.columns[curcol].cql_type == 'counter': return [Hint('')] return [] @completer_for('assignment', 'listadder') def update_listadder_completer(ctxt, cass): rhs = ctxt.get_binding('update_rhs') if rhs.startswith('['): return ['+'] return [] @completer_for('assignment', 'listcol') def update_listcol_completer(ctxt, cass): rhs = ctxt.get_binding('update_rhs') if rhs.startswith('['): colname = dequote_name(ctxt.get_binding('updatecol')) return [maybe_escape_name(colname)] return [] @completer_for('assignment', 'indexbracket') def update_indexbracket_completer(ctxt, cass): layout = get_table_meta(ctxt, cass) curcol = dequote_name(ctxt.get_binding('updatecol', '')) coltype = layout.columns[curcol].cql_type if coltype in ('map', 'list'): return ['['] return [] @completer_for('assignment', 'udt_field_dot') def update_udt_field_dot_completer(ctxt, cass): layout = get_table_meta(ctxt, cass) curcol = dequote_name(ctxt.get_binding('updatecol', '')) return ["."] if _is_usertype(layout, curcol) else [] @completer_for('assignment', 'udt_field') def assignment_udt_field_completer(ctxt, cass): layout = get_table_meta(ctxt, cass) curcol = dequote_name(ctxt.get_binding('updatecol', '')) return _usertype_fields(ctxt, cass, layout, curcol) def _is_usertype(layout, curcol): coltype = layout.columns[curcol].cql_type return coltype not in simple_cql_types and coltype not in ('map', 'set', 'list') def _usertype_fields(ctxt, cass, layout, curcol): if not _is_usertype(layout, curcol): return [] coltype = layout.columns[curcol].cql_type ks = ctxt.get_binding('ksname', None) if ks is not None: ks = dequote_name(ks) user_type = cass.get_usertype_layout(ks, coltype) return [field_name for (field_name, field_type) in user_type] @completer_for('condition', 'indexbracket') def condition_indexbracket_completer(ctxt, cass): layout = get_table_meta(ctxt, cass) curcol = dequote_name(ctxt.get_binding('conditioncol', '')) coltype = layout.columns[curcol].cql_type if coltype in ('map', 'list'): return ['['] return [] @completer_for('condition', 'udt_field_dot') def condition_udt_field_dot_completer(ctxt, cass): layout = get_table_meta(ctxt, cass) curcol = dequote_name(ctxt.get_binding('conditioncol', '')) return ["."] if _is_usertype(layout, curcol) else [] @completer_for('condition', 'udt_field') def condition_udt_field_completer(ctxt, cass): layout = get_table_meta(ctxt, cass) curcol = dequote_name(ctxt.get_binding('conditioncol', '')) return _usertype_fields(ctxt, cass, layout, curcol) syntax_rules += r''' ::= "DELETE" ( ( "," )* )? "FROM" cf= ( "USING" [delopt]= )? "WHERE" ( "IF" ( "EXISTS" | ) )? ; ::= delcol= ( ( "[" "]" ) | ( "." ) )? ; ::= "TIMESTAMP" ; ''' @completer_for('deleteStatement', 'delopt') def delete_opt_completer(ctxt, cass): opts = set('TIMESTAMP'.split()) for opt in ctxt.get_binding('delopt', ()): opts.discard(opt.split()[0]) return opts @completer_for('deleteSelector', 'delcol') def delete_delcol_completer(ctxt, cass): layout = get_table_meta(ctxt, cass) return list(map(maybe_escape_name, regular_column_names(layout))) syntax_rules += r''' ::= "BEGIN" ( "UNLOGGED" | "COUNTER" )? "BATCH" ( "USING" [batchopt]= ( "AND" [batchopt]= )* )? [batchstmt]= ";"? ( [batchstmt]= ";"? )* "APPLY" "BATCH" ; ::= | | ; ''' @completer_for('batchStatement', 'batchopt') def batch_opt_completer(ctxt, cass): opts = set('TIMESTAMP'.split()) for opt in ctxt.get_binding('batchopt', ()): opts.discard(opt.split()[0]) return opts syntax_rules += r''' ::= "TRUNCATE" ("COLUMNFAMILY" | "TABLE")? cf= ; ''' syntax_rules += r''' ::= "CREATE" wat=( "KEYSPACE" | "SCHEMA" ) ("IF" "NOT" "EXISTS")? ksname= "WITH" ( "AND" )* ; ''' @completer_for('createKeyspaceStatement', 'wat') def create_ks_wat_completer(ctxt, cass): # would prefer to get rid of the "schema" nomenclature in cql3 if ctxt.get_binding('partial', '') == '': return ['KEYSPACE'] return ['KEYSPACE', 'SCHEMA'] syntax_rules += r''' ::= "CREATE" wat=( "COLUMNFAMILY" | "TABLE" ) ("IF" "NOT" "EXISTS")? ( ks= dot="." )? cf= "(" ( | ) ")" ( "WITH" ( "AND" )* )? ; ::= | "COMPACT" "STORAGE" "CDC" | "CLUSTERING" "ORDER" "BY" "(" ( "," )* ")" ; ::= [ordercol]= ( "ASC" | "DESC" ) ; ::= [newcolname]= "PRIMARY" "KEY" ( "," [newcolname]= )* ; ::= [newcolname]= "," [newcolname]= ( "static" )? ( "," [newcolname]= ( "static" )? )* "," "PRIMARY" k="KEY" p="(" ( partkey= | [pkey]= ) ( c="," [pkey]= )* ")" ; ::= "(" [ptkey]= "," [ptkey]= ( "," [ptkey]= )* ")" ; ''' @completer_for('cfamOrdering', 'ordercol') def create_cf_clustering_order_colname_completer(ctxt, cass): colnames = list(map(dequote_name, ctxt.get_binding('newcolname', ()))) # Definitely some of these aren't valid for ordering, but I'm not sure # precisely which are. This is good enough for now return colnames @completer_for('createColumnFamilyStatement', 'wat') def create_cf_wat_completer(ctxt, cass): # would prefer to get rid of the "columnfamily" nomenclature in cql3 if ctxt.get_binding('partial', '') == '': return ['TABLE'] return ['TABLE', 'COLUMNFAMILY'] explain_completion('createColumnFamilyStatement', 'cf', '') explain_completion('compositeKeyCfSpec', 'newcolname', '') @completer_for('createColumnFamilyStatement', 'dot') def create_cf_ks_dot_completer(ctxt, cass): ks = dequote_name(ctxt.get_binding('ks')) if ks in cass.get_keyspace_names(): return ['.'] return [] @completer_for('pkDef', 'ptkey') def create_cf_pkdef_declaration_completer(ctxt, cass): cols_declared = ctxt.get_binding('newcolname') pieces_already = ctxt.get_binding('ptkey', ()) pieces_already = list(map(dequote_name, pieces_already)) while cols_declared[0] in pieces_already: cols_declared = cols_declared[1:] if len(cols_declared) < 2: return () return [maybe_escape_name(cols_declared[0])] @completer_for('compositeKeyCfSpec', 'pkey') def create_cf_composite_key_declaration_completer(ctxt, cass): cols_declared = ctxt.get_binding('newcolname') pieces_already = ctxt.get_binding('ptkey', ()) + ctxt.get_binding('pkey', ()) pieces_already = list(map(dequote_name, pieces_already)) while cols_declared[0] in pieces_already: cols_declared = cols_declared[1:] if len(cols_declared) < 2: return () return [maybe_escape_name(cols_declared[0])] @completer_for('compositeKeyCfSpec', 'k') def create_cf_composite_primary_key_keyword_completer(ctxt, cass): return ['KEY ('] @completer_for('compositeKeyCfSpec', 'p') def create_cf_composite_primary_key_paren_completer(ctxt, cass): return ['('] @completer_for('compositeKeyCfSpec', 'c') def create_cf_composite_primary_key_comma_completer(ctxt, cass): cols_declared = ctxt.get_binding('newcolname') pieces_already = ctxt.get_binding('pkey', ()) if len(pieces_already) >= len(cols_declared) - 1: return () return [','] syntax_rules += r''' ::= | | ; ::= "CREATE" "CUSTOM"? "INDEX" ("IF" "NOT" "EXISTS")? indexname=? "ON" cf= "(" ( col= | "keys(" col= ")" | "full(" col= ")" ) ")" ( "USING" ( "WITH" "OPTIONS" "=" )? )? ; ::= "(" ( "," )* ")" ; ::= "CREATE" wat="MATERIALIZED" "VIEW" ("IF" "NOT" "EXISTS")? viewname=? "AS" "SELECT" "FROM" cf= "WHERE" "IS" "NOT" "NULL" ( "AND" "IS" "NOT" "NULL")* "PRIMARY" "KEY" ( | ( "(" ( "," )* ")" )) ( "WITH" ( "AND" )* )? ; ::= "CREATE" "TYPE" ( ks= dot="." )? typename= "(" newcol= ( "," [newcolname]= )* ")" ; ::= "CREATE" ("OR" "REPLACE")? "FUNCTION" ("IF" "NOT" "EXISTS")? ( "(" ( newcol= ( "," [newcolname]= )* )? ")" )? ("RETURNS" "NULL" | "CALLED") "ON" "NULL" "INPUT" "RETURNS" "LANGUAGE" "AS" ; ::= "CREATE" ("OR" "REPLACE")? "AGGREGATE" ("IF" "NOT" "EXISTS")? ( "(" ( ( "," )* )? ")" )? "SFUNC" "STYPE" ( "FINALFUNC" )? ( "INITCOND" )? ; ''' explain_completion('createIndexStatement', 'indexname', '') explain_completion('createMaterializedViewStatement', 'viewname', '') explain_completion('createUserTypeStatement', 'typename', '') explain_completion('createUserTypeStatement', 'newcol', '') @completer_for('createIndexStatement', 'col') def create_index_col_completer(ctxt, cass): """ Return the columns for which an index doesn't exist yet. """ layout = get_table_meta(ctxt, cass) idx_targets = [idx.index_options["target"] for idx in layout.indexes.values()] colnames = [cd.name for cd in list(layout.columns.values()) if cd.name not in idx_targets] return list(map(maybe_escape_name, colnames)) syntax_rules += r''' ::= "DROP" "KEYSPACE" ("IF" "EXISTS")? ksname= ; ::= "DROP" ( "COLUMNFAMILY" | "TABLE" ) ("IF" "EXISTS")? cf= ; ::= ( ksname= dot="." )? idxname= ; ::= | | ; ::= "DROP" "INDEX" ("IF" "EXISTS")? idx= ; ::= "DROP" "MATERIALIZED" "VIEW" ("IF" "EXISTS")? mv= ; ::= "DROP" "TYPE" ut= ; ::= "DROP" "FUNCTION" ( "IF" "EXISTS" )? ; ::= "DROP" "AGGREGATE" ( "IF" "EXISTS" )? ; ''' @completer_for('indexName', 'ksname') def idx_ks_name_completer(ctxt, cass): return [maybe_escape_name(ks) + '.' for ks in cass.get_keyspace_names()] @completer_for('indexName', 'dot') def idx_ks_dot_completer(ctxt, cass): name = dequote_name(ctxt.get_binding('ksname')) if name in cass.get_keyspace_names(): return ['.'] return [] @completer_for('indexName', 'idxname') def idx_ks_idx_name_completer(ctxt, cass): ks = ctxt.get_binding('ksname', None) if ks is not None: ks = dequote_name(ks) try: idxnames = cass.get_index_names(ks) except Exception: if ks is None: return () raise return list(map(maybe_escape_name, idxnames)) syntax_rules += r''' ::= "ALTER" wat=( "COLUMNFAMILY" | "TABLE" ) ("IF" "EXISTS")? cf= ; ::= "ADD" ("IF" "NOT" "EXISTS")? newcol= ("static")? | "DROP" ("IF" "EXISTS")? existcol= | "WITH" ( "AND" )* | "RENAME" ("IF" "EXISTS")? existcol= "TO" newcol= ( "AND" existcol= "TO" newcol= )* ; ::= "ALTER" "TYPE" ("IF" "EXISTS")? ut= ; ::= "ADD" ("IF" "NOT" "EXISTS")? newcol= | "RENAME" ("IF" "EXISTS")? existcol= "TO" newcol= ( "AND" existcol= "TO" newcol= )* ; ''' @completer_for('alterInstructions', 'existcol') def alter_table_col_completer(ctxt, cass): layout = get_table_meta(ctxt, cass) cols = [str(md) for md in layout.columns] return list(map(maybe_escape_name, cols)) @completer_for('alterTypeInstructions', 'existcol') def alter_type_field_completer(ctxt, cass): layout = get_ut_layout(ctxt, cass) fields = [atuple[0] for atuple in layout] return list(map(maybe_escape_name, fields)) explain_completion('alterInstructions', 'newcol', '') explain_completion('alterTypeInstructions', 'newcol', '') syntax_rules += r''' ::= "ALTER" wat=( "KEYSPACE" | "SCHEMA" ) ("IF" "EXISTS")? ks= "WITH" ( "AND" )* ; ''' syntax_rules += r''' ::= name=( | ) ; ::= "CREATE" "USER" ( "IF" "NOT" "EXISTS" )? ( "WITH" ("HASHED")? "PASSWORD" )? ( "SUPERUSER" | "NOSUPERUSER" )? ; ::= "ALTER" "USER" ("IF" "EXISTS")? ( "WITH" "PASSWORD" )? ( "SUPERUSER" | "NOSUPERUSER" )? ; ::= "DROP" "USER" ( "IF" "EXISTS" )? ; ::= "LIST" "USERS" ; ''' syntax_rules += r''' ::= | | ; ::= "CREATE" "ROLE" ( "WITH" ("AND" )*)? ; ::= "ALTER" "ROLE" ("IF" "EXISTS")? ( "WITH" ("AND" )*)? ; ::= (("HASHED")? "PASSWORD") "=" | "OPTIONS" "=" | "SUPERUSER" "=" | "LOGIN" "=" | "ACCESS" "TO" "DATACENTERS" | "ACCESS" "TO" "ALL" "DATACENTERS" ; ::= "DROP" "ROLE" ; ::= "GRANT" "TO" ; ::= "REVOKE" "FROM" ; ::= "LIST" "ROLES" ( "OF" )? "NORECURSIVE"? ; ''' syntax_rules += r''' ::= "GRANT" "ON" "TO" ; ::= "REVOKE" "ON" "FROM" ; ::= "LIST" ( "ON" )? ( "OF" )? "NORECURSIVE"? ; ::= "AUTHORIZE" | "CREATE" | "ALTER" | "DROP" | "SELECT" | "MODIFY" | "DESCRIBE" | "EXECUTE" ; ::= ( [newpermission]= "PERMISSION"? ( "," [newpermission]= "PERMISSION"? )* ) | ( "ALL" "PERMISSIONS"? ) ; ::= | | | ; ::= ( "ALL" "KEYSPACES" ) | ( "KEYSPACE" ) | ( "ALL" "TABLES" "IN" "KEYSPACE" ) | ( "TABLE"? ) ; ::= ("ALL" "ROLES") | ("ROLE" ) ; ::= ( "ALL" "FUNCTIONS" ("IN KEYSPACE" )? ) | ( "FUNCTION" ( "(" ( newcol= ( "," [newcolname]= )* )? ")" ) ) ; ::= ( "ALL" "MBEANS") | ( ( "MBEAN" | "MBEANS" ) ) ; ''' @completer_for('permissionExpr', 'newpermission') def permission_completer(ctxt, _): new_permissions = set([permission.upper() for permission in ctxt.get_binding('newpermission')]) all_permissions = set([permission.arg for permission in ctxt.ruleset['permission'].arg]) suggestions = all_permissions - new_permissions if len(suggestions) == 0: return [Hint('No more permissions here.')] return suggestions @completer_for('username', 'name') def username_name_completer(ctxt, cass): def maybe_quote(name): if CqlRuleSet.is_valid_cql3_name(name): return name return "'%s'" % name # disable completion for CREATE USER. if ctxt.matched[0][1].upper() == 'CREATE': return [Hint('')] session = cass.session return [maybe_quote(list(row.values())[0].replace("'", "''")) for row in session.execute("LIST USERS")] @completer_for('rolename', 'role') def rolename_completer(ctxt, cass): def maybe_quote(name): if CqlRuleSet.is_valid_cql3_name(name): return name return "'%s'" % name # disable completion for CREATE ROLE. if ctxt.matched[0][1].upper() == 'CREATE': return [Hint('')] session = cass.session return [maybe_quote(row[0].replace("'", "''")) for row in session.execute("LIST ROLES")] syntax_rules += r''' ::= "CREATE" "TRIGGER" ( "IF" "NOT" "EXISTS" )? "ON" cf= "USING" class= ; ::= "DROP" "TRIGGER" ( "IF" "EXISTS" )? triggername= "ON" cf= ; ''' explain_completion('createTriggerStatement', 'class', '\'fully qualified class name\'') def get_trigger_names(ctxt, cass): ks = ctxt.get_binding('ksname', None) if ks is not None: ks = dequote_name(ks) return cass.get_trigger_names(ks) @completer_for('dropTriggerStatement', 'triggername') def drop_trigger_completer(ctxt, cass): names = get_trigger_names(ctxt, cass) return list(map(maybe_escape_name, names)) # END SYNTAX/COMPLETION RULE DEFINITIONS CqlRuleSet.append_rules(syntax_rules)