-
-
Notifications
You must be signed in to change notification settings - Fork 32.3k
gh-77617: Add interactive shell for sqlite3 #95026
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
def2e81
Add proof-of-concept REPL
1f01905
Add NEWS
erlend-aasland 86a4566
Remove redundant __name__ condition
erlend-aasland d04b6f3
Merge branch 'main' into sqlite-shell
erlend-aasland 896834c
Add -v argument for dumping SQLite version
erlend-aasland 4ba731a
Address review: close connection explicitly
erlend-aasland 814cced
Merge branch 'main' into sqlite-shell
erlend-aasland 8179a68
Add docs
erlend-aasland 91a77b7
Remove copyright, credits, and license cli commands
erlend-aasland 2f27a04
Document how to quit
erlend-aasland ae55e8e
Fix sphinx option refs by moving the footnote up to where it belongs,…
erlend-aasland 91e1ec9
Merge branch 'main' into sqlite-shell
erlend-aasland 024cd90
Address review: iterate over cursor
dee441a
Address review: repr iso. f-string
ec04ea3
Address review: pass connection iso. path
erlend-aasland 9b79230
Address review: use argparse version trick
erlend-aasland 74fb53d
Address review: use . prefix for commands, and sys.exit for quitting
erlend-aasland 1d62ccb
Address review: reduce indent level
erlend-aasland 340b896
Add filename and sql args a la sqlite3 cli
erlend-aasland d906361
Fix constructor
erlend-aasland 43c535c
Sync with main
erlend-aasland e728e17
Address Serhiy's second round of review
erlend-aasland e2e24b8
Remove useless function and lambda params
erlend-aasland 294acb9
Move CLI docs to Reference
erlend-aasland 3cfff7f
Improve NEWS and add What's New
erlend-aasland 82c753c
Partially address Kumar's review: use narrow exception
erlend-aasland 36454ab
Partially address more of Kumar's review: add guard
erlend-aasland 625e2e3
Use pattern matching
erlend-aasland f624873
Revert "Partially address more of Kumar's review: add guard"
erlend-aasland df75f5f
Print errors to stderr iso. stdout
erlend-aasland 44c2571
Non-zero exit for SQL errors passed from command line
erlend-aasland d640458
Add tests
erlend-aasland 0997004
Simplify interactive tests
erlend-aasland 7ba91e7
Use -Xutf8
erlend-aasland b74e010
Fix argument order
erlend-aasland 903b867
Address review: move cleanups up
erlend-aasland 796ba16
Last adjustment: inline runsql()
erlend-aasland File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import sqlite3 | ||
import sys | ||
|
||
from argparse import ArgumentParser | ||
from code import InteractiveConsole | ||
from textwrap import dedent | ||
|
||
|
||
def execute(c, sql, suppress_errors=True): | ||
try: | ||
for row in c.execute(sql): | ||
print(row) | ||
except sqlite3.Error as e: | ||
tp = type(e).__name__ | ||
try: | ||
print(f"{tp} ({e.sqlite_errorname}): {e}", file=sys.stderr) | ||
except AttributeError: | ||
print(f"{tp}: {e}", file=sys.stderr) | ||
if not suppress_errors: | ||
sys.exit(1) | ||
|
||
|
||
class SqliteInteractiveConsole(InteractiveConsole): | ||
|
||
def __init__(self, connection): | ||
super().__init__() | ||
self._con = connection | ||
self._cur = connection.cursor() | ||
|
||
def runsource(self, source, filename="<input>", symbol="single"): | ||
match source: | ||
case ".version": | ||
print(f"{sqlite3.sqlite_version}") | ||
case ".help": | ||
print("Enter SQL code and press enter.") | ||
case ".quit": | ||
sys.exit(0) | ||
case _: | ||
if not sqlite3.complete_statement(source): | ||
return True | ||
execute(self._cur, source) | ||
return False | ||
|
||
|
||
def main(): | ||
parser = ArgumentParser( | ||
description="Python sqlite3 CLI", | ||
prog="python -m sqlite3", | ||
) | ||
parser.add_argument( | ||
"filename", type=str, default=":memory:", nargs="?", | ||
help=( | ||
"SQLite database to open (defaults to ':memory:'). " | ||
"A new database is created if the file does not previously exist." | ||
), | ||
) | ||
parser.add_argument( | ||
"sql", type=str, nargs="?", | ||
help=( | ||
"An SQL query to execute. " | ||
"Any returned rows are printed to stdout." | ||
), | ||
) | ||
parser.add_argument( | ||
"-v", "--version", action="version", | ||
version=f"SQLite version {sqlite3.sqlite_version}", | ||
help="Print underlying SQLite library version", | ||
) | ||
args = parser.parse_args() | ||
|
||
if args.filename == ":memory:": | ||
db_name = "a transient in-memory database" | ||
else: | ||
db_name = repr(args.filename) | ||
|
||
banner = dedent(f""" | ||
sqlite3 shell, running on SQLite version {sqlite3.sqlite_version} | ||
Connected to {db_name} | ||
|
||
Each command will be run using execute() on the cursor. | ||
Type ".help" for more information; type ".quit" or CTRL-D to quit. | ||
""").strip() | ||
sys.ps1 = "sqlite> " | ||
sys.ps2 = " ... " | ||
|
||
con = sqlite3.connect(args.filename, isolation_level=None) | ||
try: | ||
if args.sql: | ||
execute(con, args.sql, suppress_errors=False) | ||
else: | ||
console = SqliteInteractiveConsole(con) | ||
console.interact(banner, exitmsg="") | ||
finally: | ||
con.close() | ||
|
||
|
||
main() | ||
erlend-aasland marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
"""sqlite3 CLI tests.""" | ||
|
||
import sqlite3 as sqlite | ||
import subprocess | ||
import sys | ||
import unittest | ||
|
||
from test.support import SHORT_TIMEOUT, requires_subprocess | ||
from test.support.os_helper import TESTFN, unlink | ||
|
||
|
||
@requires_subprocess() | ||
class CommandLineInterface(unittest.TestCase): | ||
|
||
def _do_test(self, *args, expect_success=True): | ||
with subprocess.Popen( | ||
[sys.executable, "-Xutf8", "-m", "sqlite3", *args], | ||
encoding="utf-8", | ||
bufsize=0, | ||
stdout=subprocess.PIPE, | ||
stderr=subprocess.PIPE, | ||
) as proc: | ||
proc.wait() | ||
if expect_success == bool(proc.returncode): | ||
self.fail("".join(proc.stderr)) | ||
stdout = proc.stdout.read() | ||
stderr = proc.stderr.read() | ||
if expect_success: | ||
self.assertEqual(stderr, "") | ||
else: | ||
self.assertEqual(stdout, "") | ||
return stdout, stderr | ||
|
||
def expect_success(self, *args): | ||
out, _ = self._do_test(*args) | ||
return out | ||
|
||
def expect_failure(self, *args): | ||
_, err = self._do_test(*args, expect_success=False) | ||
return err | ||
|
||
def test_cli_help(self): | ||
out = self.expect_success("-h") | ||
self.assertIn("usage: python -m sqlite3", out) | ||
|
||
def test_cli_version(self): | ||
out = self.expect_success("-v") | ||
self.assertIn(sqlite.sqlite_version, out) | ||
|
||
def test_cli_execute_sql(self): | ||
out = self.expect_success(":memory:", "select 1") | ||
self.assertIn("(1,)", out) | ||
|
||
def test_cli_execute_too_much_sql(self): | ||
stderr = self.expect_failure(":memory:", "select 1; select 2") | ||
err = "ProgrammingError: You can only execute one statement at a time" | ||
self.assertIn(err, stderr) | ||
|
||
def test_cli_execute_incomplete_sql(self): | ||
stderr = self.expect_failure(":memory:", "sel") | ||
self.assertIn("OperationalError (SQLITE_ERROR)", stderr) | ||
|
||
def test_cli_on_disk_db(self): | ||
self.addCleanup(unlink, TESTFN) | ||
erlend-aasland marked this conversation as resolved.
Show resolved
Hide resolved
|
||
out = self.expect_success(TESTFN, "create table t(t)") | ||
self.assertEqual(out, "") | ||
out = self.expect_success(TESTFN, "select count(t) from t") | ||
self.assertIn("(0,)", out) | ||
|
||
|
||
@requires_subprocess() | ||
class InteractiveSession(unittest.TestCase): | ||
TIMEOUT = SHORT_TIMEOUT / 10. | ||
MEMORY_DB_MSG = "Connected to a transient in-memory database" | ||
PS1 = "sqlite> " | ||
PS2 = "... " | ||
|
||
def start_cli(self, *args): | ||
return subprocess.Popen( | ||
[sys.executable, "-Xutf8", "-m", "sqlite3", *args], | ||
encoding="utf-8", | ||
bufsize=0, | ||
stdin=subprocess.PIPE, | ||
# Note: the banner is printed to stderr, the prompt to stdout. | ||
stdout=subprocess.PIPE, | ||
stderr=subprocess.PIPE, | ||
) | ||
|
||
def expect_success(self, proc): | ||
proc.wait() | ||
if proc.returncode: | ||
self.fail("".join(proc.stderr)) | ||
|
||
def test_interact(self): | ||
with self.start_cli() as proc: | ||
out, err = proc.communicate(timeout=self.TIMEOUT) | ||
self.assertIn(self.MEMORY_DB_MSG, err) | ||
self.assertIn(self.PS1, out) | ||
self.expect_success(proc) | ||
|
||
def test_interact_quit(self): | ||
with self.start_cli() as proc: | ||
out, err = proc.communicate(input=".quit", timeout=self.TIMEOUT) | ||
self.assertIn(self.MEMORY_DB_MSG, err) | ||
self.assertIn(self.PS1, out) | ||
self.expect_success(proc) | ||
|
||
def test_interact_version(self): | ||
with self.start_cli() as proc: | ||
out, err = proc.communicate(input=".version", timeout=self.TIMEOUT) | ||
self.assertIn(self.MEMORY_DB_MSG, err) | ||
self.assertIn(sqlite.sqlite_version, out) | ||
self.expect_success(proc) | ||
|
||
def test_interact_valid_sql(self): | ||
with self.start_cli() as proc: | ||
out, err = proc.communicate(input="select 1;", | ||
timeout=self.TIMEOUT) | ||
self.assertIn(self.MEMORY_DB_MSG, err) | ||
self.assertIn("(1,)", out) | ||
self.expect_success(proc) | ||
|
||
def test_interact_valid_multiline_sql(self): | ||
with self.start_cli() as proc: | ||
out, err = proc.communicate(input="select 1\n;", | ||
erlend-aasland marked this conversation as resolved.
Show resolved
Hide resolved
|
||
timeout=self.TIMEOUT) | ||
self.assertIn(self.MEMORY_DB_MSG, err) | ||
self.assertIn(self.PS2, out) | ||
self.assertIn("(1,)", out) | ||
self.expect_success(proc) | ||
|
||
def test_interact_invalid_sql(self): | ||
with self.start_cli() as proc: | ||
out, err = proc.communicate(input="sel;", timeout=self.TIMEOUT) | ||
self.assertIn(self.MEMORY_DB_MSG, err) | ||
self.assertIn("OperationalError (SQLITE_ERROR)", err) | ||
self.expect_success(proc) | ||
|
||
def test_interact_on_disk_file(self): | ||
self.addCleanup(unlink, TESTFN) | ||
with self.start_cli(TESTFN) as proc: | ||
out, err = proc.communicate(input="create table t(t);", | ||
timeout=self.TIMEOUT) | ||
self.assertIn(TESTFN, err) | ||
self.assertIn(self.PS1, out) | ||
self.expect_success(proc) | ||
with self.start_cli(TESTFN, "select count(t) from t") as proc: | ||
out = proc.stdout.read() | ||
err = proc.stderr.read() | ||
self.assertIn("(0,)", out) | ||
self.expect_success(proc) | ||
|
||
|
||
if __name__ == "__main__": | ||
unittest.main() |
2 changes: 2 additions & 0 deletions
2
Misc/NEWS.d/next/Library/2022-07-20-00-23-58.gh-issue-77617.XGaqSQ.rst
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
Add :mod:`sqlite3` :ref:`command-line interface <sqlite3-cli>`. | ||
Patch by Erlend Aasland. |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.