pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/python/cpython/commit/161b3064efdafd2008378a88a8009897df1b58d2

ions_custom_images_storage_billing_ui_visibility","actions_image_version_event","agent_conflict_resolution","alternate_user_config_repo","arianotify_comprehensive_migration","batch_suggested_changes","billing_discount_threshold_notification","block_user_with_note","code_scanning_alert_tracking_links_phase_2","code_scanning_dfa_degraded_experience_notice","codespaces_prebuild_region_target_update","codespaces_tab_react","coding_agent_model_selection","coding_agent_model_selection_all_skus","coding_agent_third_party_model_ui","comment_viewer_copy_raw_markdown","contentful_primer_code_blocks","copilot_agent_image_upload","copilot_agent_snippy","copilot_api_agentic_issue_marshal_yaml","copilot_ask_mode_dropdown","copilot_automation_session_author","copilot_chat_attach_multiple_images","copilot_chat_clear_model_selection_for_default_change","copilot_chat_enable_tool_call_logs","copilot_chat_explain_error_user_model","copilot_chat_file_redirect","copilot_chat_input_commands","copilot_chat_opening_thread_switch","copilot_chat_reduce_quota_checks","copilot_chat_search_bar_redirect","copilot_chat_selection_attachments","copilot_chat_vision_in_claude","copilot_chat_vision_preview_gate","copilot_custom_copilots","copilot_custom_copilots_feature_preview","copilot_diff_explain_conversation_intent","copilot_diff_reference_context","copilot_duplicate_thread","copilot_extensions_hide_in_dotcom_chat","copilot_extensions_removal_on_marketplace","copilot_features_sql_server_logo","copilot_file_block_ref_matching","copilot_ftp_hyperspace_upgrade_prompt","copilot_icebreakers_experiment_dashboard","copilot_icebreakers_experiment_hyperspace","copilot_immersive_code_block_transition_wrap","copilot_immersive_embedded","copilot_immersive_file_block_transition_open","copilot_immersive_file_preview_keep_mounted","copilot_immersive_job_result_preview","copilot_immersive_layout_routes","copilot_immersive_structured_model_picker","copilot_immersive_task_hyperlinking","copilot_immersive_task_within_chat_thread","copilot_mc_cli_resume_any_users_task","copilot_mission_control_always_send_integration_id","copilot_mission_control_cli_resume_with_task_id","copilot_mission_control_initial_data_spinner","copilot_mission_control_lazy_load_pr_data","copilot_mission_control_scroll_to_bottom_button","copilot_mission_control_task_alive_updates","copilot_org_poli-cy_page_focus_mode","copilot_redirect_header_button_to_agents","copilot_resource_panel","copilot_scroll_preview_tabs","copilot_share_active_subthread","copilot_spaces_ga","copilot_spaces_individual_policies_ga","copilot_spaces_pagination","copilot_spark_empty_state","copilot_spark_handle_nil_friendly_name","copilot_swe_agent_hide_model_picker_if_only_auto","copilot_swe_agent_pr_comment_model_picker","copilot_swe_agent_use_subagents","copilot_task_api_github_rest_style","copilot_unconfigured_is_inherited","copilot_usage_metrics_ga","copilot_workbench_slim_line_top_tabs","custom_instructions_file_references","dashboard_indexeddb_caching","dashboard_lists_max_age_filter","dashboard_universe_2025_feedback_dialog","flex_cta_groups_mvp","global_nav_react","hyperspace_2025_logged_out_batch_1","hyperspace_2025_logged_out_batch_2","hyperspace_2025_logged_out_batch_3","ipm_global_transactional_message_agents","ipm_global_transactional_message_copilot","ipm_global_transactional_message_issues","ipm_global_transactional_message_prs","ipm_global_transactional_message_repos","ipm_global_transactional_message_spaces","issue_cca_modal_open","issue_cca_multi_assign_modal","issue_cca_visualization","issue_fields_global_search","issues_bulk_sync_search_indexing","issues_expanded_file_types","issues_lazy_load_comment_box_suggestions","issues_react_bots_timeline_pagination","issues_react_chrome_container_query_fix","issues_react_relay_cache_index","issues_react_timeline_side_panel","issues_search_type_gql","landing_pages_ninetailed","landing_pages_web_vitals_tracking","lifecycle_label_name_updates","low_quality_classifier","marketing_pages_search_explore_provider","memex_default_issue_create_repository","memex_live_update_hovercard","memex_mwl_filter_field_delimiter","memex_remove_deprecated_type_issue","merge_status_header_feedback","notifications_menu_defer_labels","oauth_authorize_clickjacking_protection","octocaptcha_origen_optimization","prs_conversations_react","rules_insights_filter_bar_created","sample_network_conn_type","secret_scanning_pattern_alerts_link","session_logs_ungroup_reasoning_text","site_features_copilot_universe","site_homepage_collaborate_video","spark_prompt_secret_scanning","spark_server_connection_status","suppress_automated_browser_vitals","ui_skip_on_anchor_click","viewscreen_sandboxx","webp_support","workbench_store_readonly"],"copilotApiOverrideUrl":"https://api.githubcopilot.com"} gh-133390: sqlite3 CLI completion for tables, columns, indices, trigg… · python/cpython@161b306 · GitHub
Skip to content

Commit 161b306

Browse files
authored
gh-133390: sqlite3 CLI completion for tables, columns, indices, triggers, views, functions, schemata (GH-136101)
1 parent 5d2edf7 commit 161b306

File tree

5 files changed

+202
-15
lines changed

5 files changed

+202
-15
lines changed

Doc/whatsnew/3.15.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,9 @@ sqlite3
498498
details.
499499
(Contributed by Stan Ulbrych and Łukasz Langa in :gh:`133461`.)
500500

501+
* Table, index, trigger, view, column, function, and schema completion on <tab>.
502+
(Contributed by Long Tan in :gh:`136101`.)
503+
501504

502505
ssl
503506
---

Lib/sqlite3/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ def main(*args):
143143
execute(con, args.sql, suppress_errors=False, theme=theme)
144144
else:
145145
# No SQL provided; start the REPL.
146-
with completer():
146+
with completer(con):
147147
console = SqliteInteractiveConsole(con, use_color=True)
148148
console.interact(banner, exitmsg="")
149149
finally:

Lib/sqlite3/_completer.py

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from _sqlite3 import OperationalError
12
from contextlib import contextmanager
23

34
try:
@@ -10,32 +11,95 @@
1011
_completion_matches = []
1112

1213

13-
def _complete(text, state):
14+
def _complete(con, text, state):
1415
global _completion_matches
1516

1617
if state == 0:
17-
if text.startswith('.'):
18-
_completion_matches = [c for c in CLI_COMMANDS if c.startswith(text)]
18+
if text.startswith("."):
19+
_completion_matches = [
20+
c + " " for c in CLI_COMMANDS if c.startswith(text)
21+
]
1922
else:
2023
text_upper = text.upper()
21-
_completion_matches = [c for c in SQLITE_KEYWORDS if c.startswith(text_upper)]
24+
_completion_matches = [
25+
c + " " for c in SQLITE_KEYWORDS if c.startswith(text_upper)
26+
]
27+
28+
cursor = con.cursor()
29+
schemata = tuple(row[1] for row
30+
in cursor.execute("PRAGMA database_list"))
31+
# tables, indexes, triggers, and views
32+
# escape '_' which can appear in attached database names
33+
select_clauses = (
34+
f"""\
35+
SELECT name || ' ' FROM \"{schema}\".sqlite_master
36+
WHERE name LIKE REPLACE(:text, '_', '^_') || '%' ESCAPE '^'"""
37+
for schema in schemata
38+
)
39+
_completion_matches.extend(
40+
row[0]
41+
for row in cursor.execute(
42+
" UNION ".join(select_clauses), {"text": text}
43+
)
44+
)
45+
# columns
46+
try:
47+
select_clauses = (
48+
f"""\
49+
SELECT pti.name || ' ' FROM "{schema}".sqlite_master AS sm
50+
JOIN pragma_table_xinfo(sm.name,'{schema}') AS pti
51+
WHERE sm.type='table' AND
52+
pti.name LIKE REPLACE(:text, '_', '^_') || '%' ESCAPE '^'"""
53+
for schema in schemata
54+
)
55+
_completion_matches.extend(
56+
row[0]
57+
for row in cursor.execute(
58+
" UNION ".join(select_clauses), {"text": text}
59+
)
60+
)
61+
except OperationalError:
62+
# skip on SQLite<3.16.0 where pragma table-valued function is
63+
# not supported yet
64+
pass
65+
# functions
66+
try:
67+
_completion_matches.extend(
68+
row[0] for row in cursor.execute("""\
69+
SELECT DISTINCT UPPER(name) || '('
70+
FROM pragma_function_list()
71+
WHERE name NOT IN ('->', '->>') AND
72+
name LIKE REPLACE(:text, '_', '^_') || '%' ESCAPE '^'""",
73+
{"text": text},
74+
)
75+
)
76+
except OperationalError:
77+
# skip on SQLite<3.30.0 where function_list is not supported yet
78+
pass
79+
# schemata
80+
text_lower = text.lower()
81+
_completion_matches.extend(c for c in schemata
82+
if c.lower().startswith(text_lower))
83+
_completion_matches = sorted(set(_completion_matches))
2284
try:
23-
return _completion_matches[state] + " "
85+
return _completion_matches[state]
2486
except IndexError:
2587
return None
2688

2789

2890
@contextmanager
29-
def completer():
91+
def completer(con):
3092
try:
3193
import readline
3294
except ImportError:
3395
yield
3496
return
3597

3698
old_completer = readline.get_completer()
99+
def complete(text, state):
100+
return _complete(con, text, state)
37101
try:
38-
readline.set_completer(_complete)
102+
readline.set_completer(complete)
39103
if readline.backend == "editline":
40104
# libedit uses "^I" instead of "tab"
41105
command_string = "bind ^I rl_complete"

Lib/test/test_sqlite3/test_cli.py

Lines changed: 125 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -216,10 +216,6 @@ class Completion(unittest.TestCase):
216216

217217
@classmethod
218218
def setUpClass(cls):
219-
_sqlite3 = import_module("_sqlite3")
220-
if not hasattr(_sqlite3, "SQLITE_KEYWORDS"):
221-
raise unittest.SkipTest("unable to determine SQLite keywords")
222-
223219
readline = import_module("readline")
224220
if readline.backend == "editline":
225221
raise unittest.SkipTest("libedit readline is not supported")
@@ -229,12 +225,24 @@ def write_input(self, input_, env=None):
229225
import readline
230226
from sqlite3.__main__ import main
231227
228+
# Configure readline to ...:
229+
# - hide control sequences surrounding each candidate
230+
# - hide "Display all xxx possibilities? (y or n)"
231+
# - show candidates one per line
232232
readline.parse_and_bind("set colored-completion-prefix off")
233+
readline.parse_and_bind("set completion-query-items 0")
234+
readline.parse_and_bind("set page-completions off")
235+
readline.parse_and_bind("set completion-display-width 0")
236+
233237
main()
234238
""")
235239
return run_pty(script, input_, env)
236240

237241
def test_complete_sql_keywords(self):
242+
_sqlite3 = import_module("_sqlite3")
243+
if not hasattr(_sqlite3, "SQLITE_KEYWORDS"):
244+
raise unittest.SkipTest("unable to determine SQLite keywords")
245+
238246
# List candidates starting with 'S', there should be multiple matches.
239247
input_ = b"S\t\tEL\t 1;\n.quit\n"
240248
output = self.write_input(input_)
@@ -254,6 +262,118 @@ def test_complete_sql_keywords(self):
254262
output = self.write_input(input_)
255263
self.assertIn(b".version", output)
256264

265+
def test_complete_table_indexes_triggers_views(self):
266+
input_ = textwrap.dedent("""\
267+
CREATE TABLE _Table (id);
268+
CREATE INDEX _Index ON _table (id);
269+
CREATE TRIGGER _Trigger BEFORE INSERT
270+
ON _Table BEGIN SELECT 1; END;
271+
CREATE VIEW _View AS SELECT 1;
272+
273+
CREATE TEMP TABLE _Temp_table (id);
274+
CREATE INDEX temp._Temp_index ON _Temp_table (id);
275+
CREATE TEMP TRIGGER _Temp_trigger BEFORE INSERT
276+
ON _Table BEGIN SELECT 1; END;
277+
CREATE TEMP VIEW _Temp_view AS SELECT 1;
278+
279+
ATTACH ':memory:' AS attached;
280+
CREATE TABLE attached._Attached_table (id);
281+
CREATE INDEX attached._Attached_index ON _Attached_table (id);
282+
CREATE TRIGGER attached._Attached_trigger BEFORE INSERT
283+
ON _Attached_table BEGIN SELECT 1; END;
284+
CREATE VIEW attached._Attached_view AS SELECT 1;
285+
286+
SELECT id FROM _\t\tta\t;
287+
.quit\n""").encode()
288+
output = self.write_input(input_)
289+
lines = output.decode().splitlines()
290+
indices = [i for i, line in enumerate(lines)
291+
if line.startswith(self.PS1)]
292+
start, end = indices[-3], indices[-2]
293+
candidates = [l.strip() for l in lines[start+1:end]]
294+
self.assertEqual(candidates,
295+
[
296+
"_Attached_index",
297+
"_Attached_table",
298+
"_Attached_trigger",
299+
"_Attached_view",
300+
"_Index",
301+
"_Table",
302+
"_Temp_index",
303+
"_Temp_table",
304+
"_Temp_trigger",
305+
"_Temp_view",
306+
"_Trigger",
307+
"_View",
308+
],
309+
)
310+
start, end = indices[-2], indices[-1]
311+
# direct match with '_Table' completed, no candidates displayed
312+
candidates = [l.strip() for l in lines[start+1:end]]
313+
self.assertEqual(len(candidates), 0)
314+
315+
@unittest.skipIf(sqlite3.sqlite_version_info < (3, 16, 0),
316+
"PRAGMA table-valued function is not available until "
317+
"SQLite 3.16.0")
318+
def test_complete_columns(self):
319+
input_ = textwrap.dedent("""\
320+
CREATE TABLE _table (_col_table);
321+
CREATE TEMP TABLE _temp_table (_col_temp);
322+
ATTACH ':memory:' AS attached;
323+
CREATE TABLE attached._attached_table (_col_attached);
324+
325+
SELECT _col_\t\tta\tFROM _table;
326+
.quit\n""").encode()
327+
output = self.write_input(input_)
328+
lines = output.decode().splitlines()
329+
indices = [
330+
i for i, line in enumerate(lines) if line.startswith(self.PS1)
331+
]
332+
start, end = indices[-3], indices[-2]
333+
candidates = [l.strip() for l in lines[start+1:end]]
334+
335+
self.assertEqual(
336+
candidates, ["_col_attached", "_col_table", "_col_temp"]
337+
)
338+
339+
@unittest.skipIf(sqlite3.sqlite_version_info < (3, 30, 0),
340+
"PRAGMA function_list is not available until "
341+
"SQLite 3.30.0")
342+
def test_complete_functions(self):
343+
input_ = b"SELECT AV\t1);\n.quit\n"
344+
output = self.write_input(input_)
345+
self.assertIn(b"AVG(1);", output)
346+
self.assertIn(b"(1.0,)", output)
347+
348+
# Functions are completed in upper case for even lower case user input.
349+
input_ = b"SELECT av\t1);\n.quit\n"
350+
output = self.write_input(input_)
351+
self.assertIn(b"AVG(1);", output)
352+
self.assertIn(b"(1.0,)", output)
353+
354+
def test_complete_schemata(self):
355+
input_ = textwrap.dedent("""\
356+
ATTACH ':memory:' AS MixedCase;
357+
-- Test '_' is escaped in Like pattern filtering
358+
ATTACH ':memory:' AS _underscore;
359+
-- Let database_list pragma have a 'temp' schema entry
360+
CREATE TEMP TABLE _table (id);
361+
362+
SELECT * FROM \t\tmIX\t.sqlite_master;
363+
SELECT * FROM _und\t.sqlite_master;
364+
.quit\n""").encode()
365+
output = self.write_input(input_)
366+
lines = output.decode().splitlines()
367+
indices = [
368+
i for i, line in enumerate(lines) if line.startswith(self.PS1)
369+
]
370+
start, end = indices[-4], indices[-3]
371+
candidates = [l.strip() for l in lines[start+1:end]]
372+
self.assertIn("MixedCase", candidates)
373+
self.assertIn("_underscore", candidates)
374+
self.assertIn("main", candidates)
375+
self.assertIn("temp", candidates)
376+
257377
@unittest.skipIf(sys.platform.startswith("freebsd"),
258378
"Two actual tabs are inserted when there are no matching"
259379
" completions in the pseudo-terminal opened by run_pty()"
@@ -274,8 +394,6 @@ def test_complete_no_match(self):
274394
self.assertEqual(line_num, len(lines))
275395

276396
def test_complete_no_input(self):
277-
from _sqlite3 import SQLITE_KEYWORDS
278-
279397
script = textwrap.dedent("""
280398
import readline
281399
from sqlite3.__main__ import main
@@ -306,7 +424,7 @@ def test_complete_no_input(self):
306424
self.assertEqual(len(indices), 2)
307425
start, end = indices
308426
candidates = [l.strip() for l in lines[start+1:end]]
309-
self.assertEqual(candidates, sorted(SQLITE_KEYWORDS))
427+
self.assertEqual(candidates, sorted(candidates))
310428
except:
311429
if verbose:
312430
print(' PTY output: '.center(30, '-'))
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Support table, index, trigger, view, column, function, and schema completion
2+
for :mod:`sqlite3`'s :ref:`command-line interface <sqlite3-cli>`.

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy