Skip to content

Commit f33ca19

Browse files
CASSANDRA-19985: Enhance CQLSH to support machine-readable output formatting (csv, json)
1 parent 02352b7 commit f33ca19

4 files changed

Lines changed: 92 additions & 4 deletions

File tree

conf/cqlshrc.sample

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
; version = None
3838

3939
[ui]
40+
;; The format of the output. Valid values are tabular, csv, and json.
41+
; mode = tabular
42+
4043
;; Whether or not to display query results with colors
4144
; color = on
4245

doc/modules/cassandra/pages/managing/tools/cqlsh.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ Options:
9898
Collect coverage data
9999
`--encoding=ENCODING`::
100100
Specify a non-default encoding for output. (Default: utf-8)
101+
`--mode=MODE`::
102+
Specify the output display format. Valid values are `tabular` (default), `csv`, and `json`.
101103
`--cqlshrc=CQLSHRC`::
102104
Specify an alternative cqlshrc file location.
103105
`--credentials=CREDENTIALS`::

pylib/cqlshlib/cqlshmain.py

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -284,13 +284,18 @@ def __init__(self, hostname, port, config_file, color=False,
284284
connect_timeout=DEFAULT_CONNECT_TIMEOUT_SECONDS,
285285
is_subshell=False,
286286
auth_provider=None,
287-
disable_history=False):
287+
disable_history=False,
288+
mode='tabular'):
288289
cmd.Cmd.__init__(self, completekey=completekey)
289290
self.hostname = hostname
290291
self.port = port
291292
self.auth_provider = auth_provider
292293
self.username = username
293294
self.config_file = config_file
295+
self.mode = mode
296+
297+
if self.mode in ('csv', 'json'):
298+
self.color = False
294299

295300
if isinstance(auth_provider, PlainTextAuthProvider):
296301
self.username = auth_provider.username
@@ -956,7 +961,8 @@ def perform_simple_statement(self, statement):
956961
def print_result(self, result, table_meta):
957962
self.decoding_errors = []
958963

959-
self.writeresult("")
964+
if self.mode not in ('csv', 'json'):
965+
self.writeresult("")
960966

961967
def print_all(result, table_meta, tty):
962968
# Return the number of rows in total
@@ -981,7 +987,8 @@ def print_all(result, table_meta, tty):
981987
return num_rows
982988

983989
num_rows = print_all(result, table_meta, self.tty)
984-
self.writeresult("(%d rows)" % num_rows)
990+
if self.mode not in ('csv', 'json'):
991+
self.writeresult("(%d rows)" % num_rows)
985992

986993
if self.decoding_errors:
987994
for err in self.decoding_errors[:2]:
@@ -1009,7 +1016,11 @@ def print_static_result(self, result, table_meta, with_header, tty, row_count_of
10091016

10101017
formatted_values = [list(map(self.myformat_value, [row[c] for c in column_names], cql_types)) for row in result.current_rows]
10111018

1012-
if self.expand_enabled:
1019+
if self.mode == 'csv':
1020+
self.print_csv_result(formatted_names, formatted_values, with_header, tty)
1021+
elif self.mode == 'json':
1022+
self.print_json_result(formatted_names, formatted_values, with_header, tty)
1023+
elif self.expand_enabled:
10131024
self.print_formatted_result_vertically(formatted_names, formatted_values, row_count_offset)
10141025
else:
10151026
self.print_formatted_result(formatted_names, formatted_values, with_header, tty)
@@ -1055,6 +1066,42 @@ def print_formatted_result_vertically(self, formatted_names, formatted_values, r
10551066
self.writeresult(' ' + " | ".join([column, value]))
10561067
self.writeresult('')
10571068

1069+
def print_csv_result(self, formatted_names, formatted_values, with_header, tty):
1070+
import csv
1071+
writer = csv.writer(self.query_out)
1072+
1073+
if with_header:
1074+
headers = [hdr.strval for hdr in formatted_names]
1075+
writer.writerow(headers)
1076+
1077+
if formatted_values is None:
1078+
return
1079+
1080+
for row in formatted_values:
1081+
csv_row = [col.strval for col in row]
1082+
writer.writerow(csv_row)
1083+
1084+
def print_json_result(self, formatted_names, formatted_values, with_header, tty):
1085+
import json
1086+
1087+
if formatted_values is None:
1088+
self.writeresult("[]")
1089+
return
1090+
1091+
headers = [hdr.strval for hdr in formatted_names]
1092+
result_list = []
1093+
1094+
for row in formatted_values:
1095+
row_dict = {}
1096+
for i, col in enumerate(row):
1097+
row_dict[headers[i]] = col.strval
1098+
result_list.append(row_dict)
1099+
1100+
self.writeresult(json.dumps(result_list, indent=2))
1101+
1102+
if tty:
1103+
self.writeresult("")
1104+
10581105
def print_warnings(self, warnings):
10591106
if warnings is None or len(warnings) == 0:
10601107
return
@@ -2026,6 +2073,7 @@ def read_options(cmdlineargs, parser, config_file, cql_dir, environment=os.envir
20262073
argvalues.completekey = option_with_default(configs.get, 'ui', 'completekey',
20272074
DEFAULT_COMPLETEKEY)
20282075
argvalues.color = option_with_default(configs.getboolean, 'ui', 'color')
2076+
argvalues.mode = option_with_default(configs.get, 'ui', 'mode', 'tabular')
20292077
argvalues.time_format = raw_option_with_default(configs, 'ui', 'time_format',
20302078
DEFAULT_TIMESTAMP_FORMAT)
20312079
argvalues.nanotime_format = raw_option_with_default(configs, 'ui', 'nanotime_format',
@@ -2230,6 +2278,8 @@ def main(cmdline, pkgpath):
22302278
help='Force tty mode (command prompt).')
22312279
parser.add_argument('--disable-history', default=False, action='store_true',
22322280
help='Disable saving of history (existing history will still be loaded)')
2281+
parser.add_argument('--mode', choices=['tabular', 'csv', 'json'],
2282+
help='Specify the output format (tabular, csv, json). Default is tabular.')
22332283

22342284
# This is a hidden option to suppress the warning when the -p/--password command line option is used.
22352285
# Power users may use this option if they know no other people has access to the system where cqlsh is run or don't care about security.
@@ -2357,6 +2407,7 @@ def main(cmdline, pkgpath):
23572407
display_double_precision=options.double_precision,
23582408
display_timezone=timezone,
23592409
max_trace_wait=options.max_trace_wait,
2410+
mode=options.mode,
23602411
ssl=options.ssl,
23612412
single_statement=options.execute,
23622413
request_timeout=options.request_timeout,

pylib/cqlshlib/test/test_cqlsh_output.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,3 +1017,35 @@ def test_quoted_output_text_in_udts(self):
10171017
tty=False, input=query)
10181018
self.assertEqual(0, result)
10191019
self.assertEqual(output.splitlines()[3].strip(), "{data: 'I''m newb'}")
1020+
1021+
def test_csv_output(self):
1022+
ks = get_keyspace()
1023+
query = "SELECT a, b FROM twenty_rows_table WHERE a IN ('1', '2');"
1024+
1025+
output, result = cqlsh_testcall(args=('--mode', 'csv'), prompt=None, env=self.default_env,
1026+
tty=False, input=query + '\n')
1027+
self.assertEqual(0, result)
1028+
1029+
lines = output.strip().splitlines()
1030+
self.assertEqual(lines[0].strip(), 'a,b')
1031+
self.assertIn('1,1', [l.strip() for l in lines])
1032+
self.assertIn('2,2', [l.strip() for l in lines])
1033+
1034+
def test_json_output(self):
1035+
ks = get_keyspace()
1036+
query = "SELECT a, b FROM twenty_rows_table WHERE a IN ('1', '2');"
1037+
1038+
output, result = cqlsh_testcall(args=('--mode', 'json'), prompt=None, env=self.default_env,
1039+
tty=False, input=query + '\n')
1040+
self.assertEqual(0, result)
1041+
1042+
import json
1043+
try:
1044+
parsed_json = json.loads(output)
1045+
self.assertEqual(len(parsed_json), 2)
1046+
1047+
results = { (item['a'], item['b']) for item in parsed_json }
1048+
self.assertIn(('1', '1'), results)
1049+
self.assertIn(('2', '2'), results)
1050+
except ValueError as e:
1051+
self.fail("Output is not valid JSON: %s\nOutput was:\n%s" % (e, output))

0 commit comments

Comments
 (0)