blob: 967f2f12db1db1cb539070ee258fa2d7cb3e71d3 [file] [log] [blame]
#!/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(not (0x020500b0 < sys.hexversion < 0x03000000))' 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.2.0"
from StringIO import StringIO
from itertools import groupby
from contextlib import contextmanager, closing
from glob import glob
from functools import partial
from collections import defaultdict
import cmd
import sys
import os
import time
import optparse
import ConfigParser
import codecs
import locale
import re
import platform
import warnings
import csv
# 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(os.path.realpath(__file__)), '..', 'pylib')
if os.path.isdir(cqlshlibdir):
sys.path.insert(0, cqlshlibdir)
from cqlshlib import cqlhandling, cql3handling, pylexotron, wcwidth
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')
if os.environ.get('CQLSH_NO_BUNDLED', ''):
ZIPLIB_DIRS = ()
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
DEFAULT_CQLVER = '2'
DEFAULT_TIME_FORMAT = '%Y-%m-%d %H:%M:%S%z'
DEFAULT_FLOAT_PRECISION = 5
if readline is not None and 'libedit' in readline.__doc__:
DEFAULT_COMPLETEKEY = '\t'
else:
DEFAULT_COMPLETEKEY = 'tab'
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', dest='color',
help='Always use color output')
parser.add_option("--no-color", action='store_false', dest='color',
help='Never use color output')
parser.add_option("-u", "--username", help="Authenticate as user.")
parser.add_option("-p", "--password", help="Authenticate using password.")
parser.add_option('-k', '--keyspace', help='Authenticate to the given keyspace.')
parser.add_option("-f", "--file",
help="Execute commands from FILE, then exit")
parser.add_option('--debug', action='store_true',
help='Show additional debugging information')
parser.add_option('--cqlversion', default=DEFAULT_CQLVER,
help='Specify a particular CQL version (default: %default).'
' Examples: "2", "3.0.0-beta1"')
parser.add_option("-2", "--cql2", action="store_const", dest='cqlversion', const='2',
help="Shortcut notation for --cqlversion=2")
parser.add_option("-3", "--cql3", action="store_const", dest='cqlversion', const='3',
help="Shortcut notation for --cqlversion=3")
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'
DARK_MAGENTA = '\033[0;35m'
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
my_commands_ending_with_newline = (
'help',
'?',
'describe',
'desc',
'show',
'assume',
'source',
'capture',
'debug',
'exit',
'quit'
)
cqlsh_syntax_completers = []
def cqlsh_syntax_completer(rulename, termname):
def registrator(f):
cqlsh_syntax_completers.append((rulename, termname, f))
return f
return registrator
cqlsh_extra_syntax_rules = r'''
<cqlshCommand> ::= <CQL_Statement>
| <specialCommand> ( ";" | "\n" )
;
<specialCommand> ::= <describeCommand>
| <showCommand>
| <assumeCommand>
| <sourceCommand>
| <captureCommand>
| <copyCommand>
| <debugCommand>
| <helpCommand>
| <exitCommand>
;
<describeCommand> ::= ( "DESCRIBE" | "DESC" )
( "KEYSPACE" ksname=<keyspaceName>?
| ( "COLUMNFAMILY" | "TABLE" ) cf=<columnFamilyName>
| ( "COLUMNFAMILIES" | "TABLES" )
| "SCHEMA"
| "CLUSTER" )
;
<showCommand> ::= "SHOW" what=( "VERSION" | "HOST" | "ASSUMPTIONS" )
;
<assumeCommand> ::= "ASSUME" cf=<columnFamilyName> <assumeTypeDef>
( "," <assumeTypeDef> )*
;
<assumeTypeDef> ::= "NAMES" "ARE" names=<storageType>
| "VALUES" "ARE" values=<storageType>
| "(" colname=<colname> ")" "VALUES" "ARE" colvalues=<storageType>
;
<sourceCommand> ::= "SOURCE" fname=<stringLiteral>
;
<captureCommand> ::= "CAPTURE" ( fname=( <stringLiteral> | "OFF" ) )?
;
<copyCommand> ::= "COPY" cf=<columnFamilyName>
( "(" [colnames]=<colname> ( "," [colnames]=<colname> )* ")" )?
( dir="FROM" ( fname=<stringLiteral> | "STDIN" )
| dir="TO" ( fname=<stringLiteral> | "STDOUT" ) )
( "WITH" <copyOption> ( "AND" <copyOption> )* )?
;
<copyOption> ::= [optnames]=<cfOptionName> "=" [optvals]=<cfOptionVal>
;
# avoiding just "DEBUG" so that this rule doesn't get treated as a terminal
<debugCommand> ::= "DEBUG" "THINGS"?
;
<helpCommand> ::= ( "HELP" | "?" ) [topic]=( /[a-z_]*/ )*
;
<exitCommand> ::= "exit" | "quit"
;
<qmark> ::= "?" ;
'''
@cqlsh_syntax_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)
@cqlsh_syntax_completer('assumeTypeDef', 'colname')
def complete_assume_col(ctxt, cqlsh):
ks = ctxt.get_binding('ks', None)
ks = cqlsh.cql_unprotect_name(ks) if ks is not None else None
cf = cqlsh.cql_unprotect_name(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(cqlsh.cql_protect_name, 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
cqlsh_syntax_completer('sourceCommand', 'fname') \
(complete_source_quoted_filename)
cqlsh_syntax_completer('captureCommand', 'fname') \
(complete_source_quoted_filename)
@cqlsh_syntax_completer('copyCommand', 'fname')
def copy_fname_completer(ctxt, cqlsh):
lasttype = ctxt.get_binding('*LASTTYPE*')
if lasttype == 'unclosedString':
return complete_source_quoted_filename(ctxt, cqlsh)
partial = ctxt.get_binding('partial')
if partial == '':
return ["'"]
return ()
@cqlsh_syntax_completer('copyCommand', 'colnames')
def complete_copy_column_names(ctxt, cqlsh):
existcols = map(cqlsh.cql_unprotect_name, ctxt.get_binding('colnames', ()))
ks = cqlsh.cql_unprotect_name(ctxt.get_binding('ksname', None))
cf = cqlsh.cql_unprotect_name(ctxt.get_binding('cfname'))
colnames = cqlsh.get_column_names(ks, cf)
if len(existcols) == 0:
return [colnames[0]]
return set(colnames[1:]) - set(existcols)
COPY_OPTIONS = ('DELIMITER', 'QUOTE', 'ESCAPE', 'HEADER', 'ENCODING', 'NULL')
@cqlsh_syntax_completer('copyOption', 'optnames')
def complete_copy_options(ctxt, cqlsh):
optnames = map(str.upper, ctxt.get_binding('optnames', ()))
direction = ctxt.get_binding('dir').upper()
opts = set(COPY_OPTIONS) - set(optnames)
if direction == 'FROM':
opts -= ('ENCODING', 'NULL')
return opts
@cqlsh_syntax_completer('copyOption', 'optvals')
def complete_copy_opt_values(ctxt, cqlsh):
optnames = ctxt.get_binding('optnames', ())
lastopt = optnames[-1].lower()
if lastopt == 'header':
return ['true', 'false']
return [cqlhandling.Hint('<single_character_string>')]
class NoKeyspaceError(Exception):
pass
class KeyspaceNotFound(Exception):
pass
class ColumnFamilyNotFound(Exception):
pass
class VersionNotSupported(Exception):
pass
class DecodeError(Exception):
def __init__(self, thebytes, err, expectedtype, colname=None):
self.thebytes = thebytes
self.err = err
self.expectedtype = expectedtype
self.colname = colname
def __str__(self):
return str(self.thebytes)
def message(self):
what = 'column name %r' % (self.thebytes,)
if self.colname is not None:
what = 'value %r (for column %r)' % (self.thebytes, self.colname)
return 'Failed to decode %s as %s: %s' % (what, self.expectedtype, self.err)
def __repr__(self):
return '<%s %s>' % (self.__class__.__name__, self.message())
def full_cql_version(ver):
while ver.count('.') < 2:
ver += '.0'
ver_parts = ver.split('-', 1) + ['']
vertuple = tuple(map(int, ver_parts[0].split('.')) + [ver_parts[1]])
return ver, vertuple
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 __len__(self):
return len(self.strval)
def _pad(self, width, fill=' '):
if width > self.displaywidth:
return fill * (width - self.displaywidth)
else:
return ''
def ljust(self, width, fill=' '):
"""
Similar to self.strval.ljust(width), but takes expected terminal
display width into account for special characters, and does not
take color escape codes into account.
"""
return self.strval + self._pad(width, fill)
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
def color_ljust(self, width, fill=' '):
"""
Similar to self.ljust(width), but uses this value's colored
representation, and does not take color escape codes into account
in determining width.
"""
return self.coloredval + self._pad(width, fill)
unicode_controlchars_re = re.compile(r'[\x00-\x31\x7f-\xa0]')
controlchars_re = re.compile(r'[\x00-\x31\x7f-\xff]')
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 _make_turn_bits_red_f(color1, color2):
def _turn_bits_red(match):
txt = match.group(0)
if txt == '\\\\':
return '\\'
return color1 + txt + color2
return _turn_bits_red
DEFAULT_VALUE_COLORS = dict(
default=YELLOW,
text=YELLOW,
error=RED,
hex=DARK_MAGENTA,
timestamp=GREEN,
int=GREEN,
float=GREEN,
decimal=GREEN,
boolean=GREEN,
uuid=GREEN,
)
COLUMN_NAME_COLORS = defaultdict(lambda: MAGENTA,
error=RED,
hex=DARK_MAGENTA,
)
def unix_time_from_uuid1(u):
return (u.get_time() - 0x01B21DD213814000) / 10000000.0
def format_value(val, casstype, output_encoding, addcolor=False, time_format='',
float_precision=3, colormap=DEFAULT_VALUE_COLORS, nullval='null'):
color = colormap['default']
coloredval = None
displaywidth = None
if val is None:
bval = nullval
color = colormap['error']
elif isinstance(val, DecodeError):
casstype = 'BytesType'
bval = repr(val.thebytes)
color = colormap['hex']
elif casstype == 'UTF8Type':
escapedval = val.replace(u'\\', u'\\\\')
escapedval = unicode_controlchars_re.sub(_show_control_chars, escapedval)
bval = escapedval.encode(output_encoding, 'backslashreplace')
displaywidth = wcwidth.wcswidth(bval.decode(output_encoding))
if addcolor:
tbr = _make_turn_bits_red_f(colormap['hex'], colormap['text'])
coloredval = colormap['text'] + bits_to_turn_red_re.sub(tbr, bval) + ANSI_RESET
elif casstype in ('DateType', 'TimeUUIDType'):
if casstype == 'TimeUUIDType':
val = unix_time_from_uuid1(val)
local = time.localtime(val)
formatted = time.strftime(time_format, local)
if local.tm_isdst != 0:
offset = -time.altzone
else:
offset = -time.timezone
if formatted[-4:] != '0000' or time_format[-2:] != '%z' or offset == 0:
bval = formatted
else:
# deal with %z on platforms where it isn't supported. see CASSANDRA-4746.
if offset < 0:
sign = '-'
else:
sign = '+'
hours, minutes = divmod(abs(offset) / 60, 60)
bval = formatted[:-5] + sign + '{0:0=2}{1:0=2}'.format(hours, minutes)
color = colormap['timestamp']
elif casstype in ('LongType', 'Int32Type', 'IntegerType', 'CounterColumnType'):
# base-10 only for now; support others?
bval = str(val)
color = colormap['int']
elif casstype in ('FloatType', 'DoubleType'):
bval = '%.*g' % (float_precision, val)
color = colormap['float']
elif casstype in ('DecimalType', 'UUIDType', 'BooleanType'):
# let python do these for us
bval = str(val)
color = colormap[cqlruleset.cql_typename(casstype)]
elif casstype == 'BytesType':
bval = ''.join('%02x' % ord(c) for c in val)
color = colormap['hex']
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:
tbr = _make_turn_bits_red_f(colormap['hex'], colormap['text'])
coloredval = colormap['text'] + bits_to_turn_red_re.sub(tbr, 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)
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)
def describe_interval(seconds):
desc = []
for length, unit in ((86400, 'day'), (3600, 'hour'), (60, 'minute')):
num = int(seconds) / length
if num > 0:
desc.append('%d %s' % (num, unit))
if num > 1:
desc[-1] += 's'
seconds %= length
words = '%.03f seconds' % seconds
if len(desc) > 1:
words = ', '.join(desc) + ', and ' + words
elif len(desc) == 1:
words = desc[0] + ' and ' + words
return words
class Shell(cmd.Cmd):
default_prompt = "cqlsh> "
continue_prompt = " ... "
keyspace_prompt = "cqlsh:%s> "
keyspace_continue_prompt = "%s ... "
num_retries = 4
show_line_nums = False
debug = False
stop = False
last_hist = None
shunted_query_out = None
csv_dialect_defaults = dict(delimiter=',', doublequote=False,
escapechar='\\', quotechar='"')
def __init__(self, hostname, port, color=False, username=None,
password=None, encoding=None, stdin=None, tty=True,
completekey=DEFAULT_COMPLETEKEY, use_conn=None,
cqlver=None, keyspace=None,
display_time_format=DEFAULT_TIME_FORMAT,
display_float_precision=DEFAULT_FLOAT_PRECISION):
cmd.Cmd.__init__(self, completekey=completekey)
self.hostname = hostname
self.port = port
self.username = username
self.password = password
self.keyspace = keyspace
if use_conn is not None:
self.conn = use_conn
else:
self.conn = cql.connect(hostname, port, user=username, password=password)
self.set_expanded_cql_version(cqlver)
# we could set the keyspace through cql.connect(), but as of 1.0.10,
# it doesn't quote the keyspace for USE :(
if keyspace is not None:
tempcurs = self.conn.cursor()
tempcurs.execute('USE %s;' % self.cql_protect_name(keyspace))
tempcurs.close()
self.cursor = self.conn.cursor()
self.current_keyspace = keyspace
self.color = color
self.display_time_format = display_time_format
self.display_float_precision = display_float_precision
if encoding is None:
encoding = locale.getpreferredencoding()
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.reset_prompt()
self.report_connection()
print 'Use HELP for help.'
else:
self.show_line_nums = True
self.stdin = stdin
self.query_out = sys.stdout
def set_expanded_cql_version(self, ver):
ver, vertuple = full_cql_version(ver)
self.set_cql_version(ver)
self.cql_version = ver
self.cql_ver_tuple = vertuple
def cqlver_atleast(self, major, minor=0, patch=0):
return self.cql_ver_tuple[:3] >= (major, minor, patch)
def myformat_value(self, val, casstype, **kwargs):
if isinstance(val, DecodeError):
self.decoding_errors.append(val)
return format_value(val, casstype, self.output_codec.name,
addcolor=self.color, time_format=self.display_time_format,
float_precision=self.display_float_precision, **kwargs)
def myformat_colname(self, name, nametype):
return self.myformat_value(name, nametype, colormap=COLUMN_NAME_COLORS)
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
# system.Versions['cql'] apparently does not reflect changes with
# set_cql_version.
vers['cql'] = self.cql_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 = self.cql_protect_name(keyspace)
print 'USE %s;' % keyspace
print
for (ks, cf), override in ksoverrides:
cf = self.cql_protect_name(cf)
if override.default_name_type:
print 'ASSUME %s NAMES ARE %s;' \
% (cf, cqlruleset.cql_typename(override.default_name_type))
if override.default_value_type:
print 'ASSUME %s VALUES ARE %s;' \
% (cf, cqlruleset.cql_typename(override.default_value_type))
for colname, vtype in override.value_types.items():
colname = self.cql_protect_name(colname)
print 'ASSUME %s(%s) VALUES ARE %s;' \
% (cf, colname, cqlruleset.cql_typename(vtype))
print
def get_cluster_versions(self):
if self.cqlver_atleast(3):
query = 'select component, version from system."Versions"'
else:
query = 'select component, version from system.Versions'
try:
self.cursor.execute(query)
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 fetchdict(self):
row = self.cursor.fetchone()
if row is None:
return None
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()]
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 ColumnFamilyNotFound("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, cfdef):
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
def get_column_names(self, ksname, cfname):
if ksname is None:
ksname = self.current_keyspace
if ksname != 'system' and self.cqlver_atleast(3):
return self.get_column_names_from_layout(ksname, cfname)
else:
return self.get_column_names_from_cfdef(ksname, cfname)
def get_column_names_from_layout(self, ksname, cfname):
layout = self.get_columnfamily_layout(ksname, cfname)
return [col.name for col in layout.columns]
def get_column_names_from_cfdef(self, ksname, cfname):
cfdef = self.get_columnfamily(cfname, ksname=ksname)
key_alias = cfdef.key_alias
if key_alias is None:
key_alias = 'KEY'
return [key_alias] + sorted([cm.name for cm in cfdef.column_metadata])
# ===== 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):
try:
return self.make_hacktastic_thrift_call('describe_keyspace', ksname)
except cql.cassandra.ttypes.NotFoundException, e:
raise KeyspaceNotFound('Keyspace %s not found.' % e)
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 set_cql_version(self, ver):
try:
return self.make_hacktastic_thrift_call('set_cql_version', ver)
except cql.cassandra.ttypes.InvalidRequestException, e:
raise VersionNotSupported(e.why)
def make_hacktastic_thrift_call(self, call, *args):
client = self.conn.client
return getattr(client, call)(*args)
# ===== end thrift-dependent parts =====
# ===== cql3-dependent parts =====
def get_columnfamily_layout(self, ksname, cfname):
if ksname is None:
ksname = self.current_keyspace
self.cursor.execute("""select * from system.schema_columnfamilies
where "keyspace"=:ks and "columnfamily"=:cf""",
{'ks': ksname, 'cf': cfname})
layout = self.fetchdict()
if layout is None:
raise ColumnFamilyNotFound("Column family %r not found" % cfname)
self.cursor.execute("""select * from system.schema_columns
where "keyspace"=:ks and "columnfamily"=:cf""",
{'ks': ksname, 'cf': cfname})
cols = self.fetchdict_all()
return cql3handling.CqlTableDef.from_layout(layout, cols)
# ===== end cql3-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)
if 'libedit' in readline.__doc__:
readline.parse_and_bind("bind -e")
readline.parse_and_bind("bind '" + self.completekey + "' rl_complete")
else:
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(prompt) + '\n'
else:
line = self.stdin.readline()
if not len(line):
raise EOFError
self.lineno += 1
return line
def use_stdin_reader(self, until='', prompt=''):
until += '\n'
while True:
try:
newline = self.get_input_line(prompt=prompt)
except EOFError:
return
if newline == until:
return
yield newline
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 = cqlruleset.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):
# Concat multi-line statements and insert into history
if readline is not None:
nl_count = srcstr.count("\n")
new_hist = srcstr.replace("\n", " ").rstrip()
if nl_count > 1 and self.last_hist != new_hist:
readline.add_history(new_hist)
self.last_hist = new_hist
cmdword = tokens[0][1]
if cmdword == '?':
cmdword = 'help'
custom_handler = getattr(self, 'do_' + cmdword.lower(), None)
if custom_handler:
parsed = cqlruleset.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(cqlruleset.cql_extract_orig(tokens, srcstr))
def handle_parse_error(self, cmdword, tokens, parsed, srcstr):
if cmdword.lower() == '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(cqlruleset.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 tables
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 quoted using single quotes (CQL 2) or double quotes
(CQL 3).
"""
ksname = parsed.get_binding('ksname')
if self.perform_statement(parsed.extract_orig()):
self.current_keyspace = self.cql_unprotect_name(ksname)
def do_select(self, parsed):
"""
SELECT [FIRST n] [REVERSED] <selectExpr>
FROM [<keyspace>.]<table>
[USING CONSISTENCY <consistencylevel>]
[WHERE <clause>]
[ORDER BY <colname> [DESC]]
[LIMIT m];
SELECT is used to read one or more records from a CQL table. It returns
a set of rows matching the selection criteria specified.
Note that FIRST and REVERSED are only supported in CQL 2, and ORDER BY
is only supported in CQL 3 and higher.
For more information, see one of the following:
HELP SELECT_EXPR
HELP SELECT_TABLE
HELP SELECT_WHERE
HELP SELECT_LIMIT
HELP CONSISTENCYLEVEL
"""
ksname = parsed.get_binding('ksname')
if ksname is not None:
ksname = self.cql_unprotect_name(ksname)
cfname = self.cql_unprotect_name(parsed.get_binding('cfname'))
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.ProgrammingError, err:
self.printerr(str(err))
# try reparsing as cql3; if successful, suggest -3
if self.cqlver_atleast(3):
if self.parseable_as_cql2(statement):
self.printerr("Perhaps you meant to use CQL 2? Try using"
" the -2 option when starting cqlsh.")
else:
if self.parseable_as_cql3(statement):
self.printerr("Perhaps you meant to use CQL 3? Try using"
" the -3 option when starting cqlsh.")
return False
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(self.cursor)
elif self.cursor.description is not _VOID_DESCRIPTION:
self.print_result(self.cursor)
self.flush_output()
return True
# these next two functions are not guaranteed perfect; just checks if the
# statement parses fully according to cqlsh's own understanding of the
# grammar. Changes to the language in Cassandra frequently don't get
# updated in cqlsh right away.
def parseable_as_cql3(self, statement):
return cql3handling.CqlRuleSet.lex_and_whole_match(statement) is not None
def parseable_as_cql2(self, statement):
return cqlhandling.CqlRuleSet.lex_and_whole_match(statement) is not None
def determine_decoder_for(self, cfname, ksname=None):
decoder = ErrorHandlingSchemaDecoder
if ksname is None:
ksname = self.current_keyspace
overrides = self.schema_overrides.get((ksname, cfname), None)
if overrides:
decoder = partial(decoder, overrides=overrides)
return decoder
def get_nametype(self, cursor, num):
"""
Determine the Cassandra type of a column name from the current row of
query results on the given cursor. The column in question is given by
its zero-based ordinal number within the row.
This is necessary to differentiate some things like ascii vs. blob hex.
"""
if getattr(cursor, 'supports_name_info', False):
return cursor.name_info[num][1]
# This is a pretty big hack, but necessary until we can rely on
# python-cql 1.0.10 being around.
row = cursor.result[cursor.rs_idx - 1]
col = row.columns[num]
schema = cursor.decoder.schema
return schema.name_types.get(col.name, schema.default_name_type)
def print_count_result(self, cursor):
if not cursor.result:
return
self.writeresult('count')
self.writeresult('-----')
self.writeresult(cursor.result[0])
self.writeresult("")
def print_result(self, cursor):
self.decoding_errors = []
# first pass: see if we have a static column set
last_description = None
for row in cursor:
if last_description is not None and cursor.description != last_description:
static = False
break
last_description = cursor.description
else:
static = True
cursor._reset()
if static:
self.print_static_result(cursor)
else:
self.print_dynamic_result(cursor)
self.writeresult("")
if self.decoding_errors:
for err in self.decoding_errors[:2]:
self.writeresult(err.message(), color=RED)
if len(self.decoding_errors) > 2:
self.writeresult('%d more decoding errors suppressed.'
% (len(self.decoding_errors) - 2), color=RED)
def print_static_result(self, cursor):
colnames, coltypes = zip(*cursor.description)[:2]
colnames_t = [(name, self.get_nametype(cursor, n)) for (n, name) in enumerate(colnames)]
formatted_names = [self.myformat_colname(name, nametype) for (name, nametype) in colnames_t]
formatted_data = [map(self.myformat_value, row, coltypes) for row in cursor]
# determine column widths
widths = [n.displaywidth for n in formatted_names]
for fmtrow in formatted_data:
for num, col in enumerate(fmtrow):
widths[num] = max(widths[num], col.displaywidth)
# print header
header = ' | '.join(hdr.color_ljust(w) for (hdr, w) in zip(formatted_names, 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, cursor):
for row in cursor:
colnames, coltypes = zip(*cursor.description)[:2]
colnames_t = [(name, self.get_nametype(cursor, n)) for (n, name) in enumerate(colnames)]
colnames = [self.myformat_colname(name, nametype) for (name, nametype) in colnames_t]
colvals = [self.myformat_value(val, casstype) for (val, casstype) in zip(row, coltypes)]
line = ' | '.join('%s,%s' % (n.coloredval, v.coloredval) for (n, v) in zip(colnames, colvals))
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 cqlruleset.cql_complete(stuff_to_complete, text, cassandra_conn=self,
debug=debug_completion, startsymbol='cqlshCommand')
def set_prompt(self, prompt):
self.prompt = prompt
def cql_protect_name(self, name):
if isinstance(name, unicode):
name = name.encode('utf8')
return cqlruleset.maybe_escape_name(name)
def cql_protect_value(self, value):
return cqlruleset.escape_value(value)
def cql_unprotect_name(self, namestr):
if namestr is None:
return
return cqlruleset.dequote_name(namestr)
def cql_unprotect_value(self, valstr):
if valstr is not None:
return cqlruleset.dequote_value(valstr)
def print_recreate_keyspace(self, ksdef, out):
stratclass = trim_if_present(ksdef.strategy_class, 'org.apache.cassandra.locator.')
ksname = self.cql_protect_name(ksdef.name)
out.write("CREATE KEYSPACE %s WITH strategy_class = %s"
% (ksname, self.cql_protect_value(stratclass)))
for opname, opval in ksdef.strategy_options.iteritems():
out.write("\n AND strategy_options:%s = %s" % (opname, self.cql_protect_value(opval)))
out.write(';\n')
if ksdef.cf_defs:
out.write('\nUSE %s;\n' % ksname)
for cf in ksdef.cf_defs:
out.write('\n')
# yes, cf might be looked up again. oh well.
self.print_recreate_columnfamily(ksdef.name, 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.
"""
# no metainfo available from system.schema_* for system CFs, so we have
# to use cfdef-based description for those.
if ksname != '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:
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 = self.cql_protect_name(cfdef.name)
out.write("CREATE TABLE %s (\n" % cfname)
alias = self.cql_protect_name(cfdef.key_alias) if cfdef.key_alias else 'KEY'
keytype = cqlruleset.cql_typename(cfdef.key_validation_class)
out.write(" %s %s PRIMARY KEY" % (alias, keytype))
indexed_columns = []
for col in cfdef.column_metadata:
colname = self.cql_protect_name(col.name)
out.write(",\n %s %s" % (colname, cqlruleset.cql_typename(col.validation_class)))
if col.index_name is not None:
indexed_columns.append(col)
cf_opts = []
for (option, thriftname) in cqlruleset.columnfamily_options:
optval = getattr(cfdef, thriftname or option, None)
if optval is None:
continue
if option in ('comparator', 'default_validation'):
optval = cqlruleset.cql_typename(optval)
else:
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.')
optval = self.cql_protect_value(optval)
cf_opts.append((option, optval))
for option, thriftname, _ in cqlruleset.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.')
cf_opts.append(('%s:%s' % (option, k), self.cql_protect_value(v)))
out.write('\n)')
if cf_opts:
joiner = 'WITH'
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, self.cql_protect_name(col.name)))
def print_recreate_columnfamily_from_layout(self, layout, out):
cfname = self.cql_protect_name(layout.name)
out.write("CREATE TABLE %s (\n" % cfname)
keycol = layout.columns[0]
out.write(" %s %s" % (self.cql_protect_name(keycol.name), keycol.cqltype))
if len(layout.key_components) == 1:
out.write(" PRIMARY KEY")
indexed_columns = []
for col in layout.columns[1:]:
colname = self.cql_protect_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(self.cql_protect_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 cqlruleset.columnfamily_layout_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, self.cql_protect_value(optval)))
for option, _ in cqlruleset.columnfamily_layout_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')), self.cql_protect_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, self.cql_protect_name(col.name)))
def describe_keyspace(self, ksname):
print
self.print_recreate_keyspace(self.get_keyspace(ksname), sys.stdout)
print
def describe_columnfamily(self, ksname, cfname):
if ksname is None:
ksname = self.current_keyspace
print
self.print_recreate_columnfamily(ksname, 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:
names = self.get_columnfamily_names(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 tables 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 TABLES
Output the names of all tables in the current keyspace, or in all
keyspaces if there is no current keyspace.
DESCRIBE TABLE <tablename>
Output CQL commands that could be used to recreate the given table.
In some cases, as above, there may be table 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 = self.cql_unprotect_name(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 in ('columnfamily', 'table'):
ks = self.cql_unprotect_name(parsed.get_binding('ksname', None))
cf = self.cql_unprotect_name(parsed.get_binding('cfname'))
self.describe_columnfamily(ks, cf)
elif what in ('columnfamilies', 'tables'):
self.describe_columnfamilies(self.current_keyspace)
elif what == 'cluster':
self.describe_cluster()
elif what == 'schema':
self.describe_schema()
do_desc = do_describe
def do_copy(self, parsed):
r"""
COPY [cqlsh only]
COPY x FROM: Imports CSV data into a Cassandra table
COPY x TO: Exports data from a Cassandra table in CSV format.
COPY <table_name> [ ( column [, ...] ) ]
FROM ( '<filename>' | STDIN )
[ WITH <option>='value' [AND ...] ];
COPY <table_name> [ ( column [, ...] ) ]
TO ( '<filename>' | STDOUT )
[ WITH <option>='value' [AND ...] ];
Available options and defaults:
DELIMITER=',' - character that appears between records
QUOTE='"' - quoting character to be used to quote fields
ESCAPE='\' - character to appear before the QUOTE char when quoted
HEADER=false - whether to ignore the first line
ENCODING='utf8' - encoding for CSV output (COPY TO only)
NULL='' - string that represents a null value (COPY TO only)
When entering CSV data on STDIN, you can use the sequence "\."
on a line by itself to end the data input.
"""
ks = self.cql_unprotect_name(parsed.get_binding('ksname', None))
if ks is None:
ks = self.current_keyspace
if ks is None:
raise NoKeyspaceError("Not in any keyspace.")
cf = self.cql_unprotect_name(parsed.get_binding('cfname'))
columns = parsed.get_binding('colnames', None)
if columns is not None:
columns = map(self.cql_unprotect_name, columns)
fname = parsed.get_binding('fname', None)
if fname is not None:
fname = os.path.expanduser(self.cql_unprotect_value(fname))
copyoptnames = map(str.lower, parsed.get_binding('optnames', ()))
copyoptvals = map(self.cql_unprotect_value, parsed.get_binding('optvals', ()))
opts = dict(zip(copyoptnames, copyoptvals))
timestart = time.time()
direction = parsed.get_binding('dir').upper()
if direction == 'FROM':
rows = self.perform_csv_import(ks, cf, columns, fname, opts)
verb = 'imported'
elif direction == 'TO':
rows = self.perform_csv_export(ks, cf, columns, fname, opts)
verb = 'exported'
else:
raise SyntaxError("Unknown direction %s" % direction)
timeend = time.time()
print "%d rows %s in %s." % (rows, verb, describe_interval(timeend - timestart))
def perform_csv_import(self, ks, cf, columns, fname, opts):
dialect_options = self.csv_dialect_defaults.copy()
if 'quote' in opts:
dialect_options['quotechar'] = opts.pop('quote')
if 'escape' in opts:
dialect_options['escapechar'] = opts.pop('escape')
if 'delimiter' in opts:
dialect_options['delimiter'] = opts.pop('delimiter')
header = bool(opts.pop('header', '').lower() == 'true')
if dialect_options['quotechar'] == dialect_options['escapechar']:
dialect_options['doublequote'] = True
del dialect_options['escapechar']
if opts:
self.printerr('Unrecognized COPY FROM options: %s'
% ', '.join(opts.keys()))
return 0
if fname is None:
do_close = False
print "[Use \. on a line by itself to end input]"
linesource = self.use_stdin_reader(prompt='[copy] ', until=r'\.')
else:
do_close = True
try:
linesource = open(fname, 'rb')
except IOError, e:
self.printerr("Can't open %r for reading: %s" % (fname, e))
return 0
try:
if header:
linesource.next()
numcol, prepq = self.prep_import_insert(ks, cf, columns)
rownum = -1
reader = csv.reader(linesource, **dialect_options)
for rownum, row in enumerate(reader):
if len(row) != numcol:
self.printerr("Record #%d (line %d) has the wrong number of fields "
"(%d instead of %d)."
% (rownum, reader.line_num, len(row), numcol))
return rownum
if not self.do_import_insert(prepq, row):
self.printerr("Aborting import at record #%d (line %d). "
"Previously-inserted values still present."
% (rownum, reader.line_num))
return rownum
finally:
if do_close:
linesource.close()
elif self.tty:
print
return rownum + 1
def prep_import_insert(self, ks, cf, columns):
if columns is None:
# default to all known columns
columns = self.get_column_names(ks, cf)
# would be nice to be able to use a prepared query here, but in order
# to use that interface, we'd need to have all the input as native
# values already, reading them from text just like the various
# Cassandra cql types do. Better just to submit them all as intact
# CQL string literals and let Cassandra do its thing.
return len(columns), 'INSERT INTO %s.%s (%s) VALUES (%%s)' % (
self.cql_protect_name(ks),
self.cql_protect_name(cf),
', '.join(map(self.cql_protect_name, columns))
)
def do_import_insert(self, prepq, rowvalues):
valstring = ', '.join(map(self.cql_protect_value, rowvalues))
cql = prepq % valstring
if self.debug:
print "Import using CQL: %s" % cql
return self.perform_statement(cql)
def perform_csv_export(self, ks, cf, columns, fname, opts):
dialect_options = self.csv_dialect_defaults.copy()
if 'quote' in opts:
dialect_options['quotechar'] = opts.pop('quote')
if 'escape' in opts:
dialect_options['escapechar'] = opts.pop('escape')
if 'delimiter' in opts:
dialect_options['delimiter'] = opts.pop('delimiter')
encoding = opts.pop('encoding', 'utf8')
nullval = opts.pop('null', '')
header = bool(opts.pop('header', '').lower() == 'true')
if dialect_options['quotechar'] == dialect_options['escapechar']:
dialect_options['doublequote'] = True
del dialect_options['escapechar']
if opts:
self.printerr('Unrecognized COPY TO options: %s'
% ', '.join(opts.keys()))
return 0
if fname is None:
do_close = False
csvdest = sys.stdout
else:
do_close = True
try:
csvdest = open(fname, 'wb')
except IOError, e:
self.printerr("Can't open %r for writing: %s" % (fname, e))
return 0
try:
self.prep_export_dump(ks, cf, columns)
writer = csv.writer(csvdest, **dialect_options)
if header:
writer.writerow([d[0] for d in self.cursor.description])
rows = 0
while True:
row = self.cursor.fetchone()
if row is None:
break
fmt = lambda v, d: \
format_value(v, d[1], output_encoding=encoding, nullval=nullval,
time_format=self.display_time_format,
float_precision=self.display_float_precision).strval
writer.writerow(map(fmt, row, self.cursor.description))
rows += 1
finally:
if do_close:
csvdest.close()
return rows
def prep_export_dump(self, ks, cf, columns):
if columns is None:
columns = self.get_column_names(ks, cf)
columnlist = ', '.join(map(self.cql_protect_name, columns))
# this limit is pretty awful. would be better to use row-key-paging, so
# that the dump could be pretty easily aborted if necessary, but that
# can be kind of tricky with cql3. Punt for now, until the real cursor
# API is added in CASSANDRA-4415.
query = 'SELECT %s FROM %s.%s LIMIT 99999999' \
% (columnlist, self.cql_protect_name(ks), self.cql_protect_name(cf))
self.cursor.execute(query)
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 table's metadata. Data will be deserialized according to the
given type, and displayed appropriately when retrieved.
Use thus:
ASSUME [<keyspace>.]<tablename> NAMES ARE <type>;
Treat all column names in the given table as being of the
given type.
ASSUME [<keyspace>.]<tablename> VALUES ARE <type>;
Treat all column values in the given table 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
table's definition.
ASSUME [<keyspace>.]<tablename>(<colname>) VALUES ARE <type>;
Treat all values in the given column in the given table 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 table by
separating with commas:
ASSUME ks.table NAMES ARE uuid, VALUES ARE int, (col) VALUES ARE ascii
See HELP TYPES for information on the supported data storage types.
"""
ks = self.cql_unprotect_name(parsed.get_binding('ksname', None))
cf = self.cql_unprotect_name(parsed.get_binding('cfname'))
colname = self.cql_unprotect_name(parsed.get_binding('colname', None))
params = {}
for paramname in ('names', 'values', 'colvalues'):
val = parsed.get_binding(paramname, None)
params[paramname] = self.cql_unprotect_value(val)
if ks is None:
if self.current_keyspace is None:
self.printerr('Error: not in any keyspace.')
return
ks = self.current_keyspace
for overridetype in ('names', 'values', 'colvalues'):
cqltype = params[overridetype]
if cqltype is None:
continue
try:
validator_class = cqlruleset.find_validator_class(cqltype)
except KeyError:
self.printerr('Error: validator type %s not found.' % cqltype)
else:
self.add_assumption(ks, cf, 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(self.cql_unprotect_value(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, cqlver=self.cql_version,
display_time_format=self.display_time_format,
display_float_precision=self.display_float_precision)
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 appended to 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(self.cql_unprotect_value(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 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',):
names.remove(hide_from_help)
return names
def columnize(self, slist, *a, **kw):
return cmd.Cmd.columnize(self, sorted([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, self.cql_unprotect_value(t).lower())
def help_types(self):
print "\n CQL types recognized by this version of cqlsh:\n"
for t in cqlruleset.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.
** [FIRST n] and [REVERSED] are no longer supported in CQL 3.
"""
def help_select_table(self):
print """
SELECT: Specifying Table
SELECT ... FROM [<keyspace>.]<tablename> ...
The FROM clause is used to specify the CQL table applicable to a SELECT
query. The keyspace in which the table exists can optionally be
specified along with the table name, separated by a dot (.). This will
not change the current keyspace of the session (see HELP USE).
"""
help_select_columnfamily = help_select_table
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)
* TWO
* THREE
* 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 [<keyspace>.]<tablename>
( <colname1>, <colname2> [, <colname3> [, ...]] )
VALUES ( <colval1>, <colval2> [, <colval3> [, ...]] )
[USING CONSISTENCY <consistencylevel>
[AND TIMESTAMP <timestamp>]
[AND TTL <timeToLive]];
An INSERT is used to write one or more columns to a record in a
CQL table. No results are returned.
Values for all component columns in the table's primary key must
be given. Also, there must be at least one non-primary-key column
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 [<keyspace>.]<columnFamily>
[USING CONSISTENCY <consistencylevel>
[AND TIMESTAMP <timestamp>]
[AND TTL <timeToLive>]]
SET name1 = value1, name2 = value2 WHERE <keycol> = keyval;
An UPDATE is used to write one or more columns to a record in a table.
No results are returned. The record's primary key must be completely
and uniquely specified; that is, if the primary key includes multiple
columns, all must be explicitly given in the WHERE clause.
Statements begin with the UPDATE keyword followed by the name of the
table to be updated.
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. Cassandra timestamps are generally specified
using milliseconds since the Unix epoch (1970-01-01 00:00:00 UTC).
<timeToLive> defines the optional time to live (TTL) in seconds 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>, ...);
UPDATE ... WHERE <keycol1> = <keyval1> AND <keycol2> = <keyval2>;
Each update statement requires a precise set of keys to be specified
using a WHERE clause.
If the table's primary key consists of multiple columns, an explicit
value must be given for each for the UPDATE statement to make sense.
"""
def help_delete(self):
print """
DELETE [<col1> [, <col2>, ...] FROM [<keyspace>.]<tablename>
[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. Cassandra timestamps are generally
specified using milliseconds since the Unix epoch (1970-01-01 00:00:00
UTC).
"""
def help_delete_columns(self):
print """
DELETE: specifying columns
DELETE col1, col2, col3 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.
When column names do not parse as valid CQL identifiers, they can be
quoted in single quotes (CQL 2) or double quotes (CQL 3).
"""
def help_delete_where(self):
print """
DELETE: specifying rows
DELETE ... WHERE keycol = 'some_key_value';
DELETE ... WHERE keycol1 = 'val1' AND keycol2 = 'val2';
DELETE ... WHERE keycol 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 precise row
by specifying a particular primary key value (if the primary key has
multiple columns, values for each must be given). The second form
allows a list of key values to be specified using the IN operator
and a parenthesized list of comma-delimited key values.
"""
def help_create(self):
print """
There are different variants of CREATE. For more information, see
one of the following:
HELP CREATE_KEYSPACE;
HELP CREATE_TABLE;
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_table(self):
print """
CREATE TABLE <cfname> ( <colname> <type> PRIMARY KEY [,
<colname> <type> [, ...]] )
[WITH <optionname> = <val> [AND <optionname> = <val> [...]]];
CREATE TABLE statements create a new CQL table under the current
keyspace. Valid table names are strings of alphanumeric characters and
underscores, which begin with a letter.
Each table requires a primary key, which will correspond to the
underlying 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.
In cql3 mode, a table can have multiple columns composing the primary
key (see HELP COMPOSITE_PRIMARY_KEYS).
For more information, see one of the following:
HELP CREATE_TABLE_TYPES;
HELP CREATE_TABLE_OPTIONS;
"""
help_create_columnfamily = help_create_table
def help_create_table_types(self):
print """
CREATE TABLE: Specifying column types
CREATE ... (KEY <type> PRIMARY KEY,
othercol <type>) ...
It is possible to assign columns a type during table 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.
"""
help_create_columnfamily_types = help_create_table_types
def help_create_table_options(self):
print """
CREATE TABLE: Specifying columnfamily options
CREATE TABLE blah (...)
WITH optionname = val AND otheroption = val2;
A number of optional keyword arguments can be supplied to control the
configuration of a new CQL table, such as the size of the associated
row and key caches for the underlying Cassandra columnfamily. Consult
your CQL reference for the complete list of options and possible
values.
"""
help_create_columnfamily_options = help_create_table_options
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 CQL table, 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
schema-optional), but the column must already have a type (specified
during the CREATE TABLE, or added afterwards with ALTER TABLE).
"""
def help_drop(self):
print """
There are different variants of DROP. For more information, see
one of the following:
HELP DROP_KEYSPACE;
HELP DROP_TABLE;
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_table(self):
print """
DROP TABLE <tablename>;
A DROP TABLE statement results in the immediate, irreversible
removal of a CQL table and the underlying column family, including all
data contained in it.
"""
help_drop_columnfamily = help_drop_table
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 <tablename>;
TRUNCATE accepts a single argument for the table name, and permanently
removes all data from it.
"""
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 TABLE <tablename> ALTER <columnname> TYPE <type>;
ALTER TABLE <tablename> ADD <columnname> <type>;
ALTER TABLE <tablename> DROP <columnname>;
ALTER TABLE <tablename> WITH <optionname> = <val> [AND <optionname> = <val> [...]];
An ALTER statement is used to manipulate table metadata. It allows you
to add new typed columns, drop existing columns, change the data
storage type of existing columns, or change table 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 TABLE: altering existing typed columns
ALTER TABLE addamsFamily ALTER lastKnownLocation TYPE uuid;
ALTER TABLE ... 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 TABLE: adding a typed column
ALTER TABLE addamsFamily ADD gravesite varchar;
The ALTER TABLE ... 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 TABLE: dropping a typed column
ALTER TABLE addamsFamily DROP gender;
An ALTER TABLE ... 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 TABLE: changing column family properties
ALTER TABLE addamsFamily WITH comment = 'Glad to be here!'
AND read_repair_chance = 0.2;
An ALTER TABLE ... WITH statement makes adjustments to the
table properties, as defined when the table was created (see
HELP CREATE_TABLE_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 flush_output(self):
self.query_out.flush()
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
class OverrideableSchemaDecoder(cql.decoders.SchemaDecoder):
def __init__(self, schema, overrides=None):
cql.decoders.SchemaDecoder.__init__(self, schema)
self.apply_schema_overrides(overrides)
def apply_schema_overrides(self, overrides):
if overrides is None:
return
if overrides.default_name_type is not None:
self.schema.default_name_type = overrides.default_name_type
if overrides.default_value_type is not None:
self.schema.default_value_type = overrides.default_value_type
self.schema.name_types.update(overrides.name_types)
self.schema.value_types.update(overrides.value_types)
class ErrorHandlingSchemaDecoder(OverrideableSchemaDecoder):
def name_decode_error(self, err, namebytes, expectedtype):
return DecodeError(namebytes, err, expectedtype)
def value_decode_error(self, err, namebytes, valuebytes, expectedtype):
return DecodeError(valuebytes, err, expectedtype, colname=namebytes)
def option_with_default(cparser_getter, section, option, default=None):
try:
return cparser_getter(section, option)
except ConfigParser.Error:
return default
def raw_option_with_default(configs, section, option, default=None):
"""
Same (almost) as option_with_default() but won't do any string interpolation.
Useful for config values that include '%' symbol, e.g. time format string.
"""
try:
return configs.get(section, option, raw=True)
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.keyspace = option_with_default(configs.get, 'authentication', 'keyspace')
optvalues.completekey = option_with_default(configs.get, 'ui', 'completekey',
DEFAULT_COMPLETEKEY)
optvalues.color = option_with_default(configs.getboolean, 'ui', 'color')
optvalues.time_format = raw_option_with_default(configs, 'ui', 'time_format',
DEFAULT_TIME_FORMAT)
optvalues.float_precision = option_with_default(configs.getint, 'ui', 'float_precision',
DEFAULT_FLOAT_PRECISION)
optvalues.debug = False
optvalues.file = None
optvalues.tty = sys.stdin.isatty()
optvalues.cqlversion = option_with_default(configs.get, 'cql', 'version', DEFAULT_CQLVER)
(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.tty = False
if optvalues.color in (True, False):
options.color = optvalues.color
else:
if options.file is not None:
options.color = False
else:
options.color = should_use_color()
options.cqlversion, cqlvertup = full_cql_version(options.cqlversion)
if cqlvertup[0] < 3:
options.cqlmodule = cqlhandling
else:
options.cqlmodule = cql3handling
try:
port = int(port)
except ValueError:
parser.error('%r is not a valid port number.' % port)
return options, hostname, port
def setup_cqlruleset(cqlmodule):
global cqlruleset
cqlruleset = cqlmodule.CqlRuleSet
cqlruleset.append_rules(cqlsh_extra_syntax_rules)
for rulename, termname, func in cqlsh_syntax_completers:
cqlruleset.completer_for(rulename, termname)(func)
cqlruleset.commands_end_with_newline.update(my_commands_ending_with_newline)
def init_history():
if readline is not None:
try:
readline.read_history_file(HISTORY)
except IOError:
pass
delims = readline.get_completer_delims()
delims.replace("'", "")
delims += '.'
readline.set_completer_delims(delims)
def save_history():
if readline is not None:
try:
readline.write_history_file(HISTORY)
except IOError:
pass
def main(options, hostname, port):
setup_cqlruleset(options.cqlmodule)
init_history()
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))
if options.debug:
import thrift
sys.stderr.write("Using CQL driver: %s\n" % (cql,))
sys.stderr.write("Using thrift lib: %s\n" % (thrift,))
try:
shell = Shell(hostname,
port,
color=options.color,
username=options.username,
password=options.password,
stdin=stdin,
tty=options.tty,
completekey=options.completekey,
cqlver=options.cqlversion,
keyspace=options.keyspace,
display_time_format=options.time_format,
display_float_precision=options.float_precision)
except KeyboardInterrupt:
sys.exit('Connection aborted.')
except CQL_ERRORS, e:
sys.exit('Connection error: %s' % (e,))
except VersionNotSupported, e:
sys.exit('Unsupported CQL version: %s' % (e,))
if options.debug:
shell.debug = True
shell.cmdloop()
save_history()
if __name__ == '__main__':
main(*read_options(sys.argv[1:], os.environ))
# vim: set ft=python et ts=4 sw=4 :