summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorÉtienne BERSAC <etienne.bersac@dalibo.com>2017-04-26 12:12:35 +0200
committerÉtienne BERSAC <etienne.bersac@dalibo.com>2017-04-27 21:32:43 +0200
commit293b6b3a412255cc2c12f0fe94d41760d4f47d8e (patch)
treecdcd8acdfc7d0c5766ff4c288d0ce7fe95d0eb46
parentd4967adf41dfd69a8f9e126863465cfaab44e76b (diff)
Complete keywords depending on previous token
Keywords list is based on https://www.postgresql.org/docs/9.6/static/sql-commands.html.
-rw-r--r--changelog.rst2
-rw-r--r--pgcli/packages/pgliterals/pgliterals.json113
-rw-r--r--pgcli/packages/sqlcompletion.py9
-rw-r--r--pgcli/pgcompleter.py17
-rw-r--r--tests/metadata.py2
-rw-r--r--tests/test_naive_completion.py15
-rw-r--r--tests/test_sqlcompletion.py62
7 files changed, 179 insertions, 41 deletions
diff --git a/changelog.rst b/changelog.rst
index 1a64db29..ab41f20c 100644
--- a/changelog.rst
+++ b/changelog.rst
@@ -7,6 +7,7 @@ Features:
* Casing for column headers (Thanks: `Joakim Koljonen`_)
* Allow configurable character to be used for multi-line query continuations. (Thanks: `Owen Stephens`_)
* Completions after ORDER BY and DISTINCT now take account of table aliases. (Thanks: `Owen Stephens`_)
+* Narrow keyword candidates based on previous keyword. (Thanks: `Étienne Bersac`_)
Bug fixes:
----------
@@ -675,3 +676,4 @@ Improvements:
.. _`Owen Stephens`: https://github.com/owst
.. _`Russell Davies`: https://github.com/russelldavies
.. _`Dick Marinus`: https://github.com/meeuw
+.. _`Étienne Bersac`: https://github.com/bersace
diff --git a/pgcli/packages/pgliterals/pgliterals.json b/pgcli/packages/pgliterals/pgliterals.json
index c7b9b7f6..9021a9b9 100644
--- a/pgcli/packages/pgliterals/pgliterals.json
+++ b/pgcli/packages/pgliterals/pgliterals.json
@@ -3,12 +3,45 @@
"ACCESS": [],
"ADD": [],
"ALL": [],
- "ALTER": [],
+ "ALTER": [
+ "AGGREGATE",
+ "COLLATION",
+ "COLUMN",
+ "CONVERSION",
+ "DATABASE",
+ "DEFAULT",
+ "DOMAIN",
+ "EVENT TRIGGER",
+ "EXTENSION",
+ "FOREIGN",
+ "FUNCTION",
+ "GROUP",
+ "INDEX",
+ "LANGUAGE",
+ "LARGE OBJECT",
+ "MATERIALIZED VIEW",
+ "OPERATOR",
+ "POLICY",
+ "ROLE",
+ "RULE",
+ "SCHEMA",
+ "SEQUENCE",
+ "SERVER",
+ "SYSTEM",
+ "TABLE",
+ "TABLESPACE",
+ "TEXT SEARCH",
+ "TRIGGER",
+ "TYPE",
+ "USER",
+ "VIEW"
+ ],
"AND": [],
"ANY": [],
"AS": [],
"ASC": [],
"AUDIT": [],
+ "BEGIN": [],
"BETWEEN": [],
"BY": [],
"CASE": [],
@@ -21,7 +54,46 @@
"CONCURRENTLY": [],
"CONNECT": [],
"COPY": [],
- "CREATE": [],
+ "CREATE": [
+ "ACCESS METHOD",
+ "AGGREGATE",
+ "CAST",
+ "COLLATION",
+ "CONVERSION",
+ "DATABASE",
+ "DOMAIN",
+ "EVENT TRIGGER",
+ "EXTENSION",
+ "FOREIGN DATA WRAPPER",
+ "FOREIGN EXTENSION",
+ "FUNCTION",
+ "GLOBAL",
+ "GROUP",
+ "IF NOT EXISTS",
+ "INDEX",
+ "LANGUAGE",
+ "LOCAL",
+ "MATERIALIZED VIEW",
+ "OPERATOR",
+ "OR REPLACE",
+ "POLICY",
+ "ROLE",
+ "RULE",
+ "SCHEMA",
+ "SEQUENCE",
+ "SERVER",
+ "TABLE",
+ "TABLESPACE",
+ "TEMPORARY",
+ "TEXT SEARCH",
+ "TRIGGER",
+ "TYPE",
+ "UNIQUE",
+ "UNLOGGED",
+ "USER",
+ "USER MAPPING",
+ "VIEW"
+ ],
"CURRENT": [],
"DATABASE": [],
"DATE": [],
@@ -32,7 +104,41 @@
"DESC": [],
"DESCRIBE": [],
"DISTINCT": [],
- "DROP": [],
+ "DROP": [
+ "ACCESS METHOD",
+ "AGGREGATE",
+ "CAST",
+ "COLLATION",
+ "CONVERSION",
+ "DATABASE",
+ "DOMAIN",
+ "EVENT TRIGGER",
+ "EXTENSION",
+ "FOREIGN DATA WRAPPER",
+ "FOREIGN TABLE",
+ "FUNCTION",
+ "GROUP",
+ "INDEX",
+ "LANGUAGE",
+ "MATERIALIZED VIEW",
+ "OPERATOR",
+ "OWNED",
+ "POLICY",
+ "ROLE",
+ "RULE",
+ "SCHEMA",
+ "SEQUENCE",
+ "SERVER",
+ "TABLE",
+ "TABLESPACE",
+ "TEXT SEARCH",
+ "TRANSFORM",
+ "TRIGGER",
+ "TYPE",
+ "USER",
+ "USER MAPPING",
+ "VIEW"
+ ],
"EXPLAIN": [],
"ELSE": [],
"ENCODING": [],
@@ -105,6 +211,7 @@
"RAISE": [],
"RENAME": [],
"REPLACE": [],
+ "RESET": ["ALL"],
"RAW": [],
"REFRESH MATERIALIZED VIEW": [],
"RESOURCE": [],
diff --git a/pgcli/packages/sqlcompletion.py b/pgcli/packages/sqlcompletion.py
index bbda0f92..6bad7c38 100644
--- a/pgcli/packages/sqlcompletion.py
+++ b/pgcli/packages/sqlcompletion.py
@@ -46,7 +46,8 @@ Column = namedtuple(
)
Column.__new__.__defaults__ = (None, None, tuple(), False)
-Keyword = namedtuple('Keyword', [])
+Keyword = namedtuple('Keyword', ['last_token'])
+Keyword.__new__.__defaults__ = (None,)
NamedQuery = namedtuple('NamedQuery', [])
Datatype = namedtuple('Datatype', ['schema'])
Alias = namedtuple('Alias', ['aliases'])
@@ -394,7 +395,7 @@ def suggest_based_on_last_token(token, stmt):
return (Column(table_refs=tables, local_tables=stmt.local_tables,
qualifiable=True),
Function(schema=None),
- Keyword(),)
+ Keyword(token_v.upper()),)
elif token_v == 'as':
# Don't suggest anything for aliases
return ()
@@ -492,7 +493,7 @@ def suggest_based_on_last_token(token, stmt):
suggestions.append(Schema())
return tuple(suggestions)
elif token_v in {'alter', 'create', 'drop'}:
- return (Keyword(),)
+ return (Keyword(token_v.upper()),)
elif token.is_keyword:
# token is a keyword we haven't implemented any special handling for
# go backwards in the query until we find one we do recognize
@@ -500,7 +501,7 @@ def suggest_based_on_last_token(token, stmt):
if prev_keyword:
return suggest_based_on_last_token(prev_keyword, stmt)
else:
- return (Keyword(),)
+ return (Keyword(token_v.upper()),)
else:
return (Keyword(),)
diff --git a/pgcli/pgcompleter.py b/pgcli/pgcompleter.py
index 6c0b3754..eca8b86a 100644
--- a/pgcli/pgcompleter.py
+++ b/pgcli/pgcompleter.py
@@ -51,8 +51,10 @@ def generate_alias(tbl):
[l for l, prev in zip(tbl, '_' + tbl) if prev == '_' and l != '_'])
class PGCompleter(Completer):
+ # keywords_tree: A dict mapping keywords to well known following keywords.
+ # e.g. 'CREATE': ['TABLE', 'USER', ...],
keywords_tree = get_literals('keywords', type_=dict)
- keywords = tuple(chain(keywords_tree.keys(), *keywords_tree.values()))
+ keywords = tuple(set(chain(keywords_tree.keys(), *keywords_tree.values())))
functions = get_literals('functions')
datatypes = get_literals('datatypes')
@@ -650,7 +652,14 @@ class PGCompleter(Completer):
return self.find_matches(word_before_cursor, self.databases,
meta='database')
- def get_keyword_matches(self, _, word_before_cursor):
+ def get_keyword_matches(self, suggestion, word_before_cursor):
+ keywords = self.keywords_tree.keys()
+ # Get well known following keywords for the last token. If any, narrow
+ # candidates to this list.
+ next_keywords = self.keywords_tree.get(suggestion.last_token, [])
+ if next_keywords:
+ keywords = next_keywords
+
casing = self.keyword_casing
if casing == 'auto':
if word_before_cursor and word_before_cursor[-1].islower():
@@ -659,9 +668,9 @@ class PGCompleter(Completer):
casing = 'upper'
if casing == 'upper':
- keywords = [k.upper() for k in self.keywords]
+ keywords = [k.upper() for k in keywords]
else:
- keywords = [k.lower() for k in self.keywords]
+ keywords = [k.lower() for k in keywords]
return self.find_matches(word_before_cursor, keywords,
mode='strict', meta='keyword')
diff --git a/tests/metadata.py b/tests/metadata.py
index 15b4bc2b..37c8ff96 100644
--- a/tests/metadata.py
+++ b/tests/metadata.py
@@ -66,7 +66,7 @@ class MetaData(object):
return [datatype(dt, pos) for dt in self.completer.datatypes]
def keywords(self, pos=0):
- return [keyword(kw, pos) for kw in self.completer.keywords]
+ return [keyword(kw, pos) for kw in self.completer.keywords_tree.keys()]
def columns(self, tbl, parent='public', typ='tables', pos=0):
if typ == 'functions':
diff --git a/tests/test_naive_completion.py b/tests/test_naive_completion.py
index 986933f5..4494b358 100644
--- a/tests/test_naive_completion.py
+++ b/tests/test_naive_completion.py
@@ -56,3 +56,18 @@ def test_paths_completion(completer, complete_event):
complete_event,
smart_completion=True))
assert result > set([Completion(text="setup.py", start_position=0)])
+
+
+def test_alter_well_known_keywords_completion(completer, complete_event):
+ text = 'ALTER '
+ position = len(text)
+ result = set(completer.get_completions(
+ Document(text=text, cursor_position=position),
+ complete_event,
+ smart_completion=True))
+ assert result > set([
+ Completion(text="DATABASE", display_meta='keyword'),
+ Completion(text="TABLE", display_meta='keyword'),
+ Completion(text="SYSTEM", display_meta='keyword'),
+ ])
+ assert Completion(text="CREATE", display_meta="keyword") not in result
diff --git a/tests/test_sqlcompletion.py b/tests/test_sqlcompletion.py
index c4a7da11..23064459 100644
--- a/tests/test_sqlcompletion.py
+++ b/tests/test_sqlcompletion.py
@@ -4,23 +4,24 @@ from pgcli.packages.sqlcompletion import (
from pgcli.packages.parseutils.tables import TableReference
import pytest
-def cols_etc(table, schema=None, alias=None, is_function=False, parent=None):
+def cols_etc(table, schema=None, alias=None, is_function=False, parent=None,
+ last_keyword=None):
"""Returns the expected select-clause suggestions for a single-table
select."""
return set([
Column(table_refs=(TableReference(schema, table, alias, is_function),),
qualifiable=True),
Function(schema=parent),
- Keyword()])
+ Keyword(last_keyword)])
def test_select_suggests_cols_with_visible_table_scope():
suggestions = suggest_type('SELECT FROM tabl', 'SELECT ')
- assert set(suggestions) == cols_etc('tabl')
+ assert set(suggestions) == cols_etc('tabl', last_keyword='SELECT')
def test_select_suggests_cols_with_qualified_table_scope():
suggestions = suggest_type('SELECT FROM sch.tabl', 'SELECT ')
- assert set(suggestions) == cols_etc('tabl', 'sch')
+ assert set(suggestions) == cols_etc('tabl', 'sch', last_keyword='SELECT')
def test_cte_does_not_crash():
@@ -34,7 +35,7 @@ def test_cte_does_not_crash():
])
def test_where_suggests_columns_functions_quoted_table(expression):
suggestions = suggest_type(expression, expression)
- assert set(suggestions) == cols_etc('tabl', alias='"tabl"')
+ assert set(suggestions) == cols_etc('tabl', alias='"tabl"', last_keyword='WHERE')
@pytest.mark.parametrize('expression', [
@@ -53,7 +54,7 @@ def test_where_suggests_columns_functions_quoted_table(expression):
])
def test_where_suggests_columns_functions(expression):
suggestions = suggest_type(expression, expression)
- assert set(suggestions) == cols_etc('tabl')
+ assert set(suggestions) == cols_etc('tabl', last_keyword='WHERE')
@pytest.mark.parametrize('expression', [
@@ -62,7 +63,7 @@ def test_where_suggests_columns_functions(expression):
])
def test_where_in_suggests_columns(expression):
suggestions = suggest_type(expression, expression)
- assert set(suggestions) == cols_etc('tabl')
+ assert set(suggestions) == cols_etc('tabl', last_keyword='WHERE')
@pytest.mark.parametrize('expression', [
'SELECT 1 AS ',
@@ -76,7 +77,7 @@ def test_after_as(expression):
def test_where_equals_any_suggests_columns_or_keywords():
text = 'SELECT * FROM tabl WHERE foo = ANY('
suggestions = suggest_type(text, text)
- assert set(suggestions) == cols_etc('tabl')
+ assert set(suggestions) == cols_etc('tabl', last_keyword='WHERE')
def test_lparen_suggests_cols():
@@ -90,7 +91,7 @@ def test_select_suggests_cols_and_funcs():
assert set(suggestions) == set([
Column(table_refs=(), qualifiable=True),
Function(schema=None),
- Keyword(),
+ Keyword('SELECT'),
])
@@ -217,21 +218,24 @@ def test_distinct_suggests_cols(text):
assert set(suggestions) == set([
Column(table_refs=(), local_tables=(), qualifiable=True),
Function(schema=None),
- Keyword()
+ Keyword('DISTINCT')
])
-@pytest.mark.parametrize('text, text_before', [
+@pytest.mark.parametrize('text, text_before, last_keyword', [
(
'SELECT DISTINCT FROM tbl x JOIN tbl1 y',
- 'SELECT DISTINCT'
+ 'SELECT DISTINCT',
+ 'SELECT',
),
(
'SELECT * FROM tbl x JOIN tbl1 y ORDER BY ',
- 'SELECT * FROM tbl x JOIN tbl1 y ORDER BY '
+ 'SELECT * FROM tbl x JOIN tbl1 y ORDER BY ',
+ 'BY',
)
])
-def test_distinct_and_order_by_suggestions_with_aliases(text, text_before):
+def test_distinct_and_order_by_suggestions_with_aliases(text, text_before,
+ last_keyword):
suggestions = suggest_type(text, text_before)
assert set(suggestions) == set([
Column(
@@ -243,7 +247,7 @@ def test_distinct_and_order_by_suggestions_with_aliases(text, text_before):
qualifiable=True
),
Function(schema=None),
- Keyword()
+ Keyword(last_keyword)
])
@@ -275,7 +279,7 @@ def test_col_comma_suggests_cols():
assert set(suggestions) == set([
Column(table_refs=((None, 'tbl', None, False),), qualifiable=True),
Function(schema=None),
- Keyword(),
+ Keyword('SELECT'),
])
@@ -318,7 +322,7 @@ def test_insert_into_lparen_comma_suggests_cols():
def test_partially_typed_col_name_suggests_col_names():
suggestions = suggest_type('SELECT * FROM tabl WHERE col_n',
'SELECT * FROM tabl WHERE col_n')
- assert set(suggestions) == cols_etc('tabl')
+ assert set(suggestions) == cols_etc('tabl', last_keyword='WHERE')
def test_dot_suggests_cols_of_a_table_or_schema_qualified_table():
@@ -435,7 +439,7 @@ def test_sub_select_col_name_completion():
assert set(suggestions) == set([
Column(table_refs=((None, 'abc', None, False),), qualifiable=True),
Function(schema=None),
- Keyword(),
+ Keyword('SELECT'),
])
@@ -599,7 +603,7 @@ def test_2_statements_2nd_current():
assert set(suggestions) == set([
Column(table_refs=((None, 'b', None, False),), qualifiable=True),
Function(schema=None),
- Keyword()
+ Keyword('SELECT')
])
# Should work even if first statement is invalid
@@ -621,7 +625,7 @@ def test_2_statements_1st_current():
suggestions = suggest_type('select from a; select * from b',
'select ')
- assert set(suggestions) == cols_etc('a')
+ assert set(suggestions) == cols_etc('a', last_keyword='SELECT')
def test_3_statements_2nd_current():
@@ -634,7 +638,7 @@ def test_3_statements_2nd_current():
suggestions = suggest_type('select * from a; select from b; select * from c',
'select * from a; select ')
- assert set(suggestions) == cols_etc('b')
+ assert set(suggestions) == cols_etc('b', last_keyword='SELECT')
@pytest.mark.parametrize('text', [
'''
@@ -685,7 +689,7 @@ def test_statements_in_function_body(text):
assert set(suggestions) == set([
Column(table_refs=((None, 'foo', None, False),), qualifiable=True),
Function(schema=None),
- Keyword()
+ Keyword('SELECT'),
])
functions = [
@@ -709,12 +713,12 @@ SELECT 1 FROM foo;
@pytest.mark.parametrize('text', functions)
def test_statements_with_cursor_after_function_body(text):
suggestions = suggest_type(text, text[:text.find('; ') + 1])
- assert set(suggestions) == set([Keyword()])
+ assert set(suggestions) == set([Keyword(), Special()])
@pytest.mark.parametrize('text', functions)
def test_statements_with_cursor_before_function_body(text):
suggestions = suggest_type(text, '')
- assert set(suggestions) == set([Keyword()])
+ assert set(suggestions) == set([Keyword(), Special()])
def test_create_db_with_template():
suggestions = suggest_type('create database foo with template ',
@@ -828,16 +832,16 @@ def test_invalid_sql():
def test_suggest_where_keyword(text):
# https://github.com/dbcli/mycli/issues/135
suggestions = suggest_type(text, text)
- assert set(suggestions) == cols_etc('foo')
+ assert set(suggestions) == cols_etc('foo', last_keyword='WHERE')
@pytest.mark.parametrize('text, before, expected', [
('\\ns abc SELECT ', 'SELECT ', [
Column(table_refs=(), qualifiable=True),
Function(schema=None),
- Keyword()
+ Keyword('SELECT')
]),
- ('\\ns abc SELECT foo ', 'SELECT foo ',(Keyword(),)),
+ ('\\ns abc SELECT foo ', 'SELECT foo ', (Keyword(),)),
('\\ns abc SELECT t1. FROM tabl1 t1', 'SELECT t1.', [
Table(schema='t1'),
View(schema='t1'),
@@ -853,7 +857,7 @@ def test_named_query_completion(text, before, expected):
def test_select_suggests_fields_from_function():
suggestions = suggest_type('SELECT FROM func()', 'SELECT ')
assert set(suggestions) == cols_etc(
- 'func', is_function=True)
+ 'func', is_function=True, last_keyword='SELECT')
@pytest.mark.parametrize('sql', [
@@ -900,4 +904,4 @@ def test_handle_unrecognized_kw_generously():
'ALTER TABLE foo ALTER ',
])
def test_keyword_after_alter(sql):
- assert Keyword() in set(suggest_type(sql, sql))
+ assert Keyword('ALTER') in set(suggest_type(sql, sql))