From e3015f93cd4fe30d47d3c78abed7f09e836940d4 Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Sat, 28 Feb 2026 17:28:28 -0500 Subject: [PATCH] complete more precisely in the "value" position When in the "value" position, that is, where a value may be referred to such as a literal, function name, or column name, don't offer all keywords, but instead limit keywords to function names and function- alikes. Pygments has some errors in its designations of keywords vs functions, such as classifying JSON_VALUE() as a keyword, and some missing functions as well, so we amend the values imported from Pygments. At a certain point the amendments would be large enough that we should consider maintaining our own categorical lists. But the amendments are so far not too extensive. (We might also consider adding loadable function names.) Since we have made the list of function names more accurate, we can then remove '{"type": "keyword"}' from the completion candidates when we are in the "value" position (keeping '{"type": "function"}' which is already present. Now, functions such as JSON_VALUE() complete in the "value" position, _eg_, after a SELECT, but mere keywords such as SELECT do not. We no longer suggest "SELECT SELECT"! An exception was made here for completions within backticks, which are still not great, and need future work, because the choices are too many. Backtick completions are left in the current state. As a comment notes, we should better also define "value position" keywords such as CASE and make a separate completion set for them, rather than lumping them into the list of functions as is done here. There are other edge cases such as CURRENT_TIME, which can occur in the value position but does not take parentheses, and weird ones such as MEMBER OF(), a midfix function which also contains a space in the name. --- changelog.md | 1 + mycli/packages/completion_engine.py | 11 +- mycli/sqlcompleter.py | 108 +++++++++++++++++- test/test_completion_engine.py | 13 --- ...est_smart_completion_public_schema_only.py | 88 ++------------ 5 files changed, 127 insertions(+), 94 deletions(-) diff --git a/changelog.md b/changelog.md index 2bf0dac8..9c5cb8cc 100644 --- a/changelog.md +++ b/changelog.md @@ -10,6 +10,7 @@ Features * Add warnings-count prompt format strings: `\w` and `\W`. * Handle/document more attributes in the `[colors]` section of `~/.myclirc`. * Enable customization of table border color/attributes in `~/.myclirc`. +* Complete much more precisely in the "value" position. Bug Fixes diff --git a/mycli/packages/completion_engine.py b/mycli/packages/completion_engine.py index 6d8258b5..4ef140af 100644 --- a/mycli/packages/completion_engine.py +++ b/mycli/packages/completion_engine.py @@ -384,7 +384,9 @@ def suggest_based_on_last_token( {"type": "view", "schema": parent}, {"type": "function", "schema": parent}, ] - else: + elif is_inside_quotes(text_before_cursor, -1) == 'backtick': + # todo: this should be revised, since we complete too exuberantly within + # backticks, including keywords aliases = [alias or table for (schema, table, alias) in tables] return [ {"type": "column", "tables": tables}, @@ -392,6 +394,13 @@ def suggest_based_on_last_token( {"type": "alias", "aliases": aliases}, {"type": "keyword"}, ] + else: + aliases = [alias or table for (schema, table, alias) in tables] + return [ + {"type": "column", "tables": tables}, + {"type": "function", "schema": []}, + {"type": "alias", "aliases": aliases}, + ] elif ( (token_v.endswith("join") and isinstance(token, Token) and token.is_keyword) or (token_v in ("copy", "from", "update", "into", "describe", "truncate", "desc", "explain")) diff --git a/mycli/sqlcompleter.py b/mycli/sqlcompleter.py index de618c2f..130b7996 100644 --- a/mycli/sqlcompleter.py +++ b/mycli/sqlcompleter.py @@ -743,7 +743,113 @@ class SQLCompleter(Completer): "ZEROFILL", ] - functions = [x.upper() for x in MYSQL_FUNCTIONS] + # misclassified as keywords + # do they need to also be subtracted from keywords? + pygments_misclassified_functions = ( + 'ASCII', + 'AVG', + 'CHARSET', + 'COALESCE', + 'COLLATION', + 'CONVERT', + 'CUME_DIST', + 'CURRENT_DATE', + 'CURRENT_TIME', + 'CURRENT_TIMESTAMP', + 'CURRENT_USER', + 'DATABASE', + 'DAY', + 'DEFAULT', + 'DENSE_RANK', + 'EXISTS', + 'FIRST_VALUE', + 'FORMAT', + 'GEOMCOLLECTION', + 'GET_FORMAT', + 'GROUPING', + 'HOUR', + 'IF', + 'INSERT', + 'INTERVAL', + 'JSON_TABLE', + 'JSON_VALUE', + 'LAG', + 'LAST_VALUE', + 'LEAD', + 'LEFT', + 'LOCALTIME', + 'LOCALTIMESTAMP', + 'MATCH', + 'MICROSECOND', + 'MINUTE', + 'MOD', + 'MONTH', + 'NTH_VALUE', + 'NTILE', + 'PERCENT_RANK', + 'QUARTER', + 'RANK', + 'REPEAT', + 'REPLACE', + 'REVERSE', + 'RIGHT', + 'ROW_COUNT', + 'ROW_NUMBER', + 'SCHEMA', + 'SECOND', + 'TIMESTAMPADD', + 'TIMESTAMPDIFF', + 'TRUNCATE', + 'USER', + 'UTC_DATE', + 'UTC_TIME', + 'UTC_TIMESTAMP', + 'VALUES', + 'WEEK', + 'WEIGHT_STRING', + ) + + pygments_missing_functions = ( + 'BINARY', # deprecated function, but available everywhere + 'CHAR', + 'DATE', + 'DISTANCE', + 'ETAG', + 'GeometryCollection', + 'JSON_DUALITY_OBJECT', + 'LineString', + 'MultiLineString', + 'MultiPoint', + 'MultiPolygon', + 'Point', + 'Polygon', + 'STRING_TO_VECTOR', + 'TIME', + 'TIMESTAMP', + 'VECTOR_DIM', + 'VECTOR_TO_STRING', + 'YEAR', + ) + + # so far an incomplete list + # these should be spun out and completed independently from functions + pygments_value_position_nonfunction_keywords = ( + 'BETWEEN', + 'CASE', + 'FALSE', + 'NOT', + 'NULL', + 'TRUE', + ) + + # should https://dev.mysql.com/doc/refman/9.6/en/loadable-function-reference.html also be added? + functions = sorted({ + x.upper() + for x in MYSQL_FUNCTIONS + + pygments_misclassified_functions + + pygments_missing_functions + + pygments_value_position_nonfunction_keywords + }) # https://docs.pingcap.com/tidb/dev/tidb-functions tidb_functions = [ diff --git a/test/test_completion_engine.py b/test/test_completion_engine.py index 7b1c9f60..06720e36 100644 --- a/test/test_completion_engine.py +++ b/test/test_completion_engine.py @@ -21,7 +21,6 @@ def test_select_suggests_cols_with_visible_table_scope(): {"type": "alias", "aliases": ["tabl"]}, {"type": "column", "tables": [(None, "tabl", None)]}, {"type": "function", "schema": []}, - {"type": "keyword"}, ]) @@ -31,7 +30,6 @@ def test_select_suggests_cols_with_qualified_table_scope(): {"type": "alias", "aliases": ["tabl"]}, {"type": "column", "tables": [("sch", "tabl", None)]}, {"type": "function", "schema": []}, - {"type": "keyword"}, ]) @@ -55,7 +53,6 @@ def test_where_suggests_columns_functions(expression): {"type": "alias", "aliases": ["tabl"]}, {"type": "column", "tables": [(None, "tabl", None)]}, {"type": "function", "schema": []}, - {"type": "keyword"}, ]) @@ -67,7 +64,6 @@ def test_where_equals_suggests_enum_values_first(): {"type": "alias", "aliases": ["tabl"]}, {"type": "column", "tables": [(None, "tabl", None)]}, {"type": "function", "schema": []}, - {"type": "keyword"}, ]) @@ -84,7 +80,6 @@ def test_where_in_suggests_columns(expression): {"type": "alias", "aliases": ["tabl"]}, {"type": "column", "tables": [(None, "tabl", None)]}, {"type": "function", "schema": []}, - {"type": "keyword"}, ]) @@ -95,7 +90,6 @@ def test_where_equals_any_suggests_columns_or_keywords(): {"type": "alias", "aliases": ["tabl"]}, {"type": "column", "tables": [(None, "tabl", None)]}, {"type": "function", "schema": []}, - {"type": "keyword"}, ]) @@ -120,7 +114,6 @@ def test_select_suggests_cols_and_funcs(): {"type": "alias", "aliases": []}, {"type": "column", "tables": []}, {"type": "function", "schema": []}, - {"type": "keyword"}, ]) @@ -193,7 +186,6 @@ def test_col_comma_suggests_cols(): {"type": "alias", "aliases": ["tbl"]}, {"type": "column", "tables": [(None, "tbl", None)]}, {"type": "function", "schema": []}, - {"type": "keyword"}, ]) @@ -236,7 +228,6 @@ def test_partially_typed_col_name_suggests_col_names(): {"type": "alias", "aliases": ["tabl"]}, {"type": "column", "tables": [(None, "tabl", None)]}, {"type": "function", "schema": []}, - {"type": "keyword"}, ]) @@ -331,7 +322,6 @@ def test_sub_select_col_name_completion(): {"type": "alias", "aliases": ["abc"]}, {"type": "column", "tables": [(None, "abc", None)]}, {"type": "function", "schema": []}, - {"type": "keyword"}, ]) @@ -484,7 +474,6 @@ def test_2_statements_2nd_current(): {"type": "alias", "aliases": ["b"]}, {"type": "column", "tables": [(None, "b", None)]}, {"type": "function", "schema": []}, - {"type": "keyword"}, ]) # Should work even if first statement is invalid @@ -509,7 +498,6 @@ def test_2_statements_1st_current(): {"type": "alias", "aliases": ["a"]}, {"type": "column", "tables": [(None, "a", None)]}, {"type": "function", "schema": []}, - {"type": "keyword"}, ]) @@ -526,7 +514,6 @@ def test_3_statements_2nd_current(): {"type": "alias", "aliases": ["b"]}, {"type": "column", "tables": [(None, "b", None)]}, {"type": "function", "schema": []}, - {"type": "keyword"}, ]) diff --git a/test/test_smart_completion_public_schema_only.py b/test/test_smart_completion_public_schema_only.py index ca6ce245..b0326a5b 100644 --- a/test/test_smart_completion_public_schema_only.py +++ b/test/test_smart_completion_public_schema_only.py @@ -199,75 +199,11 @@ def test_function_name_completion(completer, complete_event): assert list(result) == [ Completion(text='MAX', start_position=-2), Completion(text='MATCH', start_position=-2), - Completion(text='MASTER', start_position=-2), - Completion(text='MAKE_SET', start_position=-2), Completion(text='MAKEDATE', start_position=-2), Completion(text='MAKETIME', start_position=-2), - Completion(text='MAX_ROWS', start_position=-2), - Completion(text='MAX_SIZE', start_position=-2), - Completion(text='MAXVALUE', start_position=-2), - Completion(text='MASTER_SSL', start_position=-2), - Completion(text='MASTER_BIND', start_position=-2), - Completion(text='MASTER_HOST', start_position=-2), - Completion(text='MASTER_PORT', start_position=-2), - Completion(text='MASTER_USER', start_position=-2), - Completion(text='MASTER_DELAY', start_position=-2), - Completion(text='MASTER_SSL_CA', start_position=-2), - Completion(text='MASTER_LOG_POS', start_position=-2), - Completion(text='MASTER_SSL_CRL', start_position=-2), - Completion(text='MASTER_SSL_KEY', start_position=-2), + Completion(text='MAKE_SET', start_position=-2), Completion(text='MASTER_POS_WAIT', start_position=-2), - Completion(text='MASTER_LOG_FILE', start_position=-2), - Completion(text='MASTER_PASSWORD', start_position=-2), - Completion(text='MASTER_SSL_CERT', start_position=-2), - Completion(text='MASTER_SSL_CAPATH', start_position=-2), - Completion(text='MASTER_SSL_CIPHER', start_position=-2), - Completion(text='MASTER_RETRY_COUNT', start_position=-2), - Completion(text='MASTER_SSL_CRLPATH', start_position=-2), - Completion(text='MASTER_TLS_VERSION', start_position=-2), - Completion(text='MASTER_AUTO_POSITION', start_position=-2), - Completion(text='MASTER_CONNECT_RETRY', start_position=-2), - Completion(text='MAX_QUERIES_PER_HOUR', start_position=-2), - Completion(text='MAX_UPDATES_PER_HOUR', start_position=-2), - Completion(text='MAX_USER_CONNECTIONS', start_position=-2), - Completion(text='MASTER_PUBLIC_KEY_PATH', start_position=-2), - Completion(text='MASTER_HEARTBEAT_PERIOD', start_position=-2), - Completion(text='MASTER_TLS_CIPHERSUITES', start_position=-2), - Completion(text='MAX_CONNECTIONS_PER_HOUR', start_position=-2), - Completion(text='MASTER_COMPRESSION_ALGORITHMS', start_position=-2), - Completion(text='MASTER_SSL_VERIFY_SERVER_CERT', start_position=-2), - Completion(text='MASTER_ZSTD_COMPRESSION_LEVEL', start_position=-2), Completion(text='email', start_position=-2), - Completion(text='DECIMAL', start_position=-2), - Completion(text='SMALLINT', start_position=-2), - Completion(text='TIMESTAMP', start_position=-2), - Completion(text='COLUMN_FORMAT', start_position=-2), - Completion(text='COLUMN_NAME', start_position=-2), - Completion(text='COMPACT', start_position=-2), - Completion(text='CONSTRAINT_SCHEMA', start_position=-2), - Completion(text='CURRENT_TIMESTAMP', start_position=-2), - Completion(text='FORMAT', start_position=-2), - Completion(text='GET_FORMAT', start_position=-2), - Completion(text='GET_MASTER_PUBLIC_KEY', start_position=-2), - Completion(text='LOCALTIMESTAMP', start_position=-2), - Completion(text='MESSAGE_TEXT', start_position=-2), - Completion(text='MIGRATE', start_position=-2), - Completion(text='NETWORK_NAMESPACE', start_position=-2), - Completion(text='PRIMARY', start_position=-2), - Completion(text='REQUIRE_ROW_FORMAT', start_position=-2), - Completion(text='REQUIRE_TABLE_PRIMARY_KEY_CHECK', start_position=-2), - Completion(text='ROW_FORMAT', start_position=-2), - Completion(text='SCHEMA', start_position=-2), - Completion(text='SCHEMA_NAME', start_position=-2), - Completion(text='SCHEMAS', start_position=-2), - Completion(text='SQL_SMALL_RESULT', start_position=-2), - Completion(text='TEMPORARY', start_position=-2), - Completion(text='TEMPTABLE', start_position=-2), - Completion(text='TERMINATED', start_position=-2), - Completion(text='TIMESTAMPADD', start_position=-2), - Completion(text='TIMESTAMPDIFF', start_position=-2), - Completion(text='UTC_TIMESTAMP', start_position=-2), - Completion(text='CHANGE MASTER TO', start_position=-2), ] @@ -292,12 +228,11 @@ def test_suggested_column_names(completer, complete_event): ] + list(map(Completion, completer.functions)) + [Completion(text="users", start_position=0)] - + [x for x in map(Completion, completer.keywords) if x.text not in completer.functions] ) def test_suggested_column_names_empty_db(empty_completer, complete_event): - """Suggest * and function/keywords when selecting from no-table db. + """Suggest * and function when selecting from no-table db. :param empty_completer: :param complete_event: @@ -312,7 +247,6 @@ def test_suggested_column_names_empty_db(empty_completer, complete_event): Completion(text="*", start_position=0), ] + list(map(Completion, empty_completer.functions)) - + [x for x in map(Completion, empty_completer.keywords) if x.text not in empty_completer.functions] ) @@ -399,7 +333,6 @@ def test_suggested_multiple_column_names(completer, complete_event): ] + list(map(Completion, completer.functions)) + [Completion(text="u", start_position=0)] - + [x for x in map(Completion, completer.keywords) if x.text not in completer.functions] ) @@ -551,7 +484,6 @@ def test_auto_escaped_col_names(completer, complete_event): ] + completer.functions + ["select"] - + [x for x in completer.keywords if x not in completer.functions] ) assert result == expected @@ -565,7 +497,7 @@ def test_un_escaped_table_names(completer, complete_event): "id", "`insert`", "ABC", - ] + completer.functions + ["réveillé"] + [x for x in completer.keywords if x not in completer.functions] + ] + completer.functions + ["réveillé"] # todo: the fixtures are insufficient; the database name should also appear in the result @@ -647,14 +579,12 @@ def test_file_name_completion(completer, complete_event, text, expected): def test_auto_case_heuristic(completer, complete_event): - text = "select jon_" - position = len("select jon_") + text = "select json_v" + position = len("select json_v") result = list(completer.get_completions(Document(text=text, cursor_position=position), complete_event)) assert [x.text for x in result] == [ - 'json_table', + 'json_valid', 'json_value', - 'join', - 'json', ] @@ -817,16 +747,17 @@ def test_backticked_column_completion_two_character(completer, complete_event): Completion(text='`fast`', start_position=-2), Completion(text='`file`', start_position=-2), Completion(text='`full`', start_position=-2), + Completion(text='`false`', start_position=-2), Completion(text='`field`', start_position=-2), Completion(text='`floor`', start_position=-2), Completion(text='`fixed`', start_position=-2), Completion(text='`float`', start_position=-2), - Completion(text='`false`', start_position=-2), Completion(text='`fetch`', start_position=-2), Completion(text='`first`', start_position=-2), Completion(text='`flush`', start_position=-2), Completion(text='`force`', start_position=-2), Completion(text='`found`', start_position=-2), + Completion(text='`format`', start_position=-2), Completion(text='`float4`', start_position=-2), Completion(text='`float8`', start_position=-2), Completion(text='`factor`', start_position=-2), @@ -834,7 +765,6 @@ def test_backticked_column_completion_two_character(completer, complete_event): Completion(text='`fields`', start_position=-2), Completion(text='`filter`', start_position=-2), Completion(text='`finish`', start_position=-2), - Completion(text='`format`', start_position=-2), Completion(text='`follows`', start_position=-2), Completion(text='`foreign`', start_position=-2), Completion(text='`fulltext`', start_position=-2), @@ -844,8 +774,8 @@ def test_backticked_column_completion_two_character(completer, complete_event): Completion(text='`first_name`', start_position=-2), Completion(text='`found_rows`', start_position=-2), Completion(text='`find_in_set`', start_position=-2), - Completion(text='`from_base64`', start_position=-2), Completion(text='`first_value`', start_position=-2), + Completion(text='`from_base64`', start_position=-2), Completion(text='`foreign key`', start_position=-2), Completion(text='`format_bytes`', start_position=-2), Completion(text='`from_unixtime`', start_position=-2),