Skip to content

gh-133390: Support basic completion for sqlite3 command-line interface #133393

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

Open
wants to merge 37 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
1b96be3
Support basic completion for sqlite3 command-line interface
tanloong May 4, 2025
5e50871
Add news entry
tanloong May 4, 2025
c1941cb
Move completion code to separate module
tanloong May 4, 2025
47daca5
Update Lib/sqlite3/_completer.py
tanloong May 4, 2025
c54c2f6
Update Doc/whatsnew/3.14.rst
tanloong May 4, 2025
8fff491
Add test
tanloong May 5, 2025
a766805
Move keyword list to module level
tanloong May 5, 2025
da55014
Remove whatsnew entry from 3.14
tanloong May 5, 2025
ca587e0
Avoid regeneration of candidates. Store them when state is 0 and returns
tanloong May 7, 2025
311b4f3
Add whatsnew entry to 3.15
tanloong May 7, 2025
70f46e9
Address Bénédikt's review
tanloong May 10, 2025
9d03730
Remove color handling of output; If CI fails might need to add back
tanloong May 10, 2025
bfcff38
Fix `run_pty()` doesn't return and test hangs
tanloong May 10, 2025
805d997
Revert "Remove color handling of output; If CI fails might need to ad…
tanloong May 10, 2025
276b4a7
Turn off colored-completion-prefix for readline
tanloong May 10, 2025
09eeac8
No need to pass "NO_COLOR" to `run_pty()`
tanloong May 10, 2025
fc57d71
Flip name
tanloong May 10, 2025
c508069
Triggering completion on Ubuntu requires 2 tabs
tanloong May 10, 2025
231b9e7
Move KEYWORDS to C
tanloong May 10, 2025
121b069
Improve style of C code
tanloong May 10, 2025
90a86cf
Improve tests
tanloong May 11, 2025
5170733
Address Bénédikt's review
tanloong May 16, 2025
b40982a
Revert "Improve style of C code"
tanloong May 16, 2025
226ea9f
Revert "Move KEYWORDS to C"
tanloong May 16, 2025
4eebbd9
Read keyword names dynamically
encukou May 16, 2025
3f9b2c1
Check candidates against KEYWORDS
tanloong May 16, 2025
0410fa2
Use slice to get candidates
tanloong May 16, 2025
bd0b9ce
Address Bénédikt's review
tanloong May 16, 2025
35a17e7
Make candidates tuple
tanloong May 16, 2025
3dd16b3
Revert "Revert "Move KEYWORDS to C""
tanloong May 16, 2025
f3ea951
Revert "Revert "Improve style of C code""
tanloong May 16, 2025
a493ad3
Merge pull request #2 from encukou/sqlite3-cli-completion
tanloong May 16, 2025
34cfc78
Fix 'KEYWORDS' not found
tanloong May 16, 2025
477b48b
Sort keywords before checking the equality
tanloong May 16, 2025
68bb4f3
Fix comparing between tuple and list
tanloong May 16, 2025
4c3b122
Fix comparing between tuple and list
tanloong May 16, 2025
4f1221e
Rename 'test_completion_order' to 'test_completion_for_nothing'
tanloong May 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@ New modules
Improved modules
================

sqlite3
-------

* Support keyword completion in the :mod:`sqlite3` command-line interface.
(Contributed by Long Tan in :gh:`133393`.)


ssl
---

Expand Down
11 changes: 5 additions & 6 deletions Lib/sqlite3/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from textwrap import dedent
from _colorize import get_theme, theme_no_color

from ._completer import enable_completer


def execute(c, sql, suppress_errors=True, theme=theme_no_color):
"""Helper that wraps execution of SQL code.
Expand Down Expand Up @@ -136,12 +138,9 @@ def main(*args):
execute(con, args.sql, suppress_errors=False, theme=theme)
else:
# No SQL provided; start the REPL.
console = SqliteInteractiveConsole(con, use_color=True)
try:
import readline # noqa: F401
except ImportError:
pass
console.interact(banner, exitmsg="")
with enable_completer():
console = SqliteInteractiveConsole(con, use_color=True)
console.interact(banner, exitmsg="")
finally:
con.close()

Expand Down
37 changes: 37 additions & 0 deletions Lib/sqlite3/_completer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from _sqlite3 import SQLITE_KEYWORDS
from contextlib import contextmanager

_completion_matches = []


def _complete(text, state):
global _completion_matches
if state == 0:
text_upper = text.upper()
_completion_matches = [c for c in SQLITE_KEYWORDS if c.startswith(text_upper)]
try:
return _completion_matches[state] + " "
except IndexError:
return None


@contextmanager
def enable_completer():
try:
import readline
except ImportError:
yield
return

old_completer = readline.get_completer()
try:
readline.set_completer(_complete)
if readline.backend == "editline":
# libedit uses "^I" instead of "tab"
command_string = "bind ^I rl_complete"
else:
command_string = "tab: complete"
readline.parse_and_bind(command_string)
yield
finally:
readline.set_completer(old_completer)
76 changes: 76 additions & 0 deletions Lib/test/test_sqlite3/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
"""sqlite3 CLI tests."""
import sqlite3
import textwrap
import unittest

from _sqlite3 import SQLITE_KEYWORDS
from sqlite3.__main__ import main as cli
from test.support.import_helper import import_module
from test.support.os_helper import TESTFN, unlink
from test.support.pty_helper import run_pty
from test.support import (
captured_stdout,
captured_stderr,
captured_stdin,
force_not_colorized_test_class,
requires_subprocess,
)


Expand Down Expand Up @@ -200,5 +205,76 @@ def test_color(self):
self.assertIn('\x1b[1;35mOperationalError (SQLITE_ERROR)\x1b[0m: '
'\x1b[35mnear "sel": syntax error\x1b[0m', err)


@requires_subprocess()
class CompletionTest(unittest.TestCase):
PS1 = "sqlite> "
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is no longer freely customizable by users via sys.ps1 ?


@classmethod
def setUpClass(cls):
readline = import_module("readline")
if readline.backend == "editline":
raise unittest.SkipTest("libedit readline is not supported")

def write_input(self, input, env=None):
script = textwrap.dedent("""
import readline
readline.parse_and_bind("set colored-completion-prefix off")
from sqlite3.__main__ import main; main()
""")
return run_pty(script, input, env)

def test_keyword_completion(self):
# List candidates starting with 'S', there should be multiple matches.
input = b"S\t\tEL\t 1;\n.quit\n"
output = self.write_input(input)
self.assertIn(b"SELECT", output)
self.assertIn(b"SET", output)
self.assertIn(b"SAVEPOINT", output)
self.assertIn(b"(1,)", output)

# Keywords are completed in upper case for even lower case user input
input = b"sel\t\t 1;\n.quit\n"
output = self.write_input(input)
self.assertIn(b"SELECT", output)
self.assertIn(b"(1,)", output)

def test_nothing_to_complete(self):
input = b"xyzzy\t\t\b\b\b\b\b.quit\n"
# set NO_COLOR to disable coloring for self.PS1
output = self.write_input(input, env={"NO_COLOR": "1"})
output_lines = output.decode().splitlines()
line_num = next((i for i, line in enumerate(output_lines, 1)
if line.startswith(f"{self.PS1}xyzzy")), -1)
self.assertNotEqual(line_num, -1)
# completions occupy lines, assert no extra lines when there is nothing
# to complete
self.assertEqual(line_num, len(output_lines))

def test_completion_for_nothing(self):
script = textwrap.dedent("""
import readline
readline.parse_and_bind("set colored-completion-prefix off")
# hide control sequences surrounding each candidate
readline.parse_and_bind("set colored-stats off")
# hide "Display all xxx possibilities? (y or n)"
readline.parse_and_bind("set completion-query-items 0")
# hide "--More--"
readline.parse_and_bind("set page-completions off")
# show candidates one per line
readline.parse_and_bind("set completion-display-width 0")
from sqlite3.__main__ import main; main()
""")
input = b"\t\t.quit\n"
output = run_pty(script, input, env={"NO_COLOR": "1"})
output_lines = output.decode().splitlines()
indices = [i for i, line in enumerate(output_lines)
if line.startswith(self.PS1)]
self.assertEqual(len(indices), 2)
start, end = indices[0] + 1, indices[1]
candidates = list(map(str.strip, output_lines[start:end]))
self.assertEqual(candidates, sorted(SQLITE_KEYWORDS))


if __name__ == "__main__":
unittest.main()
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -1864,6 +1864,7 @@ Neil Tallim
Geoff Talvola
Anish Tambe
Musashi Tamura
Long Tan
William Tanksley
Christian Tanzer
Steven Taschuk
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support keyword completion in the :mod:`sqlite3` command-line interface.
42 changes: 42 additions & 0 deletions Modules/_sqlite/module.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#include "microprotocols.h"
#include "row.h"
#include "blob.h"
#include "util.h"

#if SQLITE_VERSION_NUMBER < 3015002
#error "SQLite 3.15.2 or higher required"
Expand Down Expand Up @@ -404,6 +405,42 @@ pysqlite_error_name(int rc)
return NULL;
}

static int
add_keyword_tuple(PyObject *module)
{
int count = sqlite3_keyword_count();
PyObject *keywords = PyTuple_New(count);
if (keywords == NULL) {
goto error;
}
for (int i = 0; i < count; i++) {
const char *keyword;
int size;
int result = sqlite3_keyword_name(i, &keyword, &size);
if (result != SQLITE_OK) {
pysqlite_state *state = pysqlite_get_state(module);
set_error_from_code(state, result);
goto error;
}
PyObject *kwd = PyUnicode_FromStringAndSize(keyword, size);
if (!kwd) {
goto error;
}
if (PyTuple_SetItem(keywords, i, kwd) < 0) {
Py_DECREF(kwd);
goto error;
}
}
if (PyModule_Add(module, "SQLITE_KEYWORDS", keywords) < 0) {
goto error;
}
return 0;

error:
Py_XDECREF(keywords);
return -1;
}

static int
add_integer_constants(PyObject *module) {
#define ADD_INT(ival) \
Expand Down Expand Up @@ -702,6 +739,11 @@ module_exec(PyObject *module)
goto error;
}

/* Set the keyword tuple */
if (add_keyword_tuple(module) < 0) {
goto error;
}

if (PyModule_AddStringConstant(module, "sqlite_version", sqlite3_libversion())) {
goto error;
}
Expand Down
Loading