Skip to content

Commit 45e26f6

Browse files
CASSANDRA-19985: Enhance CQLSH to support machine-readable output formatting (csv, json)
1 parent 42afa1b commit 45e26f6

5 files changed

Lines changed: 300 additions & 23 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: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@
4646
from cqlshlib import cql3handling, pylexotron, sslhandling, cqlshhandling, authproviderhandling
4747
from cqlshlib.copyutil import ExportTask, ImportTask
4848
from cqlshlib.displaying import (ANSI_RESET, BLUE, COLUMN_NAME_COLORS, CYAN,
49-
RED, WHITE, FormattedValue, colorme)
49+
RED, WHITE, FormattedValue, colorme,
50+
TablePrinter, TabularTablePrinter, CsvTablePrinter, JsonTablePrinter)
5051
from cqlshlib.formatting import (DEFAULT_DATE_FORMAT, DEFAULT_NANOTIME_FORMAT,
5152
DEFAULT_TIMESTAMP_FORMAT, CqlType, DateTimeFormat,
5253
format_by_type)
@@ -284,13 +285,15 @@ def __init__(self, hostname, port, config_file, color=False,
284285
connect_timeout=DEFAULT_CONNECT_TIMEOUT_SECONDS,
285286
is_subshell=False,
286287
auth_provider=None,
287-
disable_history=False):
288+
disable_history=False,
289+
mode='tabular'):
288290
cmd.Cmd.__init__(self, completekey=completekey)
289291
self.hostname = hostname
290292
self.port = port
291293
self.auth_provider = auth_provider
292294
self.username = username
293295
self.config_file = config_file
296+
self.mode = mode.lower()
294297

295298
if isinstance(auth_provider, PlainTextAuthProvider):
296299
self.username = auth_provider.username
@@ -329,6 +332,8 @@ def __init__(self, hostname, port, config_file, color=False,
329332
self.browser = browser
330333
self.docspath = docspath
331334
self.color = color
335+
if self.mode in ('csv', 'json'):
336+
self.color = False
332337

333338
self.display_nanotime_format = display_nanotime_format
334339
self.display_timestamp_format = display_timestamp_format
@@ -946,42 +951,55 @@ def perform_simple_statement(self, statement):
946951
self.print_result(result, self.get_table_meta('system_auth', 'generated_values'))
947952
elif result:
948953
# CAS INSERT/UPDATE
949-
self.writeresult("")
950-
self.print_static_result(result, self.parse_for_update_meta(statement.query_string), with_header=True, tty=self.tty)
954+
if self.mode not in ('csv', 'json'):
955+
self.writeresult("")
956+
cas_printer = TablePrinter.factory(self.mode, self)
957+
self.print_static_result(result, self.parse_for_update_meta(statement.query_string),
958+
with_header=True, tty=self.tty,
959+
printer=cas_printer)
960+
cas_printer.finish()
951961
if self.elapsed_enabled:
952-
self.writeresult("(%dms elapsed)" % elapsed)
962+
elapsed_msg = "(%dms elapsed)" % elapsed
963+
if self.mode in ('csv', 'json'):
964+
self.printerr(elapsed_msg)
965+
else:
966+
self.writeresult(elapsed_msg)
953967
self.flush_output()
954968
return True, future
955969

956970
def print_result(self, result, table_meta):
957971
self.decoding_errors = []
958972

959-
self.writeresult("")
973+
if self.mode not in ('csv', 'json'):
974+
self.writeresult("")
975+
printer = TablePrinter.factory(self.mode, self)
960976

961-
def print_all(result, table_meta, tty):
962-
# Return the number of rows in total
977+
def print_all(result, table_meta, tty, printer):
978+
machine_mode = self.mode in ('csv', 'json')
979+
effective_tty = tty and not machine_mode
963980
num_rows = 0
964981
is_first = True
965982
while True:
966-
# Always print for the first page even it is empty
967983
if result.current_rows or is_first:
968-
with_header = is_first or tty
969-
self.print_static_result(result, table_meta, with_header, tty, num_rows)
984+
with_header = is_first or effective_tty
985+
self.print_static_result(result, table_meta, with_header, effective_tty,
986+
num_rows, printer)
970987
num_rows += len(result.current_rows)
971988
if result.has_more_pages:
972-
if self.shunted_query_out is None and tty:
973-
# Only pause when not capturing.
989+
if self.shunted_query_out is None and effective_tty:
974990
input("---MORE---")
975991
result.fetch_next_page()
976992
else:
977-
if not tty:
993+
if not effective_tty and not machine_mode:
978994
self.writeresult("")
979995
break
980996
is_first = False
981997
return num_rows
982998

983-
num_rows = print_all(result, table_meta, self.tty)
984-
self.writeresult("(%d rows)" % num_rows)
999+
num_rows = print_all(result, table_meta, self.tty, printer)
1000+
printer.finish()
1001+
if self.mode not in ('csv', 'json'):
1002+
self.writeresult("(%d rows)" % num_rows)
9851003

9861004
if self.decoding_errors:
9871005
for err in self.decoding_errors[:2]:
@@ -990,15 +1008,16 @@ def print_all(result, table_meta, tty):
9901008
self.writeresult('%d more decoding errors suppressed.'
9911009
% (len(self.decoding_errors) - 2), color=RED)
9921010

993-
def print_static_result(self, result, table_meta, with_header, tty, row_count_offset=0):
1011+
def print_static_result(self, result, table_meta, with_header, tty, row_count_offset=0, printer=None):
9941012
if not result.column_names and not table_meta:
9951013
return
9961014

9971015
column_names = result.column_names or list(table_meta.columns.keys())
9981016
formatted_names = [self.myformat_colname(name, table_meta) for name in column_names]
1017+
9991018
if not result.current_rows:
1000-
# print header only
1001-
self.print_formatted_result(formatted_names, None, with_header=True, tty=tty)
1019+
if with_header:
1020+
printer.print_header(formatted_names)
10021021
return
10031022

10041023
cql_types = []
@@ -1009,10 +1028,9 @@ def print_static_result(self, result, table_meta, with_header, tty, row_count_of
10091028

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

1012-
if self.expand_enabled:
1013-
self.print_formatted_result_vertically(formatted_names, formatted_values, row_count_offset)
1014-
else:
1015-
self.print_formatted_result(formatted_names, formatted_values, with_header, tty)
1031+
if with_header:
1032+
printer.print_header(formatted_names)
1033+
printer.print_rows(formatted_names, formatted_values)
10161034

10171035
def print_formatted_result(self, formatted_names, formatted_values, with_header, tty):
10181036
# determine column widths
@@ -2026,6 +2044,7 @@ def read_options(cmdlineargs, parser, config_file, cql_dir, environment=os.envir
20262044
argvalues.completekey = option_with_default(configs.get, 'ui', 'completekey',
20272045
DEFAULT_COMPLETEKEY)
20282046
argvalues.color = option_with_default(configs.getboolean, 'ui', 'color')
2047+
argvalues.mode = option_with_default(configs.get, 'ui', 'mode', 'tabular')
20292048
argvalues.time_format = raw_option_with_default(configs, 'ui', 'time_format',
20302049
DEFAULT_TIMESTAMP_FORMAT)
20312050
argvalues.nanotime_format = raw_option_with_default(configs, 'ui', 'nanotime_format',
@@ -2230,6 +2249,8 @@ def main(cmdline, pkgpath):
22302249
help='Force tty mode (command prompt).')
22312250
parser.add_argument('--disable-history', default=False, action='store_true',
22322251
help='Disable saving of history (existing history will still be loaded)')
2252+
parser.add_argument('--mode', choices=['tabular', 'csv', 'json'],
2253+
help='Specify the output format (tabular, csv, json). Default is tabular.')
22332254

22342255
# This is a hidden option to suppress the warning when the -p/--password command line option is used.
22352256
# 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 +2378,7 @@ def main(cmdline, pkgpath):
23572378
display_double_precision=options.double_precision,
23582379
display_timezone=timezone,
23592380
max_trace_wait=options.max_trace_wait,
2381+
mode=options.mode,
23602382
ssl=options.ssl,
23612383
single_statement=options.execute,
23622384
request_timeout=options.request_timeout,

pylib/cqlshlib/displaying.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,101 @@ def color_ljust(self, width, fill=' '):
126126
)
127127

128128
NO_COLOR_MAP = dict()
129+
130+
class TablePrinter:
131+
def print_header(self, formatted_names):
132+
raise NotImplementedError
133+
134+
def print_rows(self, formatted_names, formatted_values):
135+
raise NotImplementedError
136+
137+
def finish(self):
138+
pass
139+
140+
@staticmethod
141+
def factory(format_type, shell):
142+
format_map = {'csv': CsvTablePrinter, 'json': JsonTablePrinter, 'tabular': TabularTablePrinter}
143+
printer_cls = format_map.get(format_type.lower(), TabularTablePrinter)
144+
return printer_cls(shell) if format_type.lower() != 'tabular' else printer_cls(shell, shell.tty)
145+
146+
class TabularTablePrinter(TablePrinter):
147+
def __init__(self, shell, tty, row_count_offset=0):
148+
self._shell = shell
149+
self._tty = tty
150+
self._row_count_offset = row_count_offset
151+
self._pending_header = None
152+
153+
def print_header(self, formatted_names):
154+
# Store only — cannot render yet because column widths depend on
155+
# data values. print_rows will render header+data together.
156+
# Empty-result case is handled in finish().
157+
self._pending_header = formatted_names
158+
159+
def print_rows(self, formatted_names, formatted_values):
160+
# with_header=True only when print_header was called for this page.
161+
with_header = self._pending_header is not None
162+
self._pending_header = None
163+
if self._shell.expand_enabled:
164+
self._shell.print_formatted_result_vertically(
165+
formatted_names, formatted_values, self._row_count_offset)
166+
else:
167+
self._shell.print_formatted_result(
168+
formatted_names, formatted_values, with_header, self._tty)
169+
if formatted_values:
170+
self._row_count_offset += len(formatted_values)
171+
172+
def finish(self):
173+
if self._pending_header is not None:
174+
self._shell.print_formatted_result(
175+
self._pending_header, None, with_header=True, tty=self._tty)
176+
self._pending_header = None
177+
178+
class CsvTablePrinter(TablePrinter):
179+
def __init__(self, shell):
180+
import csv
181+
self._writer = csv.writer(shell.query_out)
182+
self._header_written = False
183+
self._colnames = None
184+
185+
def print_header(self, formatted_names):
186+
self._colnames = [n.strval for n in formatted_names]
187+
188+
def print_rows(self, formatted_names, formatted_values):
189+
if not self._header_written:
190+
self._writer.writerow(self._colnames)
191+
self._header_written = True
192+
if formatted_values is None:
193+
return
194+
for row in formatted_values:
195+
self._writer.writerow([col.strval for col in row])
196+
197+
def finish(self):
198+
if self._colnames is not None and not self._header_written:
199+
self._writer.writerow(self._colnames)
200+
self._header_written = True
201+
202+
class JsonTablePrinter(TablePrinter):
203+
def __init__(self, shell):
204+
self._shell = shell
205+
self._colnames = None
206+
self._first_row = True
207+
208+
def print_header(self, formatted_names):
209+
self._colnames = [n.strval for n in formatted_names]
210+
self._shell.writeresult('[')
211+
212+
def print_rows(self, formatted_names, formatted_values):
213+
import json
214+
if formatted_values is None:
215+
return
216+
for row in formatted_values:
217+
row_dict = {self._colnames[i]: col.strval for i, col in enumerate(row)}
218+
serialized = json.dumps(row_dict)
219+
if self._first_row:
220+
self._shell.writeresult(' ' + serialized, newline=False)
221+
self._first_row = False
222+
else:
223+
self._shell.writeresult(',\n ' + serialized, newline=False)
224+
225+
def finish(self):
226+
self._shell.writeresult('\n]')

0 commit comments

Comments
 (0)