Skip to content

Commit

Permalink
Add cratedb_toolkit.shell.run_sql utility primitive
Browse files Browse the repository at this point in the history
  • Loading branch information
amotl committed Nov 7, 2023
1 parent da0f0f5 commit dfd535f
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

## Unreleased

- Add `cratedb_toolkit.shell.run_sql` utility primitive


## 2023/11/06 v0.0.2
Expand Down
1 change: 1 addition & 0 deletions cratedb_toolkit/shell/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .crash import run_sql # noqa: F401
54 changes: 54 additions & 0 deletions cratedb_toolkit/shell/crash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import contextlib
import io
import json
import sys
import typing as t
from pathlib import Path
from unittest import mock


def run_sql(statement: t.Union[str, Path, io.IOBase], hosts: str = None, schema: str = None):
"""
Run SQL from string or file, using `crash`.
TODO: Validate it works well in different scenarios, both on CrateDB and CrateDB Cloud.
TODO: Returning stderr/logging from crash does not work yet.
"""
import crate.crash.command

sys.argv = ["crash"]
if hosts:
sys.argv += ["--hosts", hosts]
if schema:
sys.argv += ["--schema", schema]
if isinstance(statement, str):
sys.argv += ["--command", statement]
elif isinstance(statement, Path):
sys.stdin = io.StringIO(statement.read_text())
elif isinstance(statement, io.IOBase):
sys.stdin = statement # type: ignore[assignment]
else:
raise ValueError("Either statement or filepath must be given")

sys.argv += ["--format", "json"]

# Temporarily patch some shortcomings of `crash`, when used programmatically.
# TODO: See what can be done over in `crash` in a later iteration.
with mock.patch("crate.crash.repl.SQLCompleter._populate_keywords"), mock.patch(
"crate.crash.command.CrateShell.close"
):
buffer_out = io.StringIO()
buffer_err = io.StringIO()
with contextlib.redirect_stdout(buffer_out), contextlib.redirect_stderr(buffer_err):
# Invoke `crash`, to execute the SQL statement.
try:
crate.crash.command.main()
except SystemExit as ex:
if ex.code != 0:
raise

buffer_out.seek(0)
buffer_err.seek(0)
out = buffer_out.read()
err = buffer_err.read()
return json.loads(out), err
45 changes: 45 additions & 0 deletions tests/test_shell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import io

import pytest

from cratedb_toolkit.shell import run_sql


def test_run_sql_from_string():
sql = "SELECT 1;"
data, messages = run_sql(sql)
assert data == [{"1": 1}]

# TODO: Returning stderr/logging from crash does not work yet.
assert messages == ""


def test_run_sql_from_file(tmp_path):
sql_file = tmp_path / "temp.sql"
sql_file.write_text("SELECT 1;")
data, messages = run_sql(sql_file)
assert data == [{"1": 1}]

# TODO: Returning stderr/logging from crash does not work yet.
assert messages == ""


def test_run_sql_from_buffer():
sql_buffer = io.StringIO("SELECT 1;")
data, messages = run_sql(sql_buffer)
assert data == [{"1": 1}]

# TODO: Returning stderr/logging from crash does not work yet.
assert messages == ""


def test_run_sql_invalid_host(capsys):
sql = "SELECT 1;"
with pytest.raises(SystemExit) as ex:
run_sql(sql, hosts="localhost:12345")

# TODO: Returning stderr/logging from crash does not work yet.
out, err = capsys.readouterr()
assert out == ""
assert err == ""
assert ex.match("1")

0 comments on commit dfd535f

Please sign in to comment.