Thanks to visit codestin.com
Credit goes to github.com

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 all commits
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
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ shelve
(Contributed by Andrea Oliveri in :gh:`134004`.)


sqlite3
-------

* Support SQL 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 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 completer():
console = SqliteInteractiveConsole(con, use_color=True)
console.interact(banner, exitmsg="")
finally:
con.close()

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

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

_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 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)
98 changes: 98 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 sys
import textwrap
import unittest

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,98 @@ def test_color(self):
self.assertIn('\x1b[1;35mOperationalError (SQLITE_ERROR)\x1b[0m: '
'\x1b[35mnear "sel": syntax error\x1b[0m', err)


@requires_subprocess()
@force_not_colorized_test_class
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):
script = textwrap.dedent("""
import readline
from sqlite3.__main__ import main

readline.parse_and_bind("set colored-completion-prefix off")
main()
""")
return run_pty(script, input_, env)

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_)
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)

@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_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(lines))

def test_complete_no_input(self):
from _sqlite3 import SQLITE_KEYWORDS

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")
readline.parse_and_bind("set colored-stats off")
readline.parse_and_bind("set completion-query-items 0")
readline.parse_and_bind("set page-completions off")
readline.parse_and_bind("set completion-display-width 0")

main()
""")
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
candidates = [l.strip() for l in lines[start+1: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 @@ -1868,6 +1868,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.
39 changes: 39 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,40 @@ pysqlite_error_name(int rc)
return NULL;
}

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) {
return -1;
}
for (int i = 0; i < count; i++) {
const char *keyword;
int size;
int result = sqlite3_keyword_name(i, &keyword, &size);
Copy link
Contributor

Choose a reason for hiding this comment

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

For the record:
We could just assert that the result is SQLITE_OK. This is just an out-of-bounds check, and we know that we are within bounds here. OTOH, we don't know how the SQLite internals may change, so let's keep it like it is.

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;
}
PyTuple_SET_ITEM(keywords, i, kwd);
}
return PyModule_Add(module, "SQLITE_KEYWORDS", keywords);

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

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

if (add_keyword_tuple(module) < 0) {
goto error;
}

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