Cqlsh supports DESCRIBE on cql3-style composite CFs. Patch by paul cannon, reviewed by brandonwilliams for CASSANDRA-4164
Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/b81f5723 Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/b81f5723 Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/b81f5723 Branch: refs/heads/trunk Commit: b81f5723efdf5ed79b6e152087c78e2befc9777f Parents: 2ca2fb3 Author: Brandon Williams <brandonwilli...@apache.org> Authored: Fri Apr 27 16:04:39 2012 -0500 Committer: Brandon Williams <brandonwilli...@apache.org> Committed: Fri Apr 27 16:05:42 2012 -0500 ---------------------------------------------------------------------- bin/cqlsh | 146 ++++++++++++++++++++++++++++++++++- pylib/cqlshlib/cqlhandling.py | 18 ++++- 2 files changed, 156 insertions(+), 8 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cassandra/blob/b81f5723/bin/cqlsh ---------------------------------------------------------------------- diff --git a/bin/cqlsh b/bin/cqlsh index c26324a..00c6c0d 100755 --- a/bin/cqlsh +++ b/bin/cqlsh @@ -50,6 +50,7 @@ import ConfigParser import codecs import re import platform +import warnings # cqlsh should run correctly when run out of a Cassandra source tree, # out of an unpacked Cassandra tarball, and after a proper package install. @@ -57,9 +58,11 @@ cqlshlibdir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'p if os.path.isdir(cqlshlibdir): sys.path.insert(0, cqlshlibdir) -from cqlshlib import cqlhandling, pylexotron, wcwidth +from cqlshlib import cqlhandling, cql3handling, pylexotron, wcwidth from cqlshlib.cqlhandling import (token_dequote, cql_dequote, cql_escape, maybe_cql_escape, cql_typename) +from cqlshlib.cql3handling import (CqlTableDef, maybe_cql3_escape_name, + cql3_escape_value) try: import readline @@ -167,6 +170,7 @@ cqlhandling.commands_end_with_newline.update(( 'assume', 'source', 'capture', + 'debug', 'exit', 'quit' )) @@ -180,6 +184,7 @@ cqlhandling.CqlRuleSet.append_rules(r''' | <assumeCommand> | <sourceCommand> | <captureCommand> + | <debugCommand> | <helpCommand> | <exitCommand> ; @@ -209,6 +214,9 @@ cqlhandling.CqlRuleSet.append_rules(r''' <captureCommand> ::= "CAPTURE" ( fname=( <stringLiteral> | "OFF" ) )? ; +<debugCommand> ::= "DEBUG" + ; + <helpCommand> ::= ( "HELP" | "?" ) [topic]=( <identifier> | <stringLiteral> )* ; @@ -456,6 +464,16 @@ def format_value(val, casstype, output_encoding, addcolor=False, time_format='', return FormattedValue(bval, coloredval, displaywidth) +def show_warning_without_quoting_line(message, category, filename, lineno, file=None, line=None): + if file is None: + file = sys.stderr + try: + file.write(warnings.formatwarning(message, category, filename, lineno, line='')) + except IOError: + pass +warnings.showwarning = show_warning_without_quoting_line +warnings.filterwarnings('always', category=cql3handling.UnexpectedTableStructure) + class Shell(cmd.Cmd): default_prompt = "cqlsh> " continue_prompt = " ... " @@ -589,6 +607,18 @@ class Shell(cmd.Cmd): return {'build': 'unknown', 'cql': 'unknown', 'thrift': thrift_ver} return vers + def fetchdict(self): + row = self.cursor.fetchone() + desc = self.cursor.description + return dict(zip([d[0] for d in desc], row)) + + def fetchdict_all(self): + dicts = [] + for row in self.cursor: + desc = self.cursor.description + dicts.append(dict(zip([d[0] for d in desc], row))) + return dicts + def get_keyspace_names(self): return [k.name for k in self.get_keyspaces()] @@ -674,6 +704,21 @@ class Shell(cmd.Cmd): # ===== end thrift-dependent parts ===== + # ===== cql3-dependent parts ===== + + def get_columnfamily_layout(self, ksname, cfname): + self.cursor.execute("""select * from system.schema_columnfamilies + where "keyspace"=:ks and "columnfamily"=:cf""", + {'ks': ksname, 'cf': cfname}) + layout = self.fetchdict() + self.cursor.execute("""select * from system.schema_columns + where "keyspace"=:ks and "columnfamily"=:cf""", + {'ks': ksname, 'cf': cfname}) + cols = self.fetchdict_all() + return CqlTableDef.from_layout(layout, cols) + + # ===== end cql3-dependent parts ===== + def reset_statement(self): self.reset_prompt() self.statement.truncate(0) @@ -1030,12 +1075,45 @@ class Shell(cmd.Cmd): out.write('\nUSE %s;\n' % ksname) for cf in ksdef.cf_defs: out.write('\n') - self.print_recreate_columnfamily(cf, out) + # yes, cf might be looked up again. oh well. + self.print_recreate_columnfamily(ksname, cf.name, out) + + def print_recreate_columnfamily(self, ksname, cfname, out): + """ + Output CQL commands which should be pasteable back into a CQL session + to recreate the given table. Can change based on CQL version in use; + CQL 3 syntax will not be output when in CQL 2 mode, and properties + which are deprecated with CQL 3 use (like default_validation) will not + be output when in CQL 3 mode. + + Writes output to the given out stream. + """ - def print_recreate_columnfamily(self, cfdef, out): + # no metainfo available from system.schema_* for system CFs, so we have + # to use cfdef-based description for those. also, use cfdef-based + # description when the CF doesn't have a composite key. that seems like + # an ok compromise between hiding "comparator", + # "default_validation_class", etc for cql3, and still allowing users + # to work with old cql2-style wide tables. + + if cfname != 'system' \ + and self.cqlver_atleast(3): + try: + layout = self.get_columnfamily_layout(ksname, cfname) + except CQL_ERRORS: + # most likely a 1.1 beta where cql3 is supported, but not system.schema_* + pass + else: + if len(layout.key_components) > 1: + return self.print_recreate_columnfamily_from_layout(layout, out) + + cfdef = self.get_columnfamily(cfname, ksname=ksname) + return self.print_recreate_columnfamily_from_cfdef(cfdef, out) + + def print_recreate_columnfamily_from_cfdef(self, cfdef, out): cfname = maybe_cql_escape(cfdef.name) out.write("CREATE COLUMNFAMILY %s (\n" % cfname) - alias = cfdef.key_alias if cfdef.key_alias else 'KEY' + alias = maybe_cql_escape(cfdef.key_alias) if cfdef.key_alias else 'KEY' keytype = cql_typename(cfdef.key_validation_class) out.write(" %s %s PRIMARY KEY" % (alias, keytype)) indexed_columns = [] @@ -1061,6 +1139,8 @@ class Shell(cmd.Cmd): for option, thriftname, _ in cqlhandling.columnfamily_map_options: optmap = getattr(cfdef, thriftname or option, {}) for k, v in optmap.items(): + if option == 'compression_parameters' and k == 'sstable_compression': + v = trim_if_present(v, 'org.apache.cassandra.io.compress.') notable_columns.append(('%s:%s' % (option, k), cql_escape(v))) out.write('\n)') if notable_columns: @@ -1076,6 +1156,58 @@ class Shell(cmd.Cmd): out.write('CREATE INDEX %s ON %s (%s);\n' % (col.index_name, cfname, maybe_cql_escape(col.name))) + def print_recreate_columnfamily_from_layout(self, layout, out): + cfname = maybe_cql3_escape_name(layout.name) + out.write("CREATE COLUMNFAMILY %s (\n" % cfname) + keycol = layout.columns[0] + out.write(" %s %s" % (maybe_cql3_escape_name(keycol.name), keycol.cqltype)) + if len(layout.key_components) == 1: + out.write(" PRIMARY KEY") + + indexed_columns = [] + for col in layout.columns[1:]: + colname = maybe_cql3_escape_name(col.name) + out.write(",\n %s %s" % (colname, col.cqltype)) + if col.index_name is not None: + indexed_columns.append(col) + + if len(layout.key_components) > 1: + out.write(",\n PRIMARY KEY (%s)" % ', '.join(map(maybe_cql3_escape_name, layout.key_components))) + out.write("\n)") + joiner = 'WITH' + + if layout.compact_storage: + out.write(' WITH COMPACT STORAGE') + joiner = 'AND' + + cf_opts = [] + for option in cql3handling.columnfamily_options: + optval = getattr(layout, option, None) + if optval is None: + continue + if option == 'row_cache_provider': + optval = trim_if_present(optval, 'org.apache.cassandra.cache.') + elif option == 'compaction_strategy_class': + optval = trim_if_present(optval, 'org.apache.cassandra.db.compaction.') + cf_opts.append((option, cql3_escape_value(optval))) + for option, _ in cql3handling.columnfamily_map_options: + optmap = getattr(layout, option, {}) + for k, v in optmap.items(): + if option == 'compression_parameters' and k == 'sstable_compression': + v = trim_if_present(v, 'org.apache.cassandra.io.compress.') + cf_opts.append(('%s:%s' % (option, k.encode('ascii')), cql3_escape_value(v))) + if cf_opts: + for optname, optval in cf_opts: + out.write(" %s\n %s=%s" % (joiner, optname, optval)) + joiner = 'AND' + out.write(";\n") + + for col in indexed_columns: + out.write('\n') + # guess CQL can't represent index_type or index_options + out.write('CREATE INDEX %s ON %s (%s);\n' + % (col.index_name, cfname, maybe_cql3_escape_name(col.name))) + def describe_keyspace(self, ksname): print self.print_recreate_keyspace(self.get_keyspace(ksname), sys.stdout) @@ -1083,7 +1215,7 @@ class Shell(cmd.Cmd): def describe_columnfamily(self, cfname): print - self.print_recreate_columnfamily(self.get_columnfamily(cfname), sys.stdout) + self.print_recreate_columnfamily(ksname, cfname, sys.stdout) print def describe_columnfamilies(self, ksname): @@ -1386,6 +1518,10 @@ class Shell(cmd.Cmd): self.stop = True do_quit = do_exit + def do_debug(self, parsed): + import pdb + pdb.set_trace() + def get_names(self): names = cmd.Cmd.get_names(self) for hide_from_help in ('do_quit',): http://git-wip-us.apache.org/repos/asf/cassandra/blob/b81f5723/pylib/cqlshlib/cqlhandling.py ---------------------------------------------------------------------- diff --git a/pylib/cqlshlib/cqlhandling.py b/pylib/cqlshlib/cqlhandling.py index 3907162..09a2980 100644 --- a/pylib/cqlshlib/cqlhandling.py +++ b/pylib/cqlshlib/cqlhandling.py @@ -23,6 +23,16 @@ from itertools import izip Hint = pylexotron.Hint +keywords = set(( + 'select', 'from', 'where', 'and', 'key', 'insert', 'update', 'with', + 'limit', 'using', 'consistency', 'one', 'quorum', 'all', 'any', + 'local_quorum', 'each_quorum', 'two', 'three', 'use', 'count', 'set', + 'begin', 'apply', 'batch', 'truncate', 'delete', 'in', 'create', + 'keyspace', 'schema', 'columnfamily', 'table', 'index', 'on', 'drop', + 'primary', 'into', 'values', 'timestamp', 'ttl', 'alter', 'add', 'type', + 'first', 'reversed' +)) + columnfamily_options = ( # (CQL option name, Thrift option name (or None if same)) ('comment', None), @@ -109,7 +119,7 @@ consistency_levels = ( valid_cql_word_re = re.compile(r"^(?:[a-z][a-z0-9_]*|-?[0-9][0-9.]*)$", re.I) def is_valid_cql_word(s): - return valid_cql_word_re.match(s) is not None + return valid_cql_word_re.match(s) is not None and s not in keywords def tokenize_cql(cql_text): return CqlLexotron.scan(cql_text)[0] @@ -146,9 +156,11 @@ def token_is_word(tok): def cql_escape(value): if value is None: return 'NULL' # this totally won't work - if isinstance(value, float): + if isinstance(value, bool): + value = str(value).lower() + elif isinstance(value, float): return '%f' % value - if isinstance(value, int): + elif isinstance(value, int): return str(value) return "'%s'" % value.replace("'", "''")