Skip to content

Commit f400588

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

5 files changed

Lines changed: 286 additions & 19 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: 31 additions & 19 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,18 @@ 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
297+
298+
if self.mode in ('csv', 'json'):
299+
self.color = False
294300

295301
if isinstance(auth_provider, PlainTextAuthProvider):
296302
self.username = auth_provider.username
@@ -947,7 +953,8 @@ def perform_simple_statement(self, statement):
947953
elif result:
948954
# CAS INSERT/UPDATE
949955
self.writeresult("")
950-
self.print_static_result(result, self.parse_for_update_meta(statement.query_string), with_header=True, tty=self.tty)
956+
self.print_static_result(result, self.parse_for_update_meta(statement.query_string), with_header=True, tty=self.tty,
957+
printer=TablePrinter.factory(self.mode, self))
951958
if self.elapsed_enabled:
952959
self.writeresult("(%dms elapsed)" % elapsed)
953960
self.flush_output()
@@ -956,32 +963,33 @@ def perform_simple_statement(self, statement):
956963
def print_result(self, result, table_meta):
957964
self.decoding_errors = []
958965

959-
self.writeresult("")
966+
if self.mode not in ('csv', 'json'):
967+
self.writeresult("")
968+
printer = TablePrinter.factory(self.mode, self)
960969

961-
def print_all(result, table_meta, tty):
962-
# Return the number of rows in total
970+
def print_all(result, table_meta, tty, printer):
963971
num_rows = 0
964972
is_first = True
965973
while True:
966-
# Always print for the first page even it is empty
967974
if result.current_rows or is_first:
968975
with_header = is_first or tty
969-
self.print_static_result(result, table_meta, with_header, tty, num_rows)
976+
self.print_static_result(result, table_meta, with_header, tty, num_rows, printer)
970977
num_rows += len(result.current_rows)
971978
if result.has_more_pages:
972979
if self.shunted_query_out is None and tty:
973-
# Only pause when not capturing.
974980
input("---MORE---")
975981
result.fetch_next_page()
976982
else:
977-
if not tty:
983+
if not tty and self.mode not in ('csv', 'json'):
978984
self.writeresult("")
979985
break
980986
is_first = False
981987
return num_rows
982988

983-
num_rows = print_all(result, table_meta, self.tty)
984-
self.writeresult("(%d rows)" % num_rows)
989+
num_rows = print_all(result, table_meta, self.tty, printer)
990+
printer.finish()
991+
if self.mode not in ('csv', 'json'):
992+
self.writeresult("(%d rows)" % num_rows)
985993

986994
if self.decoding_errors:
987995
for err in self.decoding_errors[:2]:
@@ -990,15 +998,16 @@ def print_all(result, table_meta, tty):
990998
self.writeresult('%d more decoding errors suppressed.'
991999
% (len(self.decoding_errors) - 2), color=RED)
9921000

993-
def print_static_result(self, result, table_meta, with_header, tty, row_count_offset=0):
1001+
def print_static_result(self, result, table_meta, with_header, tty, row_count_offset=0, printer=None):
9941002
if not result.column_names and not table_meta:
9951003
return
9961004

9971005
column_names = result.column_names or list(table_meta.columns.keys())
9981006
formatted_names = [self.myformat_colname(name, table_meta) for name in column_names]
1007+
9991008
if not result.current_rows:
1000-
# print header only
1001-
self.print_formatted_result(formatted_names, None, with_header=True, tty=tty)
1009+
if with_header:
1010+
printer.print_header(formatted_names)
10021011
return
10031012

10041013
cql_types = []
@@ -1009,10 +1018,9 @@ def print_static_result(self, result, table_meta, with_header, tty, row_count_of
10091018

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

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)
1021+
if with_header:
1022+
printer.print_header(formatted_names)
1023+
printer.print_rows(formatted_names, formatted_values)
10161024

10171025
def print_formatted_result(self, formatted_names, formatted_values, with_header, tty):
10181026
# determine column widths
@@ -2026,6 +2034,7 @@ def read_options(cmdlineargs, parser, config_file, cql_dir, environment=os.envir
20262034
argvalues.completekey = option_with_default(configs.get, 'ui', 'completekey',
20272035
DEFAULT_COMPLETEKEY)
20282036
argvalues.color = option_with_default(configs.getboolean, 'ui', 'color')
2037+
argvalues.mode = option_with_default(configs.get, 'ui', 'mode', 'tabular')
20292038
argvalues.time_format = raw_option_with_default(configs, 'ui', 'time_format',
20302039
DEFAULT_TIMESTAMP_FORMAT)
20312040
argvalues.nanotime_format = raw_option_with_default(configs, 'ui', 'nanotime_format',
@@ -2230,6 +2239,8 @@ def main(cmdline, pkgpath):
22302239
help='Force tty mode (command prompt).')
22312240
parser.add_argument('--disable-history', default=False, action='store_true',
22322241
help='Disable saving of history (existing history will still be loaded)')
2242+
parser.add_argument('--mode', choices=['tabular', 'csv', 'json'],
2243+
help='Specify the output format (tabular, csv, json). Default is tabular.')
22332244

22342245
# This is a hidden option to suppress the warning when the -p/--password command line option is used.
22352246
# 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 +2368,7 @@ def main(cmdline, pkgpath):
23572368
display_double_precision=options.double_precision,
23582369
display_timezone=timezone,
23592370
max_trace_wait=options.max_trace_wait,
2371+
mode=options.mode,
23602372
ssl=options.ssl,
23612373
single_statement=options.execute,
23622374
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)