from _sqlite3 import OperationalError from contextlib import contextmanager try: from _sqlite3 import SQLITE_KEYWORDS except ImportError: SQLITE_KEYWORDS = () CLI_COMMANDS = ('.quit', '.help', '.version') _completion_matches = [] def _complete(con, text, state): global _completion_matches if state == 0: if text.startswith("."): _completion_matches = [ c + " " for c in CLI_COMMANDS if c.startswith(text) ] else: text_upper = text.upper() _completion_matches = [ c + " " for c in SQLITE_KEYWORDS if c.startswith(text_upper) ] cursor = con.cursor() schemata = tuple(row[1] for row in cursor.execute("PRAGMA database_list")) # tables, indexes, triggers, and views # escape '_' which can appear in attached database names select_clauses = ( f"""\ SELECT name || ' ' FROM \"{schema}\".sqlite_master WHERE name LIKE REPLACE(:text, '_', '^_') || '%' ESCAPE '^'""" for schema in schemata ) _completion_matches.extend( row[0] for row in cursor.execute( " UNION ".join(select_clauses), {"text": text} ) ) # columns try: select_clauses = ( f"""\ SELECT pti.name || ' ' FROM "{schema}".sqlite_master AS sm JOIN pragma_table_xinfo(sm.name,'{schema}') AS pti WHERE sm.type='table' AND pti.name LIKE REPLACE(:text, '_', '^_') || '%' ESCAPE '^'""" for schema in schemata ) _completion_matches.extend( row[0] for row in cursor.execute( " UNION ".join(select_clauses), {"text": text} ) ) except OperationalError: # skip on SQLite<3.16.0 where pragma table-valued function is # not supported yet pass # functions try: _completion_matches.extend( row[0] for row in cursor.execute("""\ SELECT DISTINCT UPPER(name) || '(' FROM pragma_function_list() WHERE name NOT IN ('->', '->>') AND name LIKE REPLACE(:text, '_', '^_') || '%' ESCAPE '^'""", {"text": text}, ) ) except OperationalError: # skip on SQLite<3.30.0 where function_list is not supported yet pass # schemata text_lower = text.lower() _completion_matches.extend(c for c in schemata if c.lower().startswith(text_lower)) _completion_matches = sorted(set(_completion_matches)) try: return _completion_matches[state] except IndexError: return None @contextmanager def completer(con): try: import readline except ImportError: yield return old_completer = readline.get_completer() def complete(text, state): return _complete(con, text, state) 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)