| #!/bin/sh |
| # -*- mode: Python -*- |
| |
| # 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. |
| |
| """:" |
| # bash code here; finds a suitable python interpreter and execs this file. |
| # prefer unqualified "python" if suitable: |
| python -c 'import sys; sys.exit(sys.hexversion < 0x020500b0)' 2>/dev/null \ |
| && exec python "$0" "$@" |
| for pyver in 2.6 2.7 2.5; do |
| which python$pyver > /dev/null 2>&1 && exec python$pyver "$0" "$@" |
| done |
| echo "No appropriate python interpreter found." >&2 |
| exit 1 |
| ":""" |
| |
| from __future__ import with_statement |
| |
| description = "CQL Shell for Apache Cassandra" |
| version = "2.1.0" |
| |
| from StringIO import StringIO |
| from itertools import groupby |
| from contextlib import contextmanager |
| from glob import glob |
| |
| import cmd |
| import sys |
| import os |
| import time |
| import optparse |
| import ConfigParser |
| import codecs |
| import re |
| import platform |
| |
| # cqlsh should run correctly when run out of a Cassandra source tree, |
| # out of an unpacked Cassandra tarball, and after a proper package install. |
| cqlshlibdir = os.path.join(os.path.dirname(__file__), '..', 'pylib') |
| if os.path.isdir(cqlshlibdir): |
| sys.path.insert(0, cqlshlibdir) |
| |
| from cqlshlib import cqlhandling, pylexotron, wcwidth |
| from cqlshlib.cqlhandling import (token_dequote, cql_dequote, cql_escape, |
| maybe_cql_escape, cql_typename) |
| |
| try: |
| import readline |
| except ImportError: |
| readline = None |
| |
| CQL_LIB_PREFIX = 'cql-internal-only-' |
| THRIFT_LIB_PREFIX = 'thrift-python-internal-only-' |
| |
| # use bundled libs for python-cql and thrift, if available. if there |
| # is a ../lib dir, use bundled libs there preferentially. |
| ZIPLIB_DIRS = [os.path.join(os.path.dirname(__file__), '..', 'lib')] |
| myplatform = platform.system() |
| if myplatform == 'Linux': |
| ZIPLIB_DIRS.append('/usr/share/cassandra/lib') |
| |
| def find_zip(libprefix): |
| for ziplibdir in ZIPLIB_DIRS: |
| zips = glob(os.path.join(ziplibdir, libprefix + '*.zip')) |
| if zips: |
| return max(zips) # probably the highest version, if multiple |
| |
| cql_zip = find_zip(CQL_LIB_PREFIX) |
| if cql_zip: |
| ver = os.path.splitext(os.path.basename(cql_zip))[0][len(CQL_LIB_PREFIX):] |
| sys.path.insert(0, os.path.join(cql_zip, 'cql-' + ver)) |
| thrift_zip = find_zip(THRIFT_LIB_PREFIX) |
| if thrift_zip: |
| sys.path.insert(0, thrift_zip) |
| |
| try: |
| import cql |
| except ImportError, e: |
| sys.exit("\nPython CQL driver not installed, or not on PYTHONPATH.\n" |
| 'You might try "easy_install cql".\n\n' |
| 'Python: %s\n' |
| 'Module load path: %r\n\n' |
| 'Error: %s\n' % (sys.executable, sys.path, e)) |
| |
| import cql.decoders |
| from cql.cursor import _COUNT_DESCRIPTION, _VOID_DESCRIPTION |
| |
| |
| CONFIG_FILE = os.path.expanduser(os.path.join('~', '.cqlshrc')) |
| HISTORY = os.path.expanduser(os.path.join('~', '.cqlsh_history')) |
| DEFAULT_HOST = 'localhost' |
| DEFAULT_PORT = 9160 |
| |
| epilog = """Connects to %(DEFAULT_HOST)s:%(DEFAULT_PORT)d by default. These |
| defaults can be changed by setting $CQLSH_HOST and/or $CQLSH_PORT. When a |
| host (and optional port number) are given on the command line, they take |
| precedence over any defaults.""" % globals() |
| |
| parser = optparse.OptionParser(description=description, epilog=epilog, |
| usage="Usage: %prog [options] [host [port]]", |
| version='cqlsh ' + version) |
| parser.add_option("-C", "--color", action="store_true", |
| help="Enable color output.") |
| parser.add_option("-u", "--username", help="Authenticate as user.") |
| parser.add_option("-p", "--password", help="Authenticate using password.") |
| parser.add_option("-f", "--file", |
| help="Execute commands from FILE, then exit") |
| parser.add_option('--debug', action='store_true', |
| help='Show additional debugging information') |
| |
| |
| RED = '\033[0;1;31m' |
| GREEN = '\033[0;1;32m' |
| YELLOW = '\033[0;1;33m' |
| BLUE = '\033[0;1;34m' |
| MAGENTA = '\033[0;1;35m' |
| CYAN = '\033[0;1;36m' |
| WHITE = '\033[0;1;37m' |
| ANSI_RESET = '\033[0m' |
| |
| CQL_ERRORS = (cql.Error,) |
| try: |
| from thrift.Thrift import TException |
| except ImportError: |
| pass |
| else: |
| CQL_ERRORS += (TException,) |
| |
| debug_completion = bool(os.environ.get('CQLSH_DEBUG_COMPLETION', '') == 'YES') |
| |
| # we want the cql parser to understand our cqlsh-specific commands too |
| cqlhandling.commands_end_with_newline.update(( |
| 'help', |
| '?', |
| 'describe', |
| 'desc', |
| 'show', |
| 'assume', |
| 'source', |
| 'capture', |
| 'exit', |
| 'quit' |
| )) |
| |
| cqlhandling.CqlRuleSet.append_rules(r''' |
| <cqlshCommand> ::= <CQL_Statement> |
| | <specialCommand> ( ";" | "\n" ) |
| ; |
| <specialCommand> ::= <describeCommand> |
| | <showCommand> |
| | <assumeCommand> |
| | <sourceCommand> |
| | <captureCommand> |
| | <helpCommand> |
| | <exitCommand> |
| ; |
| |
| <describeCommand> ::= ( "DESCRIBE" | "DESC" ) ( "KEYSPACE" ksname=<name>? |
| | "COLUMNFAMILY" cfname=<name> |
| | "COLUMNFAMILIES" |
| | "SCHEMA" |
| | "CLUSTER" ) |
| ; |
| |
| <showCommand> ::= "SHOW" what=( "VERSION" | "HOST" | "ASSUMPTIONS" ) |
| ; |
| |
| <assumeCommand> ::= "ASSUME" ( ks=<name> "." )? cf=<name> <assumeTypeDef> |
| ( "," <assumeTypeDef> )* |
| ; |
| |
| <assumeTypeDef> ::= "NAMES" "ARE" names=<storageType> |
| | "VALUES" "ARE" values=<storageType> |
| | "(" colname=<name> ")" "VALUES" "ARE" colvalues=<storageType> |
| ; |
| |
| <sourceCommand> ::= "SOURCE" fname=<stringLiteral> |
| ; |
| |
| <captureCommand> ::= "CAPTURE" ( fname=( <stringLiteral> | "OFF" ) )? |
| ; |
| |
| <helpCommand> ::= ( "HELP" | "?" ) [topic]=( <identifier> | <stringLiteral> )* |
| ; |
| |
| <exitCommand> ::= "exit" | "quit" |
| ; |
| |
| <qmark> ::= "?" ; |
| ''') |
| |
| @cqlhandling.cql_add_completer('helpCommand', 'topic') |
| def complete_help(ctxt, cqlsh): |
| helpfuncs = [n[5:].upper() for n in cqlsh.get_names() if n.startswith('help_')] |
| funcs_with_docstrings = [n[3:].upper() for n in cqlsh.get_names() |
| if n.startswith('do_') and getattr(cqlsh, n, None).__doc__] |
| return sorted(helpfuncs + funcs_with_docstrings) |
| |
| @cqlhandling.cql_add_completer('describeCommand', 'ksname') |
| def complete_describe_ks(ctxt, cqlsh): |
| return map(maybe_cql_escape, cqlsh.get_keyspace_names()) |
| |
| @cqlhandling.cql_add_completer('describeCommand', 'cfname') |
| def complete_describe_cf(ctxt, cqlsh): |
| return map(maybe_cql_escape, cqlsh.get_columnfamily_names()) |
| |
| @cqlhandling.cql_add_completer('assumeCommand', 'ks') |
| def complete_assume_ks(ctxt, cqlsh): |
| return [maybe_cql_escape(ks) + '.' for ks in cqlsh.get_keyspace_names()] |
| |
| @cqlhandling.cql_add_completer('assumeCommand', 'cf') |
| def complete_assume_cf(ctxt, cqlsh): |
| ks = ctxt.get_binding('ks', None) |
| if ks is not None: |
| ks = cql_dequote(ks) |
| return map(maybe_cql_escape, cqlsh.get_columnfamily_names(ks)) |
| |
| @cqlhandling.cql_add_completer('assumeTypeDef', 'colname') |
| def complete_assume_col(ctxt, cqlsh): |
| ks = ctxt.get_binding('ks', None) |
| ks = cql_dequote(ks) if ks is not None else None |
| cf = cql_dequote(ctxt.get_binding('cf')) |
| cfdef = cqlsh.get_columnfamily(cf, ksname=ks) |
| cols = [cm.name for cm in cfdef.column_metadata] |
| cols.append(cfdef.key_alias or 'KEY') |
| return map(maybe_cql_escape, cols) |
| |
| def complete_source_quoted_filename(ctxt, cqlsh): |
| partial = ctxt.get_binding('partial', '') |
| head, tail = os.path.split(partial) |
| exhead = os.path.expanduser(head) |
| try: |
| contents = os.listdir(exhead or '.') |
| except OSError: |
| return () |
| matches = filter(lambda f: f.startswith(tail), contents) |
| annotated = [] |
| for f in matches: |
| match = os.path.join(head, f) |
| if os.path.isdir(os.path.join(exhead, f)): |
| match += '/' |
| annotated.append(match) |
| return annotated |
| |
| cqlhandling.cql_add_completer('sourceCommand', 'fname') \ |
| (complete_source_quoted_filename) |
| cqlhandling.cql_add_completer('captureCommand', 'fname') \ |
| (complete_source_quoted_filename) |
| |
| class NoKeyspaceError(Exception): |
| pass |
| |
| class KeyspaceNotFound(Exception): |
| pass |
| |
| def trim_if_present(s, prefix): |
| if s.startswith(prefix): |
| return s[len(prefix):] |
| return s |
| |
| class FormattedValue: |
| def __init__(self, strval, coloredval, displaywidth): |
| self.strval = strval |
| self.coloredval = coloredval |
| self.displaywidth = displaywidth |
| |
| def _pad(self, width, fill=' '): |
| if width > self.displaywidth: |
| return fill * (width - self.displaywidth) |
| else: |
| return '' |
| |
| def rjust(self, width, fill=' '): |
| """ |
| Similar to self.strval.rjust(width), but takes expected terminal |
| display width into account for special characters, and does not |
| take color escape codes into account. |
| """ |
| return self._pad(width, fill) + self.strval |
| |
| def color_rjust(self, width, fill=' '): |
| """ |
| Similar to self.rjust(width), but uses this value's colored |
| representation, and does not take color escape codes into account |
| in determining width. |
| """ |
| return self._pad(width, fill) + self.coloredval |
| |
| controlchars_re = re.compile(r'[\x00-\x31\x7f-\xa0]') |
| |
| def _show_control_chars(match): |
| txt = repr(match.group(0)) |
| if txt.startswith('u'): |
| txt = txt[2:-1] |
| else: |
| txt = txt[1:-1] |
| return txt |
| |
| bits_to_turn_red_re = re.compile(r'\\([^uUx]|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|U[0-9a-fA-F]{8})') |
| |
| def _turn_bits_red(match): |
| txt = match.group(0) |
| if txt == '\\\\': |
| return '\\' |
| return RED + txt + YELLOW |
| |
| def format_value(val, casstype, output_encoding, addcolor=False, time_format='', float_precision=3): |
| color = YELLOW |
| coloredval = None |
| displaywidth = None |
| |
| if val is None: |
| bval = 'null' |
| color = RED |
| elif casstype == 'UTF8Type': |
| escapedval = val.replace(u'\\', u'\\\\') |
| escapedval = controlchars_re.sub(_show_control_chars, escapedval) |
| bval = escapedval.encode(output_encoding, 'backslashreplace') |
| displaywidth = wcwidth.wcswidth(bval.decode(output_encoding)) |
| if addcolor: |
| coloredval = YELLOW + bits_to_turn_red_re.sub(_turn_bits_red, bval) + ANSI_RESET |
| elif casstype == 'DateType': |
| timestamp = time.localtime(val) |
| bval = time.strftime(time_format, timestamp) |
| color = GREEN |
| elif casstype in ('LongType', 'Int32Type', 'IntegerType'): |
| # base-10 only for now; support others? |
| bval = str(val) |
| color = GREEN |
| elif casstype in ('FloatType', 'DoubleType'): |
| bval = '%.*g' % (float_precision, val) |
| color = GREEN |
| elif casstype in ('DecimalType', 'UUIDType', 'BooleanType'): |
| # let python do these for us |
| bval = str(val) |
| color = GREEN |
| elif casstype == 'BytesType': |
| bval = ''.join('%02x' % ord(c) for c in val) |
| color = RED |
| else: |
| # AsciiType is the only other one known right now, but handle others |
| escapedval = val.replace('\\', '\\\\') |
| bval = controlchars_re.sub(_show_control_chars, escapedval) |
| if addcolor: |
| coloredval = YELLOW + bits_to_turn_red_re.sub(_turn_bits_red, bval) + ANSI_RESET |
| |
| if displaywidth is None: |
| displaywidth = len(bval) |
| if not addcolor: |
| coloredval = bval |
| elif coloredval is None: |
| coloredval = color + bval + ANSI_RESET |
| |
| return FormattedValue(bval, coloredval, displaywidth) |
| |
| class Shell(cmd.Cmd): |
| default_prompt = "cqlsh> " |
| continue_prompt = " ... " |
| keyspace_prompt = "cqlsh:%s> " |
| keyspace_continue_prompt = "%s ... " |
| display_time_format = '%Y-%m-%d %H:%M:%S%z' |
| display_float_precision = 3 |
| num_retries = 4 |
| show_line_nums = False |
| debug = False |
| stop = False |
| shunted_query_out = None |
| |
| def __init__(self, hostname, port, color=False, username=None, |
| password=None, encoding=None, stdin=None, tty=True, |
| completekey='tab', use_conn=None): |
| cmd.Cmd.__init__(self, completekey=completekey) |
| self.hostname = hostname |
| self.port = port |
| if use_conn is not None: |
| self.conn = use_conn |
| else: |
| self.conn = cql.connect(hostname, port, user=username, password=password) |
| self.cursor = self.conn.cursor() |
| |
| self.current_keyspace = None |
| |
| self.color = color |
| if encoding is None: |
| encoding = sys.stdout.encoding or 'ascii' |
| self.encoding = encoding |
| self.output_codec = codecs.lookup(encoding) |
| |
| self.statement = StringIO() |
| self.lineno = 1 |
| self.in_comment = False |
| self.schema_overrides = {} |
| |
| self.prompt = '' |
| if stdin is None: |
| stdin = sys.stdin |
| self.tty = tty |
| if tty: |
| self.prompt = self.default_prompt |
| self.report_connection() |
| print 'Use HELP for help.' |
| else: |
| self.show_line_nums = True |
| self.stdin = stdin |
| self.query_out = sys.stdout |
| |
| def myformat_value(self, val, casstype): |
| return format_value(val, casstype, self.output_codec.name, |
| addcolor=self.color, time_format=self.display_time_format, |
| float_precision=self.display_float_precision) |
| |
| def report_connection(self): |
| self.show_host() |
| self.show_version() |
| |
| def show_host(self): |
| print "Connected to %s at %s:%d." % \ |
| (self.applycolor(self.get_cluster_name(), BLUE), |
| self.hostname, |
| self.port) |
| |
| def show_version(self): |
| vers = self.get_cluster_versions() |
| vers['shver'] = version |
| print "[cqlsh %(shver)s | Cassandra %(build)s | CQL spec %(cql)s | Thrift protocol %(thrift)s]" % vers |
| |
| def show_assumptions(self): |
| all_overrides = self.schema_overrides.items() |
| all_overrides.sort() |
| if all_overrides: |
| print |
| else: |
| print 'No overrides.' |
| return |
| for keyspace, ksoverrides in groupby(all_overrides, key=lambda x:x[0][0]): |
| keyspace = maybe_cql_escape(keyspace) |
| print 'USE %s;' % keyspace |
| print |
| for (ks, cf), override in ksoverrides: |
| cf = maybe_cql_escape(cf) |
| if override.default_name_type: |
| print 'ASSUME %s NAMES ARE %s;' \ |
| % (cf, cql_typename(override.default_name_type)) |
| if override.default_value_type: |
| print 'ASSUME %s VALUES ARE %s;' \ |
| % (cf, cql_typename(override.default_value_type)) |
| for colname, vtype in override.value_types.items(): |
| colname = maybe_cql_escape(colname) |
| print 'ASSUME %s(%s) VALUES ARE %s;' \ |
| % (cf, colname, cql_typename(vtype)) |
| print |
| |
| def get_cluster_versions(self): |
| try: |
| self.cursor.execute('select component, version from system.Versions') |
| vers = dict(self.cursor) |
| except cql.ProgrammingError: |
| # older Cassandra; doesn't have system.Versions |
| thrift_ver = self.get_thrift_version() |
| return {'build': 'unknown', 'cql': 'unknown', 'thrift': thrift_ver} |
| return vers |
| |
| def get_keyspace_names(self): |
| return [k.name for k in self.get_keyspaces()] |
| |
| def get_columnfamilies(self, ksname=None): |
| if ksname is None: |
| ksname = self.current_keyspace |
| if ksname is None: |
| raise NoKeyspaceError("Not in any keyspace.") |
| return self.get_keyspace(ksname).cf_defs |
| |
| def get_columnfamily(self, cfname, ksname=None): |
| if ksname is None: |
| ksname = self.current_keyspace |
| cf_defs = self.get_columnfamilies(ksname) |
| for c in cf_defs: |
| if c.name == cfname: |
| return c |
| raise KeyError("Unconfigured column family %r" % (cfname,)) |
| |
| def get_columnfamily_names(self, ksname=None): |
| return [c.name for c in self.get_columnfamilies(ksname)] |
| |
| def get_index_names(self, ksname=None): |
| indnames = [] |
| for c in self.get_columnfamilies(ksname): |
| for md in c.column_metadata: |
| if md.index_name is not None: |
| indnames.append(md.index_name) |
| return indnames |
| |
| def filterable_column_names(self, cfname, ksname=None): |
| cfdef = self.get_columnfamily(cfname, ksname=ksname) |
| filterable = set() |
| if cfdef.key_alias is not None and cfdef.key_alias != 'KEY': |
| filterable.add(cfdef.key_alias) |
| else: |
| filterable.add('KEY') |
| for cm in cfdef.column_metadata: |
| if cm.index_name is not None: |
| filterable.add(cm.name) |
| return filterable |
| |
| # ===== thrift-dependent parts ===== |
| |
| def get_cluster_name(self): |
| return self.make_hacktastic_thrift_call('describe_cluster_name') |
| |
| def get_partitioner(self): |
| return self.make_hacktastic_thrift_call('describe_partitioner') |
| |
| def get_snitch(self): |
| return self.make_hacktastic_thrift_call('describe_snitch') |
| |
| def get_thrift_version(self): |
| return self.make_hacktastic_thrift_call('describe_version') |
| |
| def get_ring(self): |
| if self.current_keyspace is None: |
| raise NoKeyspaceError("Ring view requires a current non-system keyspace") |
| return self.make_hacktastic_thrift_call('describe_ring', self.current_keyspace) |
| |
| def get_keyspace(self, ksname): |
| return self.make_hacktastic_thrift_call('describe_keyspace', ksname) |
| |
| def get_keyspaces(self): |
| return self.make_hacktastic_thrift_call('describe_keyspaces') |
| |
| def get_schema_versions(self): |
| return self.make_hacktastic_thrift_call('describe_schema_versions') |
| |
| def make_hacktastic_thrift_call(self, call, *args): |
| client = self.cursor._connection.client |
| return getattr(client, call)(*args) |
| |
| # ===== end thrift-dependent parts ===== |
| |
| def reset_statement(self): |
| self.reset_prompt() |
| self.statement.truncate(0) |
| |
| def reset_prompt(self): |
| if self.current_keyspace is None: |
| self.set_prompt(self.default_prompt) |
| else: |
| self.set_prompt(self.keyspace_prompt % self.current_keyspace) |
| |
| def set_continue_prompt(self): |
| if self.current_keyspace is None: |
| self.set_prompt(self.continue_prompt) |
| else: |
| spaces = ' ' * len(str(self.current_keyspace)) |
| self.set_prompt(self.keyspace_continue_prompt % spaces) |
| |
| @contextmanager |
| def prepare_loop(self): |
| readline = None |
| if self.tty and self.completekey: |
| try: |
| import readline |
| except ImportError: |
| pass |
| else: |
| old_completer = readline.get_completer() |
| readline.set_completer(self.complete) |
| readline.parse_and_bind(self.completekey+": complete") |
| try: |
| yield |
| finally: |
| if readline is not None: |
| readline.set_completer(old_completer) |
| |
| def get_input_line(self, prompt=''): |
| if self.tty: |
| line = raw_input(self.prompt) + '\n' |
| else: |
| sys.stdout.write(self.prompt) |
| sys.stdout.flush() |
| line = self.stdin.readline() |
| if not len(line): |
| raise EOFError |
| self.lineno += 1 |
| return line |
| |
| def cmdloop(self): |
| """ |
| Adapted from cmd.Cmd's version, because there is literally no way with |
| cmd.Cmd.cmdloop() to tell the difference between "EOF" showing up in |
| input and an actual EOF. |
| """ |
| with self.prepare_loop(): |
| while not self.stop: |
| try: |
| line = self.get_input_line(self.prompt) |
| self.statement.write(line) |
| if self.onecmd(self.statement.getvalue()): |
| self.reset_statement() |
| except EOFError: |
| self.handle_eof() |
| except cql.Error, cqlerr: |
| self.printerr(str(cqlerr)) |
| except KeyboardInterrupt: |
| self.reset_statement() |
| print |
| |
| def onecmd(self, statementtext): |
| """ |
| Returns true if the statement is complete and was handled (meaning it |
| can be reset). |
| """ |
| |
| try: |
| statements, in_batch = cqlhandling.cql_split_statements(statementtext) |
| except pylexotron.LexingError, e: |
| if self.show_line_nums: |
| self.printerr('Invalid syntax at char %d' % (e.charnum,)) |
| else: |
| self.printerr('Invalid syntax at line %d, char %d' |
| % (e.linenum, e.charnum)) |
| statementline = statementtext.split('\n')[e.linenum - 1] |
| self.printerr(' %s' % statementline) |
| self.printerr(' %s^' % (' ' * e.charnum)) |
| return True |
| |
| while statements and not statements[-1]: |
| statements = statements[:-1] |
| if not statements: |
| return True |
| if in_batch or statements[-1][-1][0] != 'endtoken': |
| self.set_continue_prompt() |
| return |
| for st in statements: |
| try: |
| self.handle_statement(st, statementtext) |
| except Exception, e: |
| if self.debug: |
| import traceback |
| traceback.print_exc() |
| else: |
| self.printerr(e) |
| return True |
| |
| def handle_eof(self): |
| if self.tty: |
| print |
| statement = self.statement.getvalue() |
| if statement.strip(): |
| if not self.onecmd(statement + ';'): |
| self.printerr('Incomplete statement at end of file') |
| self.do_exit() |
| |
| def handle_statement(self, tokens, srcstr): |
| cmdword = tokens[0][1] |
| if cmdword == '?': |
| cmdword = 'help' |
| custom_handler = getattr(self, 'do_' + cmdword.lower(), None) |
| if custom_handler: |
| parsed = cqlhandling.cql_whole_parse_tokens(tokens, srcstr=srcstr, |
| startsymbol='cqlshCommand') |
| if parsed and not parsed.remainder: |
| # successful complete parse |
| return custom_handler(parsed) |
| else: |
| return self.handle_parse_error(cmdword, tokens, parsed, srcstr) |
| return self.perform_statement(cqlhandling.cql_extract_orig(tokens, srcstr)) |
| |
| def handle_parse_error(self, cmdword, tokens, parsed, srcstr): |
| if cmdword == 'select': |
| # hey, maybe they know about some new syntax we don't. type |
| # assumptions won't work, but maybe the query will. |
| return self.perform_statement(cqlhandling.cql_extract_orig(tokens, srcstr)) |
| if parsed: |
| self.printerr('Improper %s command (problem at %r).' % (cmdword, parsed.remainder[0])) |
| else: |
| self.printerr('Improper %s command.' % cmdword) |
| |
| def do_use(self, parsed): |
| """ |
| USE <keyspacename>; |
| |
| Tells cqlsh and the connected Cassandra instance that you will be |
| working in the given keyspace. All subsequent operations on column |
| families or indexes will be in the context of this keyspace, unless |
| otherwise specified, until another USE command is issued or the |
| connection terminates. |
| |
| As always, when a keyspace name does not work as a normal identifier or |
| number, it can be enclosed in quotes and expressed as a string literal. |
| """ |
| ksname = parsed.get_binding('ksname') |
| if self.perform_statement(parsed.extract_orig()): |
| self.current_keyspace = cql_dequote(ksname) |
| |
| def do_select(self, parsed): |
| """ |
| SELECT [FIRST n] [REVERSED] <selectExpr> |
| FROM [<keyspace>.]<columnFamily> |
| [USING CONSISTENCY <consistencylevel>] |
| [WHERE <clause>] |
| [LIMIT m]; |
| |
| SELECT is used to read one or more records from a Cassandra column |
| family. It returns a result-set of rows, where each row consists of a |
| key and a collection of columns corresponding to the query. |
| |
| For more information, see one of the following: |
| |
| HELP SELECT_EXPR |
| HELP SELECT_COLUMNFAMILY |
| HELP SELECT_WHERE |
| HELP SELECT_LIMIT |
| HELP CONSISTENCYLEVEL |
| """ |
| ksname = parsed.get_binding('selectks') |
| if ksname is not None: |
| ksname = cql_dequote(ksname) |
| cfname = cql_dequote(parsed.get_binding('selectsource')) |
| decoder = self.determine_decoder_for(cfname, ksname=ksname) |
| self.perform_statement(parsed.extract_orig(), decoder=decoder) |
| |
| def perform_statement(self, statement, decoder=None): |
| if not statement: |
| return False |
| trynum = 1 |
| while True: |
| try: |
| self.cursor.execute(statement, decoder=decoder) |
| break |
| except cql.IntegrityError, err: |
| self.printerr("Attempt #%d: %s" % (trynum, str(err))) |
| trynum += 1 |
| if trynum > self.num_retries: |
| return False |
| time.sleep(1*trynum) |
| except CQL_ERRORS, err: |
| self.printerr(str(err)) |
| return False |
| except Exception, err: |
| import traceback |
| self.printerr(traceback.format_exc()) |
| return False |
| |
| if self.cursor.description is _COUNT_DESCRIPTION: |
| self.print_count_result() |
| elif self.cursor.description is not _VOID_DESCRIPTION: |
| self.print_result() |
| return True |
| |
| def determine_decoder_for(self, cfname, ksname=None): |
| if ksname is None: |
| ksname = self.current_keyspace |
| schema = self.schema_overrides.get((ksname, cfname), None) |
| if schema: |
| def use_my_schema_decoder(real_schema): |
| return cql.decoders.SchemaDecoder(schema.join(real_schema)) |
| return use_my_schema_decoder |
| |
| def print_count_result(self): |
| if not self.cursor.result: |
| return |
| self.writeresult('count') |
| self.writeresult('-----') |
| self.writeresult(self.cursor.result[0]) |
| self.writeresult("") |
| |
| def print_result(self): |
| # first pass: see if we have a static column set |
| last_description = None |
| for row in self.cursor: |
| if last_description is not None and self.cursor.description != last_description: |
| static = False |
| break |
| last_description = self.cursor.description |
| else: |
| static = True |
| self.cursor._reset() |
| |
| if static: |
| self.print_static_result() |
| else: |
| self.print_dynamic_result() |
| self.writeresult("") |
| |
| def print_static_result(self): |
| colnames, coltypes = zip(*self.cursor.description)[:2] |
| formatted_data = [map(self.myformat_value, row, coltypes) for row in self.cursor] |
| |
| # determine column widths |
| colnames = map(str, colnames) |
| widths = map(len, colnames) |
| for fmtrow in formatted_data: |
| for num, col in enumerate(fmtrow): |
| widths[num] = max(widths[num], len(col.strval)) |
| |
| # print header |
| header = ' | '.join(self.applycolor(name.ljust(w), MAGENTA) for (name, w) in zip(colnames, widths)) |
| self.writeresult(' ' + header.rstrip()) |
| self.writeresult('-%s-' % '-+-'.join('-' * w for w in widths)) |
| |
| # print row data |
| for row in formatted_data: |
| line = ' | '.join(col.color_rjust(w) for (col, w) in zip(row, widths)) |
| self.writeresult(' ' + line) |
| |
| def print_dynamic_result(self): |
| for row in self.cursor: |
| colnames, coltypes = zip(*self.cursor.description)[:2] |
| colnames = [self.applycolor(name, MAGENTA) for name in colnames] |
| colvals = [self.myformat_value(val, casstype) for (val, casstype) in zip(row, coltypes)] |
| line = ' | '.join(name + ',' + col.coloredval for (col, name) in zip(colvals, colnames)) |
| self.writeresult(' ' + line) |
| |
| def emptyline(self): |
| pass |
| |
| def parseline(self, line): |
| # this shouldn't be needed |
| raise NotImplementedError |
| |
| def complete(self, text, state): |
| if readline is None: |
| return |
| if state == 0: |
| try: |
| self.completion_matches = self.find_completions(text) |
| except Exception: |
| if debug_completion: |
| import traceback |
| traceback.print_exc() |
| else: |
| raise |
| try: |
| return self.completion_matches[state] |
| except IndexError: |
| return None |
| |
| def find_completions(self, text): |
| curline = readline.get_line_buffer() |
| prevlines = self.statement.getvalue() |
| wholestmt = prevlines + curline |
| begidx = readline.get_begidx() + len(prevlines) |
| endidx = readline.get_endidx() + len(prevlines) |
| stuff_to_complete = wholestmt[:begidx] |
| return cqlhandling.cql_complete(stuff_to_complete, text, cassandra_conn=self, |
| debug=debug_completion, startsymbol='cqlshCommand') |
| |
| def set_prompt(self, prompt): |
| if self.prompt != '': |
| self.prompt = prompt |
| |
| def print_recreate_keyspace(self, ksdef, out): |
| stratclass = trim_if_present(ksdef.strategy_class, 'org.apache.cassandra.locator.') |
| ksname = maybe_cql_escape(ksdef.name) |
| out.write("CREATE KEYSPACE %s WITH strategy_class = %s" |
| % (ksname, cql_escape(stratclass))) |
| for opname, opval in ksdef.strategy_options.iteritems(): |
| out.write("\n AND strategy_options:%s = %s" % (opname, cql_escape(opval))) |
| out.write(';\n') |
| |
| if ksdef.cf_defs: |
| out.write('\nUSE %s;\n' % ksname) |
| for cf in ksdef.cf_defs: |
| out.write('\n') |
| self.print_recreate_columnfamily(cf, out) |
| |
| def print_recreate_columnfamily(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' |
| keytype = cql_typename(cfdef.key_validation_class) |
| out.write(" %s %s PRIMARY KEY" % (alias, keytype)) |
| indexed_columns = [] |
| for col in cfdef.column_metadata: |
| colname = maybe_cql_escape(col.name) |
| out.write(",\n %s %s" % (colname, cql_typename(col.validation_class))) |
| if col.index_name is not None: |
| indexed_columns.append(col) |
| notable_columns = [] |
| for (option, thriftname) in cqlhandling.columnfamily_options: |
| optval = getattr(cfdef, thriftname or option) |
| if option in ('comparator', 'default_validation'): |
| optval = cql_typename(optval) |
| elif option == 'row_cache_provider': |
| optval = cql_escape(trim_if_present(optval, 'org.apache.cassandra.cache.')) |
| else: |
| optval = cql_escape(optval) |
| notable_columns.append((option, optval)) |
| for option, thriftname, _ in cqlhandling.columnfamily_map_options: |
| optmap = getattr(cfdef, thriftname or option) |
| for k, v in optmap.items(): |
| notable_columns.append(('%s:%s' % (option, k), cql_escape(v))) |
| out.write('\n)') |
| if notable_columns: |
| joiner = 'WITH' |
| for optname, optval in notable_columns: |
| 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_cql_escape(col.name))) |
| |
| def describe_keyspace(self, ksname): |
| print |
| self.print_recreate_keyspace(self.get_keyspace(ksname), sys.stdout) |
| print |
| |
| def describe_columnfamily(self, cfname): |
| print |
| self.print_recreate_columnfamily(self.get_columnfamily(cfname), sys.stdout) |
| print |
| |
| def describe_columnfamilies(self, ksname): |
| if ksname is None: |
| for k in self.get_keyspaces(): |
| print 'Keyspace %s' % (k.name,) |
| print '---------%s\n' % ('-' * len(k.name)) |
| cmd.Cmd.columnize(self, [c.name for c in k.cf_defs]) |
| print |
| else: |
| try: |
| names = self.get_columnfamily_names(ksname) |
| except cql.cassandra.ttypes.NotFoundException: |
| raise KeyspaceNotFound('Keyspace %s not found.' % (ksname,)) |
| print |
| cmd.Cmd.columnize(self, names) |
| print |
| |
| def describe_cluster(self): |
| print 'Cluster: %s' % self.get_cluster_name() |
| p = trim_if_present(self.get_partitioner(), 'org.apache.cassandra.dht.') |
| print 'Partitioner: %s' % p |
| snitch = trim_if_present(self.get_snitch(), 'org.apache.cassandra.locator.') |
| print 'Snitch: %s\n' % snitch |
| if self.current_keyspace is not None and self.current_keyspace != 'system': |
| print "Range ownership:" |
| ring = self.get_ring() |
| for entry in ring: |
| print ' %39s [%s]' % (entry.start_token, ', '.join(entry.endpoints)) |
| print |
| |
| def describe_schema(self): |
| print |
| for k in self.get_keyspaces(): |
| self.print_recreate_keyspace(k, sys.stdout) |
| print |
| |
| def do_describe(self, parsed): |
| """ |
| DESCRIBE [cqlsh only] |
| |
| (DESC may be used as a shorthand.) |
| |
| Outputs information about the connected Cassandra cluster, or about |
| the data stored on it. Use in one of the following ways: |
| |
| DESCRIBE KEYSPACE [<keyspacename>] |
| |
| Output CQL commands that could be used to recreate the given |
| keyspace, and the columnfamilies in it. In some cases, as the CQL |
| interface matures, there will be some metadata about a keyspace that |
| is not representable with CQL. That metadata will not be shown. |
| |
| The '<keyspacename>' argument may be omitted when using a non-system |
| keyspace; in that case, the current keyspace will be described. |
| |
| DESCRIBE COLUMNFAMILIES |
| |
| Output the names of all column families in the current keyspace, or |
| in all keyspaces if there is no current keyspace. |
| |
| DESCRIBE COLUMNFAMILY <columnfamilyname> |
| |
| Output CQL commands that could be used to recreate the given |
| columnfamily. In some cases, as above, there may be columnfamily |
| metadata which is not representable and which will not be shown. |
| |
| DESCRIBE CLUSTER |
| |
| Output information about the connected Cassandra cluster, such as the |
| cluster name, and the partitioner and snitch in use. When you are |
| connected to a non-system keyspace, also shows endpoint-range |
| ownership information for the Cassandra ring. |
| |
| DESCRIBE SCHEMA |
| |
| Output CQL commands that could be used to recreate the entire schema. |
| Works as though "DESCRIBE KEYSPACE k" was invoked for each keyspace |
| k. |
| """ |
| |
| what = parsed.matched[1][1].lower() |
| if what == 'keyspace': |
| ksname = cql_dequote(parsed.get_binding('ksname', '')) |
| if not ksname: |
| ksname = self.current_keyspace |
| if ksname is None: |
| self.printerr('Not in any keyspace.') |
| return |
| self.describe_keyspace(ksname) |
| elif what == 'columnfamily': |
| cfname = cql_dequote(parsed.get_binding('cfname')) |
| self.describe_columnfamily(cfname) |
| elif what == 'columnfamilies': |
| self.describe_columnfamilies(self.current_keyspace) |
| elif what == 'cluster': |
| self.describe_cluster() |
| elif what == 'schema': |
| self.describe_schema() |
| |
| do_desc = do_describe |
| |
| def do_show(self, parsed): |
| """ |
| SHOW [cqlsh only] |
| |
| Displays information about the current cqlsh session. Can be called in |
| the following ways: |
| |
| SHOW VERSION |
| |
| Shows the version and build of the connected Cassandra instance, as |
| well as the versions of the CQL spec and the Thrift protocol that |
| the connected Cassandra instance understands. |
| |
| SHOW HOST |
| |
| Shows where cqlsh is currently connected. |
| |
| SHOW ASSUMPTIONS |
| |
| Outputs the current list of type assumptions as specified by the |
| user. See the help for the ASSUME command for more information. |
| """ |
| |
| showwhat = parsed.get_binding('what').lower() |
| if showwhat == 'version': |
| self.show_version() |
| elif showwhat == 'host': |
| self.show_host() |
| elif showwhat == 'assumptions': |
| self.show_assumptions() |
| else: |
| self.printerr('Wait, how do I show %r?' % (showwhat,)) |
| |
| def do_assume(self, parsed): |
| """ |
| ASSUME [cqlsh only] |
| |
| Instruct cqlsh to consider certain column names or values to be of a |
| specified type, even if that type information is not specified in |
| the columnfamily's metadata. Data will be deserialized according to |
| the given type, and displayed appropriately when retrieved. |
| |
| Use thus: |
| |
| ASSUME [<keyspace>.]<columnfamily> NAMES ARE <type>; |
| |
| Treat all column names in the given columnfamily as being of the |
| given type. |
| |
| ASSUME [<keyspace>.]<columnfamily> VALUES ARE <type>; |
| |
| Treat all column values in the given columnfamily as being of the |
| given type, unless there is more information about the specific |
| column being deserialized. That is, a column-specific ASSUME will |
| take precedence here, as will column-specific metadata in the |
| columnfamily's definition. |
| |
| ASSUME [<keyspace>.]<columnfamily>(<colname>) VALUES ARE <type>; |
| |
| Treat all values in the given column in the given columnfamily as |
| being of the specified type. This overrides any other information |
| about the type of a value. |
| |
| Assign multiple overrides at once for the same columnfamily by |
| separating with commas: |
| |
| ASSUME ks.cf NAMES ARE uuid, VALUES ARE int, (col) VALUES ARE ascii |
| |
| See HELP TYPES for information on the supported data storage types. |
| """ |
| |
| params = {} |
| for paramname in ('ks', 'cf', 'colname', 'names', 'values', 'colvalues'): |
| val = parsed.get_binding(paramname, None) |
| if val is not None: |
| val = cql_dequote(val) |
| params[paramname] = val |
| |
| if params['ks'] is None: |
| if self.current_keyspace is None: |
| self.printerr('Error: not in any keyspace.') |
| return |
| params['ks'] = self.current_keyspace |
| |
| for overridetype in ('names', 'values', 'colvalues'): |
| cqltype = params[overridetype] |
| if cqltype is None: |
| continue |
| try: |
| validator_class = cqlhandling.find_validator_class(cqltype) |
| except KeyError: |
| self.printerr('Error: validator type %s not found.' % cqltype) |
| self.add_assumption(params['ks'], params['cf'], params['colname'], |
| overridetype, validator_class) |
| |
| def do_source(self, parsed): |
| """ |
| SOURCE [cqlsh only] |
| |
| Executes a file containing CQL statements. Gives the output for each |
| statement in turn, if any, or any errors that occur along the way. |
| |
| Errors do NOT abort execution of the CQL source file. |
| |
| Usage: |
| |
| SOURCE '<file>'; |
| |
| That is, the path to the file to be executed must be given inside a |
| string literal. The path is interpreted relative to the current working |
| directory. The tilde shorthand notation ("~/mydir") is supported for |
| referring to $HOME. |
| |
| See also the --file option to cqlsh. |
| """ |
| |
| fname = parsed.get_binding('fname') |
| fname = os.path.expanduser(cql_dequote(fname)) |
| try: |
| f = open(fname, 'r') |
| except IOError, e: |
| self.printerr('Could not open %r: %s' % (fname, e)) |
| return |
| subshell = Shell(self.hostname, self.port, color=self.color, |
| encoding=self.encoding, stdin=f, tty=False, |
| use_conn=self.conn) |
| subshell.cmdloop() |
| f.close() |
| |
| def do_capture(self, parsed): |
| """ |
| CAPTURE [cqlsh only] |
| |
| Begins capturing command output and appending it to a specified file. |
| Output will not be shown at the console while it is captured. |
| |
| Usage: |
| |
| CAPTURE '<file>'; |
| CAPTURE OFF; |
| CAPTURE; |
| |
| That is, the path to the file to be executed must be given inside a |
| string literal. The path is interpreted relative to the current working |
| directory. The tilde shorthand notation ("~/mydir") is supported for |
| referring to $HOME. |
| |
| Only query result output is captured. Errors and output from cqlsh-only |
| commands will still be shown in the cqlsh session. |
| |
| To stop capturing output and show it in the cqlsh session again, use |
| CAPTURE OFF. |
| |
| To inspect the current capture configuration, use CAPTURE with no |
| arguments. |
| """ |
| |
| fname = parsed.get_binding('fname') |
| if fname is None: |
| if self.shunted_query_out is not None: |
| print "Currently capturing query output to %r." % (self.query_out.name,) |
| else: |
| print "Currently not capturing query output." |
| return |
| |
| if fname.upper() == 'OFF': |
| if self.shunted_query_out is None: |
| self.printerr('Not currently capturing output.') |
| return |
| self.query_out.close() |
| self.query_out = self.shunted_query_out |
| self.color = self.shunted_color |
| self.shunted_query_out = None |
| del self.shunted_color |
| return |
| |
| if self.shunted_query_out is not None: |
| self.printerr('Already capturing output to %s. Use CAPTURE OFF' |
| ' to disable.' % (self.query_out.name,)) |
| return |
| |
| fname = os.path.expanduser(cql_dequote(fname)) |
| try: |
| f = open(fname, 'a') |
| except IOError, e: |
| self.printerr('Could not open %r for append: %s' % (fname, e)) |
| return |
| self.shunted_query_out = self.query_out |
| self.shunted_color = self.color |
| self.query_out = f |
| self.color = False |
| print 'Now capturing query output to %r.' % (fname,) |
| |
| def do_exit(self, parsed=None): |
| """ |
| EXIT/QUIT [cqlsh only] |
| |
| Exits cqlsh. |
| """ |
| self.stop = True |
| do_quit = do_exit |
| |
| def get_names(self): |
| names = cmd.Cmd.get_names(self) |
| for hide_from_help in ('do_quit',): |
| names.remove(hide_from_help) |
| return names |
| |
| def columnize(self, slist, *a, **kw): |
| return cmd.Cmd.columnize(self, [u.upper() for u in slist], *a, **kw) |
| |
| def do_help(self, parsed): |
| """ |
| HELP [cqlsh only] |
| |
| Gives information about cqlsh commands. To see available topics, |
| enter "HELP" without any arguments. To see help on a topic, |
| use "HELP <topic>". |
| """ |
| topics = parsed.get_binding('topic', ()) |
| if not topics: |
| return cmd.Cmd.do_help(self, '') |
| for t in topics: |
| cmd.Cmd.do_help(self, cql_dequote(t).lower()) |
| |
| def help_types(self): |
| print "\n CQL types recognized by this version of cqlsh:\n" |
| for t in cqlhandling.cql_types: |
| print ' ' + t |
| print """ |
| For information on the various recognizable input formats for these |
| types, or on controlling the formatting of cqlsh query output, see |
| one of the following topics: |
| |
| HELP TIMESTAMP_INPUT |
| HELP BLOB_INPUT |
| HELP UUID_INPUT |
| HELP BOOLEAN_INPUT |
| |
| HELP TEXT_OUTPUT |
| HELP TIMESTAMP_OUTPUT |
| """ |
| |
| def help_timestamp_input(self): |
| print """ |
| Timestamp input |
| |
| CQL supports any of the following ISO 8601 formats for timestamp |
| specification: |
| |
| yyyy-mm-dd HH:mm |
| yyyy-mm-dd HH:mm:ss |
| yyyy-mm-dd HH:mmZ |
| yyyy-mm-dd HH:mm:ssZ |
| yyyy-mm-dd'T'HH:mm |
| yyyy-mm-dd'T'HH:mmZ |
| yyyy-mm-dd'T'HH:mm:ss |
| yyyy-mm-dd'T'HH:mm:ssZ |
| yyyy-mm-dd |
| yyyy-mm-ddZ |
| |
| The Z in these formats refers to an RFC-822 4-digit time zone, |
| expressing the time zone's difference from UTC. For example, a |
| timestamp in Pacific Standard Time might be given thus: |
| |
| 2012-01-20 16:14:12-0800 |
| |
| If no time zone is supplied, the current time zone for the Cassandra |
| server node will be used. |
| """ |
| |
| def help_blob_input(self): |
| print """ |
| Blob input |
| |
| CQL blob data must be specified in a string literal as hexidecimal |
| data. Example: to store the ASCII values for the characters in the |
| string "CQL", use '43514c'. |
| """ |
| |
| def help_uuid_input(self): |
| print """ |
| UUID input |
| |
| UUIDs may be specified in CQL using 32 hexidecimal characters, |
| split up using dashes in the standard UUID format: |
| |
| XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX |
| """ |
| |
| def help_boolean_input(self): |
| print """ |
| Boolean input |
| |
| CQL accepts the strings 'true' and 'false' (case insensitive) |
| as input for boolean types. |
| """ |
| |
| def help_text_output(self): |
| print """ |
| Textual output |
| |
| When control characters, or other characters which can't be encoded |
| in your current locale, are found in values of 'text' or 'ascii' |
| types, it will be shown as a backslash escape. If color is enabled, |
| any such backslash escapes will be shown in a different color from |
| the surrounding text. |
| |
| Unicode code points in your data will be output intact, if the |
| encoding for your locale is capable of decoding them. If you prefer |
| that non-ascii characters be shown with Python-style "\\uABCD" |
| escape sequences, invoke cqlsh with an ASCII locale (for example, |
| by setting the $LANG environment variable to "C"). |
| """ |
| |
| help_ascii_output = help_text_output |
| |
| def help_timestamp_output(self): |
| print """ |
| Timestamp output |
| |
| Cqlsh will display timestamps in this format: |
| |
| yyyy-mm-dd HH:mm:ssZ |
| |
| which is a format acceptable as CQL timestamp input as well. It is |
| planned that cqlsh should allow the user to change that output format |
| if desired, but that feature is not yet available. |
| """ |
| |
| def help_select_expr(self): |
| print """ |
| SELECT: Specifying Columns |
| |
| SELECT [FIRST n] [REVERSED] name1, name2, name3 FROM ... |
| SELECT [FIRST n] [REVERSED] name1..nameN FROM ... |
| SELECT COUNT(*) FROM ... |
| |
| The SELECT expression determines which columns will appear in the |
| results and takes the form of either a comma separated list of names, |
| or a range. The range notation consists of a start and end column name |
| separated by two periods (..). The set of columns returned for a |
| range is start and end inclusive. |
| |
| The FIRST option accepts an integer argument and can be used to apply a |
| limit to the number of columns returned per row. When this limit is |
| left unset, it defaults to 10,000 columns. |
| |
| The REVERSED option causes the sort order of the results to be |
| reversed. |
| |
| It is worth noting that unlike the projection in a SQL SELECT, there is |
| no guarantee that the results will contain all of the columns |
| specified. This is because Cassandra is schema-less and there are no |
| guarantees that a given column exists. |
| |
| When the COUNT aggregate function is specified as a column to fetch, a |
| single row will be returned, with a single column named "count" whose |
| value is the number of rows from the pre-aggregation resultset. |
| |
| Currently, COUNT is the only function supported by CQL. |
| """ |
| |
| def help_select_columnfamily(self): |
| print """ |
| SELECT: Specifying Column Family |
| |
| SELECT ... FROM [<keyspace>.]<columnFamily> ... |
| |
| The FROM clause is used to specify the Cassandra column family |
| applicable to a SELECT query. The keyspace in which the column family |
| exists can optionally be specified along with the column family name, |
| separated by a dot (.). This will not change the current keyspace of |
| the session (see HELP USE). |
| """ |
| |
| def help_select_where(self): |
| print """ |
| SELECT: Filtering rows |
| |
| SELECT ... WHERE <key> = keyname AND name1 = value1 |
| SELECT ... WHERE <key> >= startkey and <key> =< endkey AND name1 = value1 |
| SELECT ... WHERE <key> IN ('<key>', '<key>', '<key>', ...) |
| |
| The WHERE clause provides for filtering the rows that appear in |
| results. The clause can filter on a key name, or range of keys, and in |
| the case of indexed columns, on column values. Key filters are |
| specified using the KEY keyword or key alias name, a relational |
| operator (one of =, >, >=, <, and <=), and a term value. When terms |
| appear on both sides of a relational operator it is assumed the filter |
| applies to an indexed column. With column index filters, the term on |
| the left of the operator is the name, the term on the right is the |
| value to filter _on_. |
| |
| Note: The greater-than and less-than operators (> and <) result in key |
| ranges that are inclusive of the terms. There is no supported notion of |
| "strictly" greater-than or less-than; these operators are merely |
| supported as aliases to >= and <=. |
| """ |
| |
| def help_select_limit(self): |
| print """ |
| SELECT: Limiting results |
| |
| SELECT ... WHERE <clause> [LIMIT n] ... |
| |
| Limiting the number of rows returned can be achieved by adding the |
| LIMIT option to a SELECT expression. LIMIT defaults to 10,000 when left |
| unset. |
| """ |
| |
| def help_consistencylevel(self): |
| print """ |
| Consistency Level Specification |
| |
| ... USING CONSISTENCY <consistencylevel> ... |
| |
| Consistency level specifications are made up of keyword USING, |
| followed by a consistency level identifier. Valid consistency level |
| identifiers are as follows: |
| |
| * ANY |
| * ONE (default) |
| * QUORUM |
| * ALL |
| * LOCAL_QUORUM |
| * EACH_QUORUM |
| |
| For more information on how consistency levels work, consult your |
| Cassandra documentation. |
| """ |
| |
| def help_insert(self): |
| print """ |
| INSERT INTO <columnFamily> |
| ( <keyname>, <colname1> [, <colname2> [, ...]] ) |
| VALUES ( <keyval>, <colval1> [, <colval2> [, ...]] ) |
| [USING CONSISTENCY <consistencylevel> |
| [AND TIMESTAMP <timestamp>] |
| [AND TTL <timeToLive]]; |
| |
| An INSERT is used to write one or more columns to a record in a |
| Cassandra column family. No results are returned. |
| |
| The first column name in the INSERT list must be the name of the |
| column family key. Also, there must be more than one column name |
| specified (Cassandra rows are not considered to exist with only |
| a key and no associated columns). |
| |
| Unlike in SQL, the semantics of INSERT and UPDATE are identical. |
| In either case a record is created if none existed before, and |
| udpated when it does. For more information, see one of the |
| following: |
| |
| HELP UPDATE |
| HELP UPDATE_USING |
| HELP CONSISTENCYLEVEL |
| """ |
| |
| def help_update(self): |
| print """ |
| UPDATE <columnFamily> [USING CONSISTENCY <consistencylevel> |
| [AND TIMESTAMP <timestamp>] |
| [AND TTL <timeToLive>]] |
| SET name1 = value1, name2 = value2 WHERE <KEY> = keyname; |
| |
| An UPDATE is used to write one or more columns to a record in a |
| Cassandra column family. No results are returned. Key can be given |
| using the KEY keyword or by an alias set per columnfamily. |
| |
| Statements begin with the UPDATE keyword followed by a Cassandra |
| column family name. |
| |
| For more information, see one of the following: |
| |
| HELP UPDATE_USING |
| HELP UPDATE_SET |
| HELP UPDATE_COUNTERS |
| HELP UPDATE_WHERE |
| HELP CONSISTENCYLEVEL |
| """ |
| |
| def help_update_using(self): |
| print """ |
| UPDATE: the USING clause |
| |
| UPDATE ... USING TIMESTAMP <timestamp>; |
| UPDATE ... USING TTL <timeToLive>; |
| UPDATE ... USING CONSISTENCY <consistencylevel>; |
| |
| The USING clause allows setting of certain query and data parameters. |
| If multiple parameters need to be set, these may be joined using AND. |
| Example: |
| |
| UPDATE ... USING TTL 43200 AND CONSISTENCY LOCAL_QUORUM; |
| |
| <timestamp> defines the optional timestamp for the new column value(s). |
| It must be an integer. |
| |
| <timeToLive> defines the optional time to live (TTL) for the new column |
| value(s). It must be an integer. |
| """ |
| |
| def help_update_set(self): |
| print """ |
| UPDATE: Specifying Columns and Row |
| |
| UPDATE ... SET name1 = value1, name2 = value2 |
| WHERE <key> = keyname; |
| UPDATE ... SET name1 = value1, name2 = value2 |
| WHERE <key> IN ('<key1>', '<key2>', ...) |
| |
| Rows are created or updated by supplying column names and values in |
| term assignment format. Multiple columns can be set by separating the |
| name/value pairs using commas. |
| """ |
| |
| def help_update_counters(self): |
| print """ |
| UPDATE: Updating Counter Columns |
| |
| UPDATE ... SET name1 = name1 + <value> ... |
| UPDATE ... SET name1 = name1 - <value> ... |
| |
| Counter columns can be incremented or decremented by an arbitrary |
| numeric value though the assignment of an expression that adds or |
| substracts the value. |
| """ |
| |
| def help_update_where(self): |
| print """ |
| UPDATE: Selecting rows to update |
| |
| UPDATE ... WHERE <keyname> = <keyval>; |
| UPDATE ... WHERE <keyname> IN (<keyval1>, <keyval2>, ...); |
| |
| Each update statement requires a precise set of keys to be specified |
| using a WHERE clause. |
| |
| <keyname> can be the keyword KEY or the key alias for the column |
| family. |
| """ |
| |
| def help_delete(self): |
| print """ |
| DELETE [<col1> [, <col2>, ...] FROM <columnFamily> |
| [USING CONSISTENCY <consistencylevel> |
| [AND TIMESTAMP <timestamp>]] |
| WHERE <keyname> = <keyvalue>; |
| |
| A DELETE is used to perform the removal of one or more columns from one |
| or more rows. Each DELETE statement requires a precise set of row keys |
| to be specified using a WHERE clause and the KEY keyword or key alias. |
| |
| For more information, see one of the following: |
| |
| HELP DELETE_USING |
| HELP DELETE_COLUMNS |
| HELP DELETE_WHERE |
| HELP CONSISTENCYLEVEL |
| """ |
| |
| def help_delete_using(self): |
| print """ |
| DELETE: the USING clause |
| |
| DELETE ... USING CONSISTENCY <consistencylevel>; |
| DELETE ... USING TIMESTAMP <timestamp>; |
| |
| The USING clause allows setting of certain query and data parameters. |
| If multiple parameters need to be set, these may be joined using AND. |
| Example: |
| |
| DELETE ... CONSISTENCY LOCAL_QUORUM AND TIMESTAMP 1318452291034; |
| |
| <timestamp> defines the optional timestamp for the new tombstone |
| record. It must be an integer. |
| """ |
| |
| def help_delete_columns(self): |
| print """ |
| DELETE: specifying columns |
| |
| DELETE col1, 'col2 name', 3 FROM ... |
| |
| Following the DELETE keyword is an optional comma-delimited list of |
| column name terms. When no column names are given, the remove applies |
| to the entire row(s) matched by the WHERE clause. |
| """ |
| |
| def help_delete_where(self): |
| print """ |
| DELETE: specifying rows |
| |
| DELETE ... WHERE KEY = 'some_key_value'; |
| DELETE ... WHERE keyalias IN (key1, key2); |
| |
| The WHERE clause is used to determine to which row(s) a DELETE |
| applies. The first form allows the specification of a single keyname |
| using the KEY keyword (or the key alias) and the = operator. The |
| second form allows a list of keyname terms to be specified using the |
| IN operator and a parenthesized list of comma-delimited keyname |
| terms. |
| """ |
| |
| def help_create(self): |
| print """ |
| There are different variants of CREATE. For more information, see |
| one of the following: |
| |
| HELP CREATE_KEYSPACE; |
| HELP CREATE_COLUMNFAMILY; |
| HELP CREATE_INDEX; |
| """ |
| |
| def help_create_keyspace(self): |
| print """ |
| CREATE KEYSPACE <ksname> WITH strategy_class = '<strategy>' |
| [AND strategy_options:<option> = <val>]; |
| |
| The CREATE KEYSPACE statement creates a new top-level namespace (aka |
| "keyspace"). Valid names are any string constructed of alphanumeric |
| characters and underscores. Names which do not work as valid |
| identifiers or integers should be quoted as string literals. Properties |
| such as replication strategy and count are specified during creation |
| using the following accepted keyword arguments: |
| |
| strategy_class [required]: The name of the replication strategy class |
| which should be used for the new keyspace. Some often-used classes |
| are SimpleStrategy and NetworkTopologyStrategy. |
| |
| strategy_options: Most strategies require additional arguments which |
| can be supplied by appending the option name to "strategy_options", |
| separated by a colon (:). For example, a strategy option of "DC1" |
| with a value of "1" would be specified as "strategy_options:DC1 = 1". |
| The replication factor option for SimpleStrategy could be |
| "strategy_options:replication_factor=3". |
| """ |
| |
| def help_create_columnfamily(self): |
| print """ |
| CREATE COLUMNFAMILY <cfname> ( <colname> <type> PRIMARY KEY [, |
| <colname> <type> [, ...]] ) |
| [WITH <optionname> = <val> [AND <optionname> = <val> [...]]]; |
| |
| CREATE COLUMNFAMILY statements create a new column family under the |
| current keyspace. Valid column family names are strings of alphanumeric |
| characters and underscores, which begin with a letter. |
| |
| Each columnfamily requires at least one column definition and type, |
| which will correspond to the columnfamily key and key validator. It's |
| important to note that the key type you use must be compatible with the |
| partitioner in use. For example, OrderPreservingPartitioner and |
| CollatingOrderPreservingPartitioner both require UTF-8 keys. If you |
| use an identifier for the primary key name, instead of the KEY |
| keyword, a key alias will be set automatically. |
| |
| For more information, see one of the following: |
| |
| HELP CREATE_COLUMNFAMILY_TYPES; |
| HELP CREATE_COLUMNFAMILY_OPTIONS; |
| """ |
| |
| def help_create_columnfamily_types(self): |
| print """ |
| CREATE COLUMNFAMILY: Specifying column types |
| |
| CREATE ... (KEY <type> PRIMARY KEY, |
| othercol <type>) ... |
| |
| It is possible to assign columns a type during column family creation. |
| Columns configured with a type are validated accordingly when a write |
| occurs, and intelligent CQL drivers and interfaces will be able to |
| decode the column values correctly when receiving them. Column types |
| are specified as a parenthesized, comma-separated list of column term |
| and type pairs. See HELP TYPES; for the list of recognized types. |
| """ |
| |
| def help_create_columnfamily_options(self): |
| print """ |
| CREATE COLUMNFAMILY: Specifying columnfamily options |
| |
| CREATE COLUMNFAMILY blah (...) |
| WITH optionname = val AND otheroption = val2; |
| |
| A number of optional keyword arguments can be supplied to control the |
| configuration of a new column family, such as the size of the |
| associated row and key caches. Consult your CQL reference for the |
| complete list of options and possible values. |
| """ |
| |
| def help_create_index(self): |
| print """ |
| CREATE INDEX [<indexname>] ON <cfname> ( <colname> ); |
| |
| A CREATE INDEX statement is used to create a new, automatic secondary |
| index on the given column family, for the named column. A name for the |
| index itself can be specified before the ON keyword, if desired. A |
| single column name must be specified inside the parentheses. It is not |
| necessary for the column to exist on any current rows (Cassandra is |
| schemaless), but the column must already have a type (specified during |
| the CREATE COLUMNFAMILY, or added afterwards with ALTER COLUMNFAMILY. |
| """ |
| |
| def help_drop(self): |
| print """ |
| There are different variants of DROP. For more information, see |
| one of the following: |
| |
| HELP DROP_KEYSPACE; |
| HELP DROP_COLUMNFAMILY; |
| HELP DROP_INDEX; |
| """ |
| |
| def help_drop_keyspace(self): |
| print """ |
| DROP KEYSPACE <keyspacename>; |
| |
| A DROP KEYSPACE statement results in the immediate, irreversible |
| removal of a keyspace, including all column families in it, and all |
| data contained in those column families. |
| """ |
| |
| def help_drop_columnfamily(self): |
| print """ |
| DROP COLUMNFAMILY <columnfamilyname>; |
| |
| A DROP COLUMNFAMILY statement results in the immediate, irreversible |
| removal of a column family, including all data contained in it. |
| """ |
| |
| def help_drop_index(self): |
| print """ |
| DROP INDEX <indexname>; |
| |
| A DROP INDEX statement is used to drop an existing secondary index. |
| """ |
| |
| def help_truncate(self): |
| print """ |
| TRUNCATE <columnfamilyname>; |
| |
| TRUNCATE accepts a single argument for the column family name, and |
| permanently removes all data from said column family. |
| """ |
| |
| def help_begin(self): |
| print """ |
| BEGIN BATCH [USING CONSISTENCY <level> |
| [AND TIMESTAMP <timestamp>]] |
| <insert or update or delete statement> ; |
| [ <another insert or update or delete statement ; |
| [...]] |
| APPLY BATCH; |
| |
| BATCH supports setting a client-supplied optional global timestamp |
| which will be used for each of the operations included in the batch. |
| |
| A single consistency level is used for the entire batch. It appears |
| after the BEGIN BATCH statement, and uses the standard "consistency |
| level specification" (see HELP CONSISTENCYLEVEL). Batched statements |
| default to CONSISTENCY.ONE when left unspecified. |
| |
| Only data modification statements (specifically, UPDATE, INSERT, |
| and DELETE) are allowed in a BATCH statement. BATCH is _not_ an |
| analogue for SQL transactions. |
| |
| _NOTE: While there are no isolation guarantees, UPDATE queries are |
| atomic within a given record._ |
| """ |
| help_apply = help_begin |
| |
| def help_alter(self): |
| print """ |
| ALTER COLUMNFAMILY <cfname> ALTER <columnname> TYPE <type>; |
| ALTER COLUMNFAMILY <cfname> ADD <columnname> <type>; |
| ALTER COLUMNFAMILY <cfname> DROP <columnname>; |
| ALTER COLUMNFAMILY <cfname> WITH <optionname> = <val> [AND <optionname> = <val> [...]]; |
| |
| An ALTER statement is used to manipulate column family column |
| metadata. It allows you to add new columns, drop existing columns, |
| change the data storage type of existing columns, or change column |
| family properties. No results are returned. |
| |
| See one of the following for more information: |
| |
| HELP ALTER_ALTER; |
| HELP ALTER_ADD; |
| HELP ALTER_DROP; |
| HELP ALTER_WITH; |
| """ |
| |
| def help_alter_alter(self): |
| print """ |
| ALTER COLUMNFAMILY: altering existing typed columns |
| |
| ALTER COLUMNFAMILY addamsFamily ALTER lastKnownLocation TYPE uuid; |
| |
| ALTER COLUMNFAMILY ... ALTER changes the expected storage type for a |
| column. The column must already have a type in the column family |
| metadata. The column may or may not already exist in current rows-- but |
| be aware that no validation of existing data is done. The bytes stored |
| in values for that column will remain unchanged, and if existing data |
| is not deserializable according to the new type, this may cause your |
| CQL driver or interface to report errors. |
| """ |
| |
| def help_alter_add(self): |
| print """ |
| ALTER COLUMNFAMILY: adding a typed column |
| |
| ALTER COLUMNFAMILY addamsFamily ADD gravesite varchar; |
| |
| The ALTER COLUMNFAMILY ... ADD variant adds a typed column to a column |
| family. The column must not already have a type in the column family |
| metadata. See the warnings on HELP ALTER_ALTER regarding the lack of |
| validation of existing data; they apply here as well. |
| """ |
| |
| def help_alter_drop(self): |
| print """ |
| ALTER COLUMNFAMILY: dropping a typed column |
| |
| ALTER COLUMNFAMILY addamsFamily DROP gender; |
| |
| An ALTER COLUMNFAMILY ... DROP statement removes the type of a column |
| from the column family metadata. Note that this does _not_ remove the |
| column from current rows; it just removes the metadata saying that the |
| bytes stored under that column are expected to be deserializable |
| according to a certain type. |
| """ |
| |
| def help_alter_with(self): |
| print """ |
| ALTER COLUMNFAMILY: changing column family properties |
| |
| ALTER COLUMNFAMILY addamsFamily WITH comment = 'Glad to be here!' |
| AND read_repair_chance = 0.2; |
| |
| An ALTER COLUMNFAMILY ... WITH statement makes adjustments to the |
| column family properties, as defined when the column family was created |
| (see HELP CREATE_COLUMNFAMILY_OPTIONS, and your Cassandra documentation |
| for information about the supported parameter names and values). |
| """ |
| |
| def applycolor(self, text, color=None): |
| if not color or not self.color: |
| return text |
| return color + text + ANSI_RESET |
| |
| def writeresult(self, text, color=None, newline=True, out=None): |
| if out is None: |
| out = self.query_out |
| out.write(self.applycolor(str(text), color) + ('\n' if newline else '')) |
| |
| def printerr(self, text, color=RED, newline=True, shownum=None): |
| if shownum is None: |
| shownum = self.show_line_nums |
| if shownum: |
| text = '%s:%d:%s' % (self.stdin.name, self.lineno, text) |
| self.writeresult(text, color, newline=newline, out=sys.stderr) |
| |
| def add_assumption(self, ksname, cfname, colname, valtype, valclass): |
| try: |
| v_info = self.schema_overrides[(ksname, cfname)] |
| except KeyError: |
| v_info = self.schema_overrides[(ksname, cfname)] = FakeCqlMetadata() |
| if valtype == 'names': |
| v_info.default_name_type = valclass |
| elif valtype == 'values': |
| v_info.default_value_type = valclass |
| elif valtype == 'colvalues': |
| v_info.value_types[colname] = valclass |
| |
| |
| class FakeCqlMetadata: |
| def __init__(self): |
| self.name_types = {} |
| self.value_types = {} |
| self.default_name_type = None |
| self.default_value_type = None |
| |
| def join(self, realschema): |
| f = self.__class__() |
| f.default_name_type = self.default_name_type or realschema.default_name_type |
| f.default_value_types = self.default_value_type or realschema.default_value_type |
| f.name_types = realschema.name_types.copy() |
| f.name_types.update(self.name_types) |
| f.value_types = realschema.value_types.copy() |
| f.value_types.update(self.value_types) |
| return f |
| |
| |
| def option_with_default(cparser_getter, section, option, default=None): |
| try: |
| return cparser_getter(section, option) |
| except ConfigParser.Error: |
| return default |
| |
| def should_use_color(): |
| if not sys.stdout.isatty(): |
| return False |
| if os.environ.get('TERM', 'dumb') == 'dumb': |
| return False |
| try: |
| import subprocess |
| p = subprocess.Popen(['tput', 'colors'], stdout=subprocess.PIPE) |
| stdout, _ = p.communicate() |
| if int(stdout.strip()) < 8: |
| return False |
| except (OSError, ImportError): |
| # oh well, we tried. at least we know there's a $TERM and it's |
| # not "dumb". |
| pass |
| return True |
| |
| def read_options(cmdlineargs, environment): |
| configs = ConfigParser.SafeConfigParser() |
| configs.read(CONFIG_FILE) |
| |
| optvalues = optparse.Values() |
| optvalues.username = option_with_default(configs.get, 'authentication', 'username') |
| optvalues.password = option_with_default(configs.get, 'authentication', 'password') |
| optvalues.completekey = option_with_default(configs.get, 'ui', 'completekey', 'tab') |
| optvalues.color = option_with_default(configs.getboolean, 'ui', 'color') |
| if optvalues.color is None: |
| # default yes if tty |
| optvalues.color = should_use_color() |
| optvalues.debug = False |
| optvalues.file = None |
| optvalues.tty = sys.stdin.isatty() |
| |
| (options, arguments) = parser.parse_args(cmdlineargs, values=optvalues) |
| |
| hostname = option_with_default(configs.get, 'connection', 'hostname', DEFAULT_HOST) |
| port = option_with_default(configs.get, 'connection', 'port', DEFAULT_PORT) |
| |
| hostname = environment.get('CQLSH_HOST', hostname) |
| port = environment.get('CQLSH_PORT', port) |
| |
| if len(arguments) > 0: |
| hostname = arguments[0] |
| if len(arguments) > 1: |
| port = arguments[1] |
| |
| if options.file is not None: |
| options.color = False |
| options.tty = False |
| |
| try: |
| port = int(port) |
| except ValueError: |
| parser.error('%r is not a valid port number.' % port) |
| |
| return options, hostname, port |
| |
| def main(options, hostname, port): |
| if os.path.exists(HISTORY) and readline is not None: |
| readline.read_history_file(HISTORY) |
| delims = readline.get_completer_delims() |
| delims.replace("'", "") |
| delims += '.' |
| readline.set_completer_delims(delims) |
| |
| if options.file is None: |
| stdin = None |
| else: |
| try: |
| stdin = open(options.file, 'r') |
| except IOError, e: |
| sys.exit("Can't open %r: %s" % (options.file, e)) |
| |
| try: |
| shell = Shell(hostname, |
| port, |
| color=options.color, |
| username=options.username, |
| password=options.password, |
| stdin=stdin, |
| tty=options.tty, |
| completekey=options.completekey) |
| except KeyboardInterrupt: |
| sys.exit('Connection aborted.') |
| except CQL_ERRORS, e: |
| sys.exit('Connection error: %s' % (e,)) |
| if options.debug: |
| shell.debug = True |
| |
| shell.cmdloop() |
| |
| if readline is not None: |
| readline.write_history_file(HISTORY) |
| |
| if __name__ == '__main__': |
| main(*read_options(sys.argv[1:], os.environ)) |
| |
| # vim: set ft=python et ts=4 sw=4 : |