diff --git a/changelog.md b/changelog.md index 136c85c4..6048bced 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,7 @@ Features --------- * Dynamic terminal titles based on prompt format strings. * Ability to turn off the toolbar. +* Add completions for introducers on literals. Bug Fixes diff --git a/mycli/completion_refresher.py b/mycli/completion_refresher.py index e28b5081..f34c5b89 100644 --- a/mycli/completion_refresher.py +++ b/mycli/completion_refresher.py @@ -160,6 +160,11 @@ def refresh_procedures(completer: SQLCompleter, executor: SQLExecute) -> None: completer.extend_procedures(executor.procedures()) +@refresher("character_sets") +def refresh_character_sets(completer: SQLCompleter, executor: SQLExecute) -> None: + completer.extend_character_sets(executor.character_sets()) + + @refresher("special_commands") def refresh_special(completer: SQLCompleter, executor: SQLExecute) -> None: completer.extend_special_commands(list(COMMANDS.keys())) diff --git a/mycli/packages/completion_engine.py b/mycli/packages/completion_engine.py index 4ef140af..c8b3d40e 100644 --- a/mycli/packages/completion_engine.py +++ b/mycli/packages/completion_engine.py @@ -399,6 +399,7 @@ def suggest_based_on_last_token( return [ {"type": "column", "tables": tables}, {"type": "function", "schema": []}, + {"type": "introducer", "schema": []}, {"type": "alias", "aliases": aliases}, ] elif ( diff --git a/mycli/sqlcompleter.py b/mycli/sqlcompleter.py index 9f4fa9b7..112effae 100644 --- a/mycli/sqlcompleter.py +++ b/mycli/sqlcompleter.py @@ -1086,6 +1086,18 @@ def extend_procedures(self, procedure_data: Generator[tuple]) -> None: continue metadata[self.dbname][elt[0]] = None + def extend_character_sets(self, character_set_data: Generator[tuple]) -> None: + metadata = self.dbmetadata["character_sets"] + if self.dbname not in metadata: + metadata[self.dbname] = {} + + for elt in character_set_data: + if not elt: + continue + if not elt[0]: + continue + metadata[self.dbname][elt[0]] = None + def set_dbname(self, dbname: str | None) -> None: self.dbname = dbname or '' @@ -1099,6 +1111,7 @@ def reset_completions(self) -> None: "views": {}, "functions": {}, "procedures": {}, + "character_sets": {}, "enum_values": {}, } self.all_completions = set(self.keywords + self.functions) @@ -1307,6 +1320,16 @@ def get_completions( ) completions.extend([(*x, rank) for x in procs_m]) + elif suggestion['type'] == 'introducer': + charsets = self.populate_schema_objects(suggestion['schema'], 'character_sets') + introducers = [f'_{x}' for x in charsets] + introducers_m = self.find_matches( + word_before_cursor, + introducers, + text_before_cursor=document.text_before_cursor, + ) + completions.extend([(*x, rank) for x in introducers_m]) + elif suggestion["type"] == "table": # If this is a select and columns are given, parse the columns and # then only return tables that have one or more of the given columns. @@ -1440,6 +1463,7 @@ def get_completions( text_before_cursor=document.text_before_cursor, ) completions.extend([(*x, rank) for x in subcommands_m]) + elif suggestion["type"] == "enum_value": enum_values = self.populate_enum_values( suggestion["tables"], diff --git a/mycli/sqlexecute.py b/mycli/sqlexecute.py index 2b70957e..18c5e689 100644 --- a/mycli/sqlexecute.py +++ b/mycli/sqlexecute.py @@ -103,6 +103,8 @@ class SQLExecute: procedures_query = '''SELECT ROUTINE_NAME FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_TYPE="PROCEDURE" AND ROUTINE_SCHEMA = %s''' + character_sets_query = '''SHOW CHARACTER SET''' + table_columns_query = """select TABLE_NAME, COLUMN_NAME from information_schema.columns where table_schema = %s order by table_name,ordinal_position""" @@ -466,6 +468,20 @@ def procedures(self) -> Generator[tuple, None, None]: else: yield from cur + def character_sets(self) -> Generator[tuple, None, None]: + """Yields tuples of (character_set_name, )""" + + assert isinstance(self.conn, Connection) + with self.conn.cursor() as cur: + _logger.debug("Character sets Query. sql: %r", self.character_sets_query) + try: + cur.execute(self.character_sets_query) + except pymysql.DatabaseError as e: + _logger.error('No character_set completions due to %r', e) + yield () + else: + yield from cur + def show_candidates(self) -> Generator[tuple, None, None]: assert isinstance(self.conn, Connection) with self.conn.cursor() as cur: diff --git a/test/test_completion_engine.py b/test/test_completion_engine.py index 06720e36..0d62e65a 100644 --- a/test/test_completion_engine.py +++ b/test/test_completion_engine.py @@ -21,6 +21,7 @@ def test_select_suggests_cols_with_visible_table_scope(): {"type": "alias", "aliases": ["tabl"]}, {"type": "column", "tables": [(None, "tabl", None)]}, {"type": "function", "schema": []}, + {"type": "introducer", "schema": []}, ]) @@ -30,6 +31,7 @@ def test_select_suggests_cols_with_qualified_table_scope(): {"type": "alias", "aliases": ["tabl"]}, {"type": "column", "tables": [("sch", "tabl", None)]}, {"type": "function", "schema": []}, + {"type": "introducer", "schema": []}, ]) @@ -53,6 +55,7 @@ def test_where_suggests_columns_functions(expression): {"type": "alias", "aliases": ["tabl"]}, {"type": "column", "tables": [(None, "tabl", None)]}, {"type": "function", "schema": []}, + {"type": "introducer", "schema": []}, ]) @@ -64,6 +67,7 @@ def test_where_equals_suggests_enum_values_first(): {"type": "alias", "aliases": ["tabl"]}, {"type": "column", "tables": [(None, "tabl", None)]}, {"type": "function", "schema": []}, + {"type": "introducer", "schema": []}, ]) @@ -80,6 +84,7 @@ def test_where_in_suggests_columns(expression): {"type": "alias", "aliases": ["tabl"]}, {"type": "column", "tables": [(None, "tabl", None)]}, {"type": "function", "schema": []}, + {"type": "introducer", "schema": []}, ]) @@ -90,6 +95,7 @@ def test_where_equals_any_suggests_columns_or_keywords(): {"type": "alias", "aliases": ["tabl"]}, {"type": "column", "tables": [(None, "tabl", None)]}, {"type": "function", "schema": []}, + {"type": "introducer", "schema": []}, ]) @@ -114,6 +120,7 @@ def test_select_suggests_cols_and_funcs(): {"type": "alias", "aliases": []}, {"type": "column", "tables": []}, {"type": "function", "schema": []}, + {"type": "introducer", "schema": []}, ]) @@ -186,6 +193,7 @@ def test_col_comma_suggests_cols(): {"type": "alias", "aliases": ["tbl"]}, {"type": "column", "tables": [(None, "tbl", None)]}, {"type": "function", "schema": []}, + {"type": "introducer", "schema": []}, ]) @@ -228,6 +236,7 @@ def test_partially_typed_col_name_suggests_col_names(): {"type": "alias", "aliases": ["tabl"]}, {"type": "column", "tables": [(None, "tabl", None)]}, {"type": "function", "schema": []}, + {"type": "introducer", "schema": []}, ]) @@ -322,6 +331,7 @@ def test_sub_select_col_name_completion(): {"type": "alias", "aliases": ["abc"]}, {"type": "column", "tables": [(None, "abc", None)]}, {"type": "function", "schema": []}, + {"type": "introducer", "schema": []}, ]) @@ -331,6 +341,7 @@ def test_sub_select_multiple_col_name_completion(): assert sorted_dicts(suggestions) == sorted_dicts([ {"type": "column", "tables": [(None, "abc", None)]}, {"type": "function", "schema": []}, + {"type": "introducer", "schema": []}, ]) @@ -474,6 +485,7 @@ def test_2_statements_2nd_current(): {"type": "alias", "aliases": ["b"]}, {"type": "column", "tables": [(None, "b", None)]}, {"type": "function", "schema": []}, + {"type": "introducer", "schema": []}, ]) # Should work even if first statement is invalid @@ -498,6 +510,7 @@ def test_2_statements_1st_current(): {"type": "alias", "aliases": ["a"]}, {"type": "column", "tables": [(None, "a", None)]}, {"type": "function", "schema": []}, + {"type": "introducer", "schema": []}, ]) @@ -514,6 +527,7 @@ def test_3_statements_2nd_current(): {"type": "alias", "aliases": ["b"]}, {"type": "column", "tables": [(None, "b", None)]}, {"type": "function", "schema": []}, + {"type": "introducer", "schema": []}, ]) diff --git a/test/test_completion_refresher.py b/test/test_completion_refresher.py index ad527df8..fbf5e88a 100644 --- a/test/test_completion_refresher.py +++ b/test/test_completion_refresher.py @@ -30,6 +30,7 @@ def test_ctor(refresher): "users", "functions", "procedures", + "character_sets", "special_commands", "show_commands", "keywords", diff --git a/test/test_smart_completion_public_schema_only.py b/test/test_smart_completion_public_schema_only.py index dbf73d73..6a9db9ba 100644 --- a/test/test_smart_completion_public_schema_only.py +++ b/test/test_smart_completion_public_schema_only.py @@ -125,6 +125,16 @@ def test_select_star(completer, complete_event): assert list(result) == list(map(Completion, completer.keywords)) +def test_introducer_completion(completer, complete_event): + completer.extend_character_sets([('latin1',), ('utf8mb4',)]) + text = 'SELECT _' + position = len(text) + result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event)) + result_text = [item.text for item in result] + assert '_latin1' in result_text + assert '_utf8mb4' in result_text + + def test_table_completion(completer, complete_event): text = "SELECT * FROM " position = len(text)