From 3fcc661ff5bae30447b465c0141c3a3a8859222f Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sat, 21 Jun 2025 22:28:37 +0800 Subject: [PATCH 1/9] Add .tables for sqlite3 command-line interface --- Lib/sqlite3/__main__.py | 17 +++++++++++++++++ Lib/test/test_sqlite3/test_cli.py | 21 +++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index b3746ed757332f..b6080fc5b62a35 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -63,11 +63,28 @@ def runsource(self, source, filename="", symbol="single"): # Remember to update CLI_COMMANDS in _completer.py if source[0] == ".": match source[1:].strip(): + case "tables": + schema_names = tuple(row[1] + for row in self._cur.execute("PRAGMA database_list")) + select_clauses = (f"""SELECT + CASE '{schema}' + WHEN 'main' THEN name + ELSE CONCAT('{schema}.', name) + END + FROM "{schema}".sqlite_master + WHERE type IN ('table', 'view') + AND name NOT LIKE 'sqlite_%'""" + for schema in schema_names + ) + command = " UNION ALL ".join(select_clauses) + " ORDER BY 1" + for row in self._cur.execute(command): + print(row[0]) case "version": print(sqlite3.sqlite_version) case "help": t = theme.syntax print(f"Enter SQL code or one of the below commands, and press enter.\n\n" + f"{t.builtin}.tables{t.reset} List names of tables\n" f"{t.builtin}.version{t.reset} Print underlying SQLite library version\n" f"{t.builtin}.help{t.reset} Print this help message\n" f"{t.builtin}.quit{t.reset} Exit the CLI, equivalent to CTRL-D\n") diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 98aadaa829a969..e2a47085bf63f9 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -125,6 +125,27 @@ def test_interact_version(self): self.assertEqual(out.count(self.PS2), 0) self.assertIn(sqlite3.sqlite_version, out) + def test_interact_tables(self): + out, err = self.run_cli(commands=( + "CREATE TABLE table_ (id INTEGER);", + "CREATE TEMP TABLE temp_table (id INTEGER);", + "CREATE VIEW view_ AS SELECT * FROM table_;", + "CREATE TEMP VIEW temp_view As SELECT * FROM table_;", + "ATTACH ':memory:' AS attach_;", + "CREATE TABLE attach_.table_ (id INTEGER);", + "CREATE VIEW attach_.view_ AS SELECT * FROM table_;", + "ATTACH ':memory:' AS 123;", + "CREATE TABLE \"123\".table_ (id INTEGER);", + "CREATE VIEW \"123\".view_ AS SELECT * FROM table_;", + ".tables", + )) + self.assertIn(self.MEMORY_DB_MSG, err) + self.assertEndsWith(out, self.PS1) + self.assertEqual(out.count(self.PS1), 12) + self.assertEqual(out.count(self.PS2), 0) + self.assertIn("123.table_\n123.view_\nattach_.table_\nattach_.view_\n" + "table_\ntemp.temp_table\ntemp.temp_view\nview_\n", out) + def test_interact_empty_source(self): out, err = self.run_cli(commands=("", " ")) self.assertIn(self.MEMORY_DB_MSG, err) From 61de02cd5b862a2a28a04ff2cdb06ee366fe1b21 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sun, 22 Jun 2025 02:15:26 +0800 Subject: [PATCH 2/9] blurb add --- .../next/Library/2025-06-22-02-15-09.gh-issue-135795.5HPn-r.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-06-22-02-15-09.gh-issue-135795.5HPn-r.rst diff --git a/Misc/NEWS.d/next/Library/2025-06-22-02-15-09.gh-issue-135795.5HPn-r.rst b/Misc/NEWS.d/next/Library/2025-06-22-02-15-09.gh-issue-135795.5HPn-r.rst new file mode 100644 index 00000000000000..9d1b2d439d79ee --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-06-22-02-15-09.gh-issue-135795.5HPn-r.rst @@ -0,0 +1 @@ +Support `.tables` in the sqlite3 command-line interface. From e14da1e4af7ad07b53abf589cc4a8b78e285ac68 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sun, 22 Jun 2025 02:23:46 +0800 Subject: [PATCH 3/9] fix news entry --- .../next/Library/2025-06-22-02-15-09.gh-issue-135795.5HPn-r.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-06-22-02-15-09.gh-issue-135795.5HPn-r.rst b/Misc/NEWS.d/next/Library/2025-06-22-02-15-09.gh-issue-135795.5HPn-r.rst index 9d1b2d439d79ee..3e50d7501622ff 100644 --- a/Misc/NEWS.d/next/Library/2025-06-22-02-15-09.gh-issue-135795.5HPn-r.rst +++ b/Misc/NEWS.d/next/Library/2025-06-22-02-15-09.gh-issue-135795.5HPn-r.rst @@ -1 +1 @@ -Support `.tables` in the sqlite3 command-line interface. +Support ``.tables`` in the :mod:`sqlite3` command-line interface. From f0ace802cd3e60809b1133b677837154b1c096c3 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sun, 22 Jun 2025 02:36:27 +0800 Subject: [PATCH 4/9] fix CONCAT function not found on old version SQLite --- Lib/sqlite3/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index b6080fc5b62a35..bf9c11e84e358f 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -69,7 +69,7 @@ def runsource(self, source, filename="", symbol="single"): select_clauses = (f"""SELECT CASE '{schema}' WHEN 'main' THEN name - ELSE CONCAT('{schema}.', name) + ELSE '{schema}.' || name END FROM "{schema}".sqlite_master WHERE type IN ('table', 'view') From 459bc029bd70e2a58e052391bd5bb28140f255a7 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Mon, 23 Jun 2025 11:17:16 +0800 Subject: [PATCH 5/9] remove unnecessary collection of schema name with tuple(); update help text for consistency; simplify view creation in test case --- Lib/sqlite3/__main__.py | 4 ++-- Lib/test/test_sqlite3/test_cli.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index bf9c11e84e358f..e3c9f53bfc3fa9 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -64,7 +64,7 @@ def runsource(self, source, filename="", symbol="single"): if source[0] == ".": match source[1:].strip(): case "tables": - schema_names = tuple(row[1] + schema_names = (row[1] for row in self._cur.execute("PRAGMA database_list")) select_clauses = (f"""SELECT CASE '{schema}' @@ -84,7 +84,7 @@ def runsource(self, source, filename="", symbol="single"): case "help": t = theme.syntax print(f"Enter SQL code or one of the below commands, and press enter.\n\n" - f"{t.builtin}.tables{t.reset} List names of tables\n" + f"{t.builtin}.tables{t.reset} Print names of tables\n" f"{t.builtin}.version{t.reset} Print underlying SQLite library version\n" f"{t.builtin}.help{t.reset} Print this help message\n" f"{t.builtin}.quit{t.reset} Exit the CLI, equivalent to CTRL-D\n") diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index e2a47085bf63f9..8cf60cdabbe8a9 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -129,14 +129,14 @@ def test_interact_tables(self): out, err = self.run_cli(commands=( "CREATE TABLE table_ (id INTEGER);", "CREATE TEMP TABLE temp_table (id INTEGER);", - "CREATE VIEW view_ AS SELECT * FROM table_;", - "CREATE TEMP VIEW temp_view As SELECT * FROM table_;", + "CREATE VIEW view_ AS SELECT 1;", + "CREATE TEMP VIEW temp_view AS SELECT 1;", "ATTACH ':memory:' AS attach_;", "CREATE TABLE attach_.table_ (id INTEGER);", - "CREATE VIEW attach_.view_ AS SELECT * FROM table_;", + "CREATE VIEW attach_.view_ AS SELECT 1;", "ATTACH ':memory:' AS 123;", "CREATE TABLE \"123\".table_ (id INTEGER);", - "CREATE VIEW \"123\".view_ AS SELECT * FROM table_;", + "CREATE VIEW \"123\".view_ AS SELECT 1;", ".tables", )) self.assertIn(self.MEMORY_DB_MSG, err) From 288ac5451feab511f014b5f1779329260b25ba28 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Mon, 23 Jun 2025 21:39:25 +0800 Subject: [PATCH 6/9] Fix false filtering out table names with "sqlite" prefix instead of "sqlite_" --- Lib/sqlite3/__main__.py | 2 +- Lib/test/test_sqlite3/test_cli.py | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index e3c9f53bfc3fa9..0b71ef9268a96d 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -73,7 +73,7 @@ def runsource(self, source, filename="", symbol="single"): END FROM "{schema}".sqlite_master WHERE type IN ('table', 'view') - AND name NOT LIKE 'sqlite_%'""" + AND name NOT LIKE 'sqlite_%' ESCAPE '_'""" for schema in schema_names ) command = " UNION ALL ".join(select_clauses) + " ORDER BY 1" diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 8cf60cdabbe8a9..91f4f660198187 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -128,6 +128,7 @@ def test_interact_version(self): def test_interact_tables(self): out, err = self.run_cli(commands=( "CREATE TABLE table_ (id INTEGER);", + "CREATE TABLE sqlitee (id INTEGER);", "CREATE TEMP TABLE temp_table (id INTEGER);", "CREATE VIEW view_ AS SELECT 1;", "CREATE TEMP VIEW temp_view AS SELECT 1;", @@ -141,10 +142,18 @@ def test_interact_tables(self): )) self.assertIn(self.MEMORY_DB_MSG, err) self.assertEndsWith(out, self.PS1) - self.assertEqual(out.count(self.PS1), 12) + self.assertEqual(out.count(self.PS1), 13) self.assertEqual(out.count(self.PS2), 0) - self.assertIn("123.table_\n123.view_\nattach_.table_\nattach_.view_\n" - "table_\ntemp.temp_table\ntemp.temp_view\nview_\n", out) + tables = ("123.table_", + "123.view_", + "attach_.table_", + "attach_.view_", + "sqlitee", + "table_", + "temp.temp_table", + "temp.temp_view", + "view_") + self.assertIn("\n".join(tables), out) def test_interact_empty_source(self): out, err = self.run_cli(commands=("", " ")) From 2fbc54a395ff51bf38367cd780779438ebf7c6e3 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 9 Oct 2025 23:55:56 +0800 Subject: [PATCH 7/9] add .tables to CLI_COMMANDS in _completer.py --- Lib/sqlite3/_completer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/sqlite3/_completer.py b/Lib/sqlite3/_completer.py index ba580f968bf92d..b0c1eda084afec 100644 --- a/Lib/sqlite3/_completer.py +++ b/Lib/sqlite3/_completer.py @@ -6,7 +6,7 @@ except ImportError: SQLITE_KEYWORDS = () -CLI_COMMANDS = ('.quit', '.help', '.version') +CLI_COMMANDS = ('.quit', '.help', '.version', '.tables') _completion_matches = [] From ade6c0fe187317e9748104a4640ab8870b1f9ac1 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 10 Oct 2025 00:11:56 +0800 Subject: [PATCH 8/9] fix misuse of ESCAPE keyword --- Lib/sqlite3/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 0b71ef9268a96d..dfd5d29aecd845 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -73,7 +73,7 @@ def runsource(self, source, filename="", symbol="single"): END FROM "{schema}".sqlite_master WHERE type IN ('table', 'view') - AND name NOT LIKE 'sqlite_%' ESCAPE '_'""" + AND name NOT LIKE 'sqlite^_%' ESCAPE '^'""" for schema in schema_names ) command = " UNION ALL ".join(select_clauses) + " ORDER BY 1" From 6deb0eaf71bff538959e347c65db72dacbddc63d Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sun, 8 Feb 2026 12:17:56 +0800 Subject: [PATCH 9/9] Add .indexes --- Lib/sqlite3/__main__.py | 15 +++++++++----- Lib/sqlite3/_completer.py | 2 +- Lib/test/test_sqlite3/test_cli.py | 34 ++++++++++++++++++++++++++----- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index dfd5d29aecd845..d29a4de0cca16a 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -62,18 +62,21 @@ def runsource(self, source, filename="", symbol="single"): return False # Remember to update CLI_COMMANDS in _completer.py if source[0] == ".": - match source[1:].strip(): - case "tables": + cmd_name = source[1:].strip() + match cmd_name: + case "tables" | "indices" | "indexes": schema_names = (row[1] for row in self._cur.execute("PRAGMA database_list")) + where_clause = ("""WHERE type IN ('table', 'view') + AND name NOT LIKE 'sqlite^_%' ESCAPE '^'""" + if cmd_name == "tables" else "WHERE type='index'" + ) select_clauses = (f"""SELECT CASE '{schema}' WHEN 'main' THEN name ELSE '{schema}.' || name END - FROM "{schema}".sqlite_master - WHERE type IN ('table', 'view') - AND name NOT LIKE 'sqlite^_%' ESCAPE '^'""" + FROM "{schema}".sqlite_master {where_clause}""" for schema in schema_names ) command = " UNION ALL ".join(select_clauses) + " ORDER BY 1" @@ -85,6 +88,8 @@ def runsource(self, source, filename="", symbol="single"): t = theme.syntax print(f"Enter SQL code or one of the below commands, and press enter.\n\n" f"{t.builtin}.tables{t.reset} Print names of tables\n" + f"{t.builtin}.indexes{t.reset} Print names of indexes\n" + f"{t.builtin}.indices{t.reset} Print names of indices\n" f"{t.builtin}.version{t.reset} Print underlying SQLite library version\n" f"{t.builtin}.help{t.reset} Print this help message\n" f"{t.builtin}.quit{t.reset} Exit the CLI, equivalent to CTRL-D\n") diff --git a/Lib/sqlite3/_completer.py b/Lib/sqlite3/_completer.py index b0c1eda084afec..74caa055b8d1ac 100644 --- a/Lib/sqlite3/_completer.py +++ b/Lib/sqlite3/_completer.py @@ -6,7 +6,7 @@ except ImportError: SQLITE_KEYWORDS = () -CLI_COMMANDS = ('.quit', '.help', '.version', '.tables') +CLI_COMMANDS = ('.quit', '.help', '.version', '.tables', '.indices', '.indexes') _completion_matches = [] diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 91f4f660198187..82d1a10d144232 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -129,9 +129,9 @@ def test_interact_tables(self): out, err = self.run_cli(commands=( "CREATE TABLE table_ (id INTEGER);", "CREATE TABLE sqlitee (id INTEGER);", - "CREATE TEMP TABLE temp_table (id INTEGER);", + "CREATE TEMP TABLE table_ (id INTEGER);", "CREATE VIEW view_ AS SELECT 1;", - "CREATE TEMP VIEW temp_view AS SELECT 1;", + "CREATE TEMP VIEW view_ AS SELECT 1;", "ATTACH ':memory:' AS attach_;", "CREATE TABLE attach_.table_ (id INTEGER);", "CREATE VIEW attach_.view_ AS SELECT 1;", @@ -150,10 +150,34 @@ def test_interact_tables(self): "attach_.view_", "sqlitee", "table_", - "temp.temp_table", - "temp.temp_view", + "temp.table_", + "temp.view_", "view_") - self.assertIn("\n".join(tables), out) + self.assertEqual("\n".join(tables), out.replace(self.PS1, "").strip()) + + def test_interact_indexes(self): + out, err = self.run_cli(commands=( + "CREATE TABLE table_ (id INTEGER);", + "CREATE INDEX idx_table_ ON table_ (id);", + "CREATE TEMP TABLE temp_table (id INTEGER);", + "CREATE INDEX temp.idx_temp_table_ ON temp_table (id);", + "ATTACH ':memory:' AS attach_;", + "CREATE TABLE attach_.attach_table (id INTEGER);", + "CREATE INDEX attach_.idx_attach_table ON attach_table (id);", + ".indexes", + ".indices", + )) + self.assertIn(self.MEMORY_DB_MSG, err) + self.assertEndsWith(out, self.PS1) + self.assertEqual(out.count(self.PS1), 10) + self.assertEqual(out.count(self.PS2), 0) + expected = ("attach_.idx_attach_table", + "idx_table_", + "temp.idx_temp_table_", + "attach_.idx_attach_table", + "idx_table_", + "temp.idx_temp_table_") + self.assertEqual("\n".join(expected), out.replace(self.PS1, "").strip()) def test_interact_empty_source(self): out, err = self.run_cli(commands=("", " "))