mirror of https://github.com/apache/cassandra
2802 lines
103 KiB
Python
Executable File
2802 lines
103 KiB
Python
Executable File
#!/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 :
|