8000 gh-133390: Support SQL keyword completion for sqlite3 CLI by tanloong · Pull Request #133393 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

gh-133390: Support SQL keyword completion for sqlite3 CLI #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

Merged
merged 54 commits into from
Jun 6, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
54 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
3865131
Don't decrease reference for `PyModule_Add()` and `PyTuple_SetItem()`
tanloong May 25, 2025
8d4f659
Merge branch 'main' into sqlite3-cli-completion
encukou May 29, 2025
ccd98a5
Add @force_not_colorized_test_class
tanloong May 31, 2025
d681425
Merge branch 'main' into sqlite3-cli-completion
encukou Jun 5, 2025
ffd0f02
Add two '\b\b'; Skip tests on FreeBSD
tanloong Jun 5, 2025
6188a6d
Amend skipping reason
tanloong Jun 5, 2025
370dd8b
Remove comment 'set the keyword tuple'
tanloong Jun 5, 2025
16b1674
Disable keyword completion for SQLite<3.24.0
tanloong Jun 5, 2025
ea108ba
Don't disable the whole completion in case there will be more completion
tanloong Jun 5, 2025
13b527e
Use compile-time check
tanloong Jun 5, 2025
fafd1bb
Correct #if usage
tanloong Jun 5, 2025
140818c
Wrap add_keyword_tuple() definition and its call in #if/#endif
tanloong Jun 5, 2025
fd6c89e
Suggestions to python/cpython#133393
erlend-aasland Jun 6, 2025
588fb6a
Merge pull request #3 from erlend-aasland/suggestion
tanloong Jun 6, 2025
5623f16
Merge branch 'main' into sqlite3-cli-completion
erlend-aasland Jun 6, 2025
88c8d59
Update Doc/whatsnew/3.15.rst
erlend-aasland Jun 6, 2025
b3a2b88
Merge branch 'main' into sqlite3-cli-completion
erlend-aasland Jun 6, 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
Prev Previous commit
Next Next commit
Suggestions to #133393
  • Loading branch information
erlend-aasland committed Jun 6, 2025
commit fd6c89ef68f4384d720abe33c256fcc51ed49e1f
4 changes: 2 additions & 2 deletions Lib/sqlite3/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from textwrap import dedent
from _colorize import get_theme, theme_no_color

from ._completer import enable_completer
from ._completer import completer


def execute(c, sql, suppress_errors=True, theme=theme_no_color):
Expand Down Expand Up @@ -138,7 +138,7 @@ def main(*args):
execute(con, args.sql, suppress_errors=False, theme=theme)
else:
# No SQL provided; start the REPL.
with enable_completer():
with completer():
console = SqliteInteractiveConsole(con, use_color=True)
console.interact(banner, exitmsg="")
finally:
Expand Down
12 changes: 7 additions & 5 deletions Lib/sqlite3/_completer.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from contextlib import contextmanager

try:
from _sqlite3 import SQLITE_KEYWORDS
except ImportError:
SQLITE_KEYWORDS = ()

_completion_matches = []


def _complete(text, state):
try:
from _sqlite3 import SQLITE_KEYWORDS
except ImportError:
SQLITE_KEYWORDS = ()
global _completion_matches

if state == 0:
text_upper = text.upper()
_completion_matches = [c for c in SQLITE_KEYWORDS if c.startswith(text_upper)]
Expand All @@ -19,7 +21,7 @@ def _complete(text, state):


@contextmanager
def enable_completer():
def completer():
try:
import readline
except ImportError:
Expand Down
84 changes: 50 additions & 34 deletions Lib/test/test_sqlite3/test_cli.py
< 6DB6 td id="diff-def0649c1914572c2c375318f834f14cad70a55d5e68403ca43079db3db980f0R234" data-line-number="234" class="blob-num blob-num-addition js-linkable-line-number js-blob-rnum">
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
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
Expand Down Expand Up @@ -209,76 +208,93 @@ def test_color(self):

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

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 ?

Copy link
Contributor

Choose a reason for hiding this comment

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

Can you explain your remark. Do you want changes?


@classmethod
def setUpClass(cls):
_sqlite3 = import_module("_sqlite3")
if not hasattr(_sqlite3, "SQLITE_KEYWORDS"):
raise unittest.SkipTest("unable to determine SQLite keywords")

readline = import_module("readline")
if readline.backend == "editline":
raise unittest.SkipTest("libedit readline is not supported")

def write_input(self, input, env=None):
def write_input(self, input_, env=None):
script = textwrap.dedent("""
import readline
from sqlite3.__main__ import main

readline.parse_and_bind("set colored-completion-prefix off")
from sqlite3.__main__ import main; main()
main()
""")
return run_pty(script, input, env)
return run_pty(script, input_, env)

def test_keyword_completion(self):
def test_complete_sql_keywords(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)
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)
# 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)

@unittest.skipIf(sys.platform.startswith("freebsd"),
"Two actual tabs are inserted when there are no matching"
" completions in the pseudo-terminal opened by run_pty()"
" on FreeBSD")
def test_nothing_to_complete(self):
input = b"xyzzy\t\t\b\b\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)
def test_complete_no_match(self):
input_ = b"xyzzy\t\t\b\b\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"})
lines = output.decode().splitlines()
indices = (
i for i, line in enumerate(lines, 1)
if line.startswith(f"{self.PS1}xyzzy")
)
line_num = next(indices, -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))
# Completions occupy lines, assert no extra lines when there is nothing
# to complete.
self.assertEqual(line_num, len(lines))

def test_complete_no_input(self):
from _sqlite3 import SQLITE_KEYWORDS

def test_completion_for_nothing(self):
script = textwrap.dedent("""
import readline
from sqlite3.__main__ import main

# Configure readline to ...:
# - hide control sequences surrounding each candidate
# - hide "Display all xxx possibilities? (y or n)"
# - hide "--More--"
# - show candidates one per line
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()

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)]
input_ = b"\t\t.quit\n"
output = run_pty(script, input_, env={"NO_COLOR": "1"})
lines = output.decode().splitlines()
indices = [
i for i, line in enumerate(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]))
start, end = indices
candidates = [l.strip() for l in lines[start+1:end]]
self.assertEqual(candidates, sorted(SQLITE_KEYWORDS))


Expand Down
16 changes: 7 additions & 9 deletions Modules/_sqlite/module.c
Original file line number Diff line number Diff line change
Expand Up @@ -405,14 +405,14 @@ pysqlite_error_name(int rc)
return NULL;
}

#if SQLITE_VERSION_NUMBER < 3024000
static int
add_keyword_tuple(PyObject *module)
{
#if SQLITE_VERSION_NUMBER >= 3024000
int count = sqlite3_keyword_count();
PyObject *keywords = PyTuple_New(count);
if (keywords == NULL) {
goto error;
return -1;
}
for (int i = 0; i < count; i++) {
const char *keyword;
Expand All @@ -427,17 +427,17 @@ add_keyword_tuple(PyObject *module)
if (!kwd) {
goto error;
}
if (PyTuple_SetItem(keywords, i, kwd) < 0) {
goto error;
}
PyTuple_SET_ITEM(keywords, i, kwd);
}
return PyModule_Add(module, "SQLITE_KEYWORDS", keywords);

error:
Py_XDECREF(keywords);
Py_DECREF(keywords);
return -1;
}
#else
return 0;
#endif
}

static int
add_integer_constants(PyObject *module) {
Expand Down Expand Up @@ -737,11 +737,9 @@ module_exec(PyObject *module)
goto error;
}

#if SQLITE_VERSION_NUMBER < 3024000
if (add_keyword_tuple(module) < 0) {
goto error;
}
#endif

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