Skip to content

Commit bbd6c0f

Browse files
author
Stephen Hoover
authored
ENH SQL CLI (#319)
Provide options to run SQL commands through the CLI. Users can either run a command with short output from a file or from the command line, or download the results of a query.
1 parent 7fb1bb1 commit bbd6c0f

File tree

5 files changed

+124
-2
lines changed

5 files changed

+124
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
44

55
## Unreleased
66
### Added
7+
- Add CLI command "sql" for command line SQL query execution. (#319)
78
- Add helper function (run_template) to run a template given its id and return
89
either the JSON output or the associated file ids. (#318)
910
- Add helper function to list CivisML models. (#314)

civis/cli/__main__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from civis.cli._cli_commands import (
2828
civis_ascii_art, files_download_cmd, files_upload_cmd,
2929
notebooks_download_cmd, notebooks_new_cmd,
30-
notebooks_up, notebooks_down, notebooks_open)
30+
notebooks_up, notebooks_down, notebooks_open, sql_cmd)
3131
from civis.resources import get_api_spec, CACHED_SPEC_PATH
3232
from civis.resources._resources import parse_method_name
3333
from civis._utils import open_session
@@ -226,6 +226,8 @@ def add_extra_commands(cli):
226226
notebooks_cmd.add_command(notebooks_open)
227227
cli.add_command(civis_ascii_art)
228228

229+
cli.add_command(sql_cmd)
230+
229231

230232
def configure_log_level():
231233
if os.getenv('CIVIS_LOG_LEVEL'):

civis/cli/_cli_commands.py

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
"""
44
Additional commands to add to the CLI beyond the OpenAPI spec.
55
"""
6-
6+
from __future__ import print_function
7+
import functools
78
import os
9+
import sys
810

911
import click
1012
import requests
@@ -70,6 +72,90 @@ def files_download_cmd(file_id, path):
7072
civis_to_file(file_id, f)
7173

7274

75+
@click.command('sql')
76+
@click.option('--dbname', '-d', type=str, required=True,
77+
help='Execute the query on this Civis Platform database')
78+
@click.option('--command', '-c', type=str, default=None,
79+
help='Execute a single input command string')
80+
@click.option('--filename', '-f', type=click.Path(exists=True),
81+
help='Execute a query read from the given file')
82+
@click.option('--output', '-o', type=click.Path(),
83+
help='Download query results to this file')
84+
@click.option('--quiet', '-q', is_flag=True, help='Suppress screen output')
85+
@click.option('-n', type=int, default=100,
86+
help="Display up to this many rows of the result. Max 100.")
87+
def sql_cmd(dbname, command, filename, output, quiet, n):
88+
"""\b Execute a SQL query in Civis Platform
89+
90+
If neither a command nor an input file is specified, read
91+
the SQL command from stdin.
92+
If writing to an output file, use a Civis SQL script and write the
93+
entire query output to the specified file.
94+
If not writing to an output file, use a Civis Query, and return a
95+
preview of the results, up to a maximum of 100 rows.
96+
"""
97+
if filename:
98+
with open(filename, 'rt') as f:
99+
sql = f.read()
100+
elif not command:
101+
# Read the SQL query from user input. This also allows use of a heredoc
102+
lines = []
103+
while True:
104+
try:
105+
_i = input()
106+
except (KeyboardInterrupt, EOFError):
107+
# The end of a heredoc produces an EOFError.
108+
break
109+
if not _i:
110+
break
111+
else:
112+
lines.append(_i)
113+
sql = '\n'.join(lines)
114+
else:
115+
sql = command
116+
117+
if not sql:
118+
# If the user didn't enter a query, exit.
119+
if not quiet:
120+
print('Did not receive a SQL query.', file=sys.stderr)
121+
return
122+
123+
if not quiet:
124+
print('\nExecuting query...', file=sys.stderr)
125+
if output:
126+
fut = civis.io.civis_to_csv(output, sql, database=dbname)
127+
fut.result() # Block for completion and raise exceptions if any
128+
if not quiet:
129+
print("Downloaded the result of the query to %s." % output,
130+
file=sys.stderr)
131+
else:
132+
fut = civis.io.query_civis(sql, database=dbname,
133+
preview_rows=n, polling_interval=3)
134+
cols = fut.result()['result_columns']
135+
rows = fut.result()['result_rows']
136+
if not quiet:
137+
print('...Query complete.\n', file=sys.stderr)
138+
print(_str_table_result(cols, rows))
139+
140+
141+
def _str_table_result(cols, rows):
142+
"""Turn a Civis Query result into a readable table."""
143+
# Determine the maximum width of each column.
144+
# First find the width of each element in each row, then find the max
145+
# width in each position.
146+
max_len = functools.reduce(
147+
lambda x, y: [max(z) for z in zip(x, y)],
148+
[[len(_v) for _v in _r] for _r in [cols] + rows])
149+
150+
header_str = " | ".join("{0:<{width}}".format(_v, width=_l)
151+
for _l, _v in zip(max_len, cols))
152+
tb_strs = [header_str, len(header_str) * '-']
153+
for row in rows:
154+
tb_strs.append(" | ".join("{0:>{width}}".format(_v, width=_l)
155+
for _l, _v in zip(max_len, row)))
156+
return '\n'.join(tb_strs)
157+
158+
73159
@click.command('download')
74160
@click.argument('notebook_id', type=int)
75161
@click.argument('path')

civis/tests/test_cli.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pytest
66

77
from civis.cli.__main__ import generate_cli, invoke, make_operation_name
8+
from civis.cli._cli_commands import _str_table_result
89
from civis.compat import mock
910
from civis.tests import TEST_SPEC
1011

@@ -118,3 +119,11 @@ def test_parameter_case(mock_session):
118119
)
119120
def test_make_operation_name(path, method, resource_name, exp):
120121
assert make_operation_name(path, method, resource_name) == exp
122+
123+
124+
def test_str_table_result():
125+
cols = ['a', 'snake!']
126+
rows = [['2', '3'], ['1.1', '3.3']]
127+
128+
out = _str_table_result(cols, rows)
129+
assert out == "a | snake!\n------------\n 2 | 3\n1.1 | 3.3"

docs/source/cli.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,27 @@ backend for your Jupyter notebooks.
5959
- ``civis notebooks open $NOTEBOOK_ID``
6060

6161
Open an existing notebook (which may or may not be running) in your default browser.
62+
63+
SQL
64+
---
65+
66+
The Civis CLI allows for easy running of SQL queries on Civis Platform
67+
through the following commands:
68+
69+
- ``civis sql [-n $MAX_LINES] -d $DATABASE_NAME -f $FILE_NAME``
70+
71+
Read a SQL query from a text file and run it on the specified database.
72+
The results of the query, if any, will be shown after it completes
73+
(up to a maximum of $MAX_LINES rows, defaulting to 100).
74+
75+
- ``civis sql [-n $MAX_LINES] -d $DATABASE_NAME -c [$SQL_QUERY]``
76+
77+
Instead of reading from a file, read query text from a command line
78+
argument. If you do not provide a query on the command line,
79+
the query text will be taken from stdin.
80+
81+
- ``civis sql -d $DATABASE_NAME [-f $SQL_FILE_NAME] -o $OUTPUT_FILE_NAME``
82+
83+
With the `-o` or `--output` option specified, the complete results
84+
of the query will be downloaded to a CSV file at the requested location
85+
after the query completes.

0 commit comments

Comments
 (0)