1
0
Fork 0

Remove pandas dependency by storing database metadata as lists of tuples

This commit is contained in:
Darik Gamble 2015-01-25 11:39:03 -05:00
parent 7e7051fef7
commit 830d1beaa8
10 changed files with 202 additions and 301 deletions

View File

@ -267,10 +267,10 @@ class PGCli(object):
pgexecute = self.pgexecute pgexecute = self.pgexecute
schemata, tables, columns = pgexecute.get_metadata() completer.set_search_path(pgexecute.search_path())
completer.extend_schemata(schemata) completer.extend_schemata(pgexecute.schemata())
completer.extend_tables(tables) completer.extend_tables(pgexecute.tables())
completer.extend_columns(columns) completer.extend_columns(pgexecute.columns())
completer.extend_database_names(pgexecute.databases()) completer.extend_database_names(pgexecute.databases())
def get_completions(self, text, cursor_positition): def get_completions(self, text, cursor_positition):

View File

@ -1,7 +1,6 @@
from __future__ import print_function from __future__ import print_function
import re import re
import sqlparse import sqlparse
from pandas import DataFrame
from sqlparse.sql import IdentifierList, Identifier, Function from sqlparse.sql import IdentifierList, Identifier, Function
from sqlparse.tokens import Keyword, DML, Punctuation from sqlparse.tokens import Keyword, DML, Punctuation
@ -132,12 +131,12 @@ def extract_table_identifiers(token_stream):
def extract_tables(sql): def extract_tables(sql):
"""Extract the table names from an SQL statment. """Extract the table names from an SQL statment.
Returns a DataFrame with columns [schema, table, alias] Returns a list of (schema, table, alias) tuples
""" """
parsed = sqlparse.parse(sql) parsed = sqlparse.parse(sql)
if not parsed: if not parsed:
return DataFrame({}, columns=['schema', 'table', 'alias']) return []
# INSERT statements must stop looking for tables at the sign of first # INSERT statements must stop looking for tables at the sign of first
# Punctuation. eg: INSERT INTO abc (col1, col2) VALUES (1, 2) # Punctuation. eg: INSERT INTO abc (col1, col2) VALUES (1, 2)
@ -145,8 +144,7 @@ def extract_tables(sql):
# we'll identify abc, col1 and col2 as table names. # we'll identify abc, col1 and col2 as table names.
insert_stmt = parsed[0].token_first().value.lower() == 'insert' insert_stmt = parsed[0].token_first().value.lower() == 'insert'
stream = extract_from_part(parsed[0], stop_at_punctuation=insert_stmt) stream = extract_from_part(parsed[0], stop_at_punctuation=insert_stmt)
tables = extract_table_identifiers(stream) return list(extract_table_identifiers(stream))
return DataFrame.from_records(tables, columns=['schema', 'table', 'alias'])
def find_prev_keyword(sql): def find_prev_keyword(sql):
if not sql.strip(): if not sql.strip():

View File

@ -78,12 +78,12 @@ def suggest_based_on_last_token(token, text_before_cursor, full_text):
elif token_v.lower() in ('from', 'update', 'into', 'describe', 'join', 'table'): elif token_v.lower() in ('from', 'update', 'into', 'describe', 'join', 'table'):
return [{'type': 'schema'}, {'type': 'table', 'schema': []}] return [{'type': 'schema'}, {'type': 'table', 'schema': []}]
elif token_v.lower() == 'on': elif token_v.lower() == 'on':
tables = extract_tables(full_text) tables = extract_tables(full_text) # [(schema, table, alias), ...]
# Use table alias if there is one, otherwise the table name # Use table alias if there is one, otherwise the table name
alias = tables['alias'].where(tables['alias'].notnull(), tables['table']) alias = [t[2] or t[1] for t in tables]
return [{'type': 'alias', 'aliases': list(alias)}] return [{'type': 'alias', 'aliases': alias}]
elif token_v in ('d',): # \d elif token_v in ('d',): # \d
return [{'type': 'schema'}, {'type': 'table', 'schema': []}] return [{'type': 'schema'}, {'type': 'table', 'schema': []}]
@ -102,9 +102,8 @@ def suggest_based_on_last_token(token, text_before_cursor, full_text):
# TABLE.<suggestion> or SCHEMA.TABLE.<suggestion> # TABLE.<suggestion> or SCHEMA.TABLE.<suggestion>
tables = extract_tables(full_text) tables = extract_tables(full_text)
tables = get_matching_tables(tables, identifier) tables = [t for t in tables if identifies(identifier, *t)]
suggestions.append({'type': 'column', suggestions.append({'type': 'column', 'tables': tables})
'tables': tables[['schema', 'table', 'alias']]})
# SCHEMA.<suggestion> # SCHEMA.<suggestion>
suggestions.append({'type': 'table', 'schema': identifier}) suggestions.append({'type': 'table', 'schema': identifier})
@ -113,20 +112,7 @@ def suggest_based_on_last_token(token, text_before_cursor, full_text):
return [{'type': 'keyword'}] return [{'type': 'keyword'}]
def get_matching_tables(tables, identifier):
"""
:param tables: DataFrame with columns [schema, table, alias]
:param identifier: a table name, table alias, or fully qualified schema.tablename
:return: a row or rows from tables that match the indentifier
"""
tables['full_name'] = tables.apply(qualify_table_name, axis=1)
#match a table to an identifier if the identifer is equal to any one of the def identifies(id, schema, table, alias):
#table name, table alias, or schema-qualified table name return id == alias or id == table or (
matches = tables[['table', 'alias', 'full_name']] == identifier schema and (id == schema + '.' + table))
is_match = matches.any(axis=1)
return tables[is_match]
def qualify_table_name(row):
return row['schema'] + '.' + row['table'] if row['schema'] else row['table']

View File

@ -1,11 +1,10 @@
from __future__ import print_function from __future__ import print_function
import logging import logging
from collections import defaultdict
from prompt_toolkit.completion import Completer, Completion from prompt_toolkit.completion import Completer, Completion
from .packages.sqlcompletion import suggest_type from .packages.sqlcompletion import suggest_type
from .packages.parseutils import last_word from .packages.parseutils import last_word
from re import compile from re import compile
from pandas import DataFrame
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -35,11 +34,9 @@ class PGCompleter(Completer):
'UCASE'] 'UCASE']
special_commands = [] special_commands = []
databases = [] databases = []
schemata = DataFrame({}, columns=['schema']) dbmetadata = {}
tables = DataFrame({}, columns=['schema', 'table', 'alias']) search_path = []
columns = DataFrame({}, columns=['schema', 'table', 'column'])
all_completions = set(keywords + functions) all_completions = set(keywords + functions)
@ -52,7 +49,7 @@ class PGCompleter(Completer):
self.name_pattern = compile("^[_a-z][_a-z0-9\$]*$") self.name_pattern = compile("^[_a-z][_a-z0-9\$]*$")
def escape_name(self, name): def escape_name(self, name):
if name and not name=='*' and ((not self.name_pattern.match(name)) if name and ((not self.name_pattern.match(name))
or (name.upper() in self.reserved_words) or (name.upper() in self.reserved_words)
or (name.upper() in self.functions)): or (name.upper() in self.functions)):
name = '"%s"' % name name = '"%s"' % name
@ -82,41 +79,44 @@ class PGCompleter(Completer):
self.keywords.extend(additional_keywords) self.keywords.extend(additional_keywords)
self.all_completions.update(additional_keywords) self.all_completions.update(additional_keywords)
def extend_schemata(self, data): def extend_schemata(self, schemata):
# data is a DataFrame with columns [schema] # data is a DataFrame with columns [schema]
data['schema'] = data['schema'].apply(self.escape_name) schemata = self.escaped_names(schemata)
self.schemata = self.schemata.append(data) for schema in schemata:
self.all_completions.update(data['schema']) self.dbmetadata[schema] = {}
def extend_tables(self, data): self.all_completions.update(schemata)
# data is a DataFrame with columns [schema, table, is_visible] def extend_tables(self, table_data):
data[['schema', 'table']] = \
data[['schema', 'table']].apply(self.escaped_names)
self.tables = self.tables.append(data)
self.all_completions.update(data['schema']) # table_data is a list of (schema_name, table_name) tuples
self.all_completions.update(data['table']) table_data = [self.escaped_names(d) for d in table_data]
# Auto-add '*' as a column in all tables # dbmetadata['schema_name']['table_name'] should be a list of column
cols = data[['schema', 'table']].copy() # names. Default to an asterisk
cols['column'] = '*' for schema, table in table_data:
self.columns = self.columns.append(cols) self.dbmetadata[schema][table] = ['*']
def extend_columns(self, data): self.all_completions.update(t[1] for t in table_data)
# data is a DataFrame with columns [schema, table, column] def extend_columns(self, column_data):
data[['schema', 'table', 'column']] = \
data[['schema', 'table', 'column']].apply(self.escaped_names) # column_data is a list of (schema_name, table_name, column_name) tuples
self.columns = self.columns.append(data) column_data = [self.escaped_names(d) for d in column_data]
self.all_completions.update(data.column)
for schema, table, column in column_data:
self.dbmetadata[schema][table].append(column)
self.all_completions.update(t[2] for t in column_data)
def set_search_path(self, search_path):
self.search_path = self.escaped_names(search_path)
def reset_completions(self): def reset_completions(self):
self.databases = [] self.databases = []
self.schemata = DataFrame({}, columns=['schema']) self.search_path = []
self.tables = DataFrame({}, columns=['schema', 'table', 'alias']) self.dbmetadata = {}
self.columns = DataFrame({}, columns=['schema', 'table', 'column'])
self.all_completions = set(self.keywords) self.all_completions = set(self.keywords)
@staticmethod @staticmethod
@ -155,17 +155,23 @@ class PGCompleter(Completer):
completions.extend(funcs) completions.extend(funcs)
elif suggestion['type'] == 'schema': elif suggestion['type'] == 'schema':
schema_names = self.schemata['schema'] schema_names = self.dbmetadata.keys()
schema_names = self.find_matches(word_before_cursor, schema_names) schema_names = self.find_matches(word_before_cursor, schema_names)
completions.extend(schema_names) completions.extend(schema_names)
elif suggestion['type'] == 'table': elif suggestion['type'] == 'table':
meta = self.tables
if suggestion['schema']: if suggestion['schema']:
tables = meta.table[meta.schema == suggestion['schema']] try:
tables = self.dbmetadata[suggestion['schema']].keys()
except KeyError:
#schema doesn't exist
tables = []
else: else:
tables = meta.table[meta.is_visible] schemas = self.search_path
meta = self.dbmetadata
tables = [tbl for schema in schemas
for tbl in meta[schema].keys()]
tables = self.find_matches(word_before_cursor, tables) tables = self.find_matches(word_before_cursor, tables)
completions.extend(tables) completions.extend(tables)
@ -186,25 +192,34 @@ class PGCompleter(Completer):
def populate_scoped_cols(self, scoped_tbls): def populate_scoped_cols(self, scoped_tbls):
""" Find all columns in a set of scoped_tables """ Find all columns in a set of scoped_tables
:param scoped_tbls: DataFrame with columns [schema, table, alias] :param scoped_tbls: list of (schema, table, alias) tuples
:return: list of column names :return: list of column names
""" """
columns = self.columns # dataframe with columns [schema, table, column] columns = []
meta = self.dbmetadata
scoped_tbls[['schema', 'table', 'alias']] = \ for tbl in scoped_tbls:
scoped_tbls[['schema', 'table', 'alias']].apply(self.escaped_names) if tbl[0]:
# A fully qualified schema.table reference
schema = self.escape_name(tbl[0])
table = self.escape_name(tbl[1])
try:
# Get columns from the corresponding schema.table
columns.extend(meta[schema][table])
except KeyError:
# Either the schema or table doesn't exist
pass
else:
for schema in self.search_path:
table = self.escape_name(tbl[1])
try:
columns.extend(meta[schema][table])
break
except KeyError:
pass
return columns
# For fully qualified tables, inner join on (schema, table)
qualed = scoped_tbls.merge(columns, how='inner', on=['schema', 'table'])
# Only allow unqualified table reference on visible tables
vis_tables = self.tables[self.tables['is_visible']]
unqualed_tables = scoped_tbls.merge(vis_tables,
how='inner', on=['table'], suffixes=['_left', '_right'])
unqualed_tables['schema'] = unqualed_tables['schema_right']
unqualed = unqualed_tables.merge(columns, how='inner', on=['schema', 'table'])
return list(qualed['column']) + list(unqualed['column'])

View File

@ -3,7 +3,6 @@ import psycopg2
import psycopg2.extras import psycopg2.extras
import psycopg2.extensions import psycopg2.extensions
import sqlparse import sqlparse
from pandas import DataFrame
from .packages import pgspecial from .packages import pgspecial
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -24,16 +23,19 @@ psycopg2.extensions.set_wait_callback(psycopg2.extras.wait_select)
class PGExecute(object): class PGExecute(object):
search_path_query = '''
SELECT * FROM unnest(current_schemas(false))'''
schemata_query = ''' schemata_query = '''
SELECT nspname SELECT nspname
FROM pg_catalog.pg_namespace FROM pg_catalog.pg_namespace
WHERE nspname !~ '^pg_' WHERE nspname !~ '^pg_'
AND nspname <> 'information_schema' ''' AND nspname <> 'information_schema'
ORDER BY 1 '''
tables_query = ''' tables_query = '''
SELECT n.nspname schema_name, SELECT n.nspname schema_name,
c.relname table_name, c.relname table_name
pg_catalog.pg_table_is_visible(c.oid) is_visible
FROM pg_catalog.pg_class c FROM pg_catalog.pg_class c
LEFT JOIN pg_catalog.pg_namespace n LEFT JOIN pg_catalog.pg_namespace n
ON n.oid = c.relnamespace ON n.oid = c.relnamespace
@ -139,34 +141,37 @@ class PGExecute(object):
_logger.debug('No rows in result.') _logger.debug('No rows in result.')
return (None, None, cur.statusmessage) return (None, None, cur.statusmessage)
def get_metadata(self): def search_path(self):
""" Returns a tuple [schemata, tables, columns] of DataFrames """Returns the current search path as a list of schema names"""
schemata: DataFrame with columns [schema] with self.conn.cursor() as cur:
tables: DataFrame with columns [schema, table, is_visible] _logger.debug('Search path query. sql: %r', self.search_path_query)
columns: DataFrame with columns [schema, table, column] cur.execute(self.search_path_query)
return [x[0] for x in cur.fetchall()]
""" def schemata(self):
"""Returns a list of schema names in the database"""
with self.conn.cursor() as cur: with self.conn.cursor() as cur:
_logger.debug('Schemata Query. sql: %r', self.schemata_query) _logger.debug('Schemata Query. sql: %r', self.schemata_query)
cur.execute(self.schemata_query) cur.execute(self.schemata_query)
schemata = DataFrame.from_records(cur, return [x[0] for x in cur.fetchall()]
columns=['schema'])
def tables(self):
"""Returns a list of (schema_name, table_name) tuples """
with self.conn.cursor() as cur: with self.conn.cursor() as cur:
_logger.debug('Tables Query. sql: %r', self.tables_query) _logger.debug('Tables Query. sql: %r', self.tables_query)
cur.execute(self.tables_query) cur.execute(self.tables_query)
tables = DataFrame.from_records(cur, return cur.fetchall()
columns=['schema', 'table', 'is_visible'])
def columns(self):
"""Returns a list of (schema_name, table_name, column_name) tuples"""
with self.conn.cursor() as cur: with self.conn.cursor() as cur:
_logger.debug('Columns Query. sql: %r', self.columns_query) _logger.debug('Columns Query. sql: %r', self.columns_query)
cur.execute(self.columns_query) cur.execute(self.columns_query)
columns = DataFrame.from_records(cur, return cur.fetchall()
columns=['schema', 'table', 'column'])
return [schemata, tables, columns]
def databases(self): def databases(self):
with self.conn.cursor() as cur: with self.conn.cursor() as cur:

View File

@ -28,8 +28,7 @@ setup(
'jedi == 0.8.1', # Temporary fix for installation woes. 'jedi == 0.8.1', # Temporary fix for installation woes.
'prompt_toolkit==0.26', 'prompt_toolkit==0.26',
'psycopg2 >= 2.5.4', 'psycopg2 >= 2.5.4',
'sqlparse >= 0.1.14', 'sqlparse >= 0.1.14'
'pandas >= 0.15.0'
], ],
entry_points=''' entry_points='''
[console_scripts] [console_scripts]

View File

@ -4,114 +4,81 @@ from pgcli.packages.parseutils import extract_tables
def test_empty_string(): def test_empty_string():
tables = extract_tables('') tables = extract_tables('')
assert tables.to_dict('list') == {'schema': [], 'table': [], 'alias': []} assert tables == []
def test_simple_select_single_table(): def test_simple_select_single_table():
tables = extract_tables('select * from abc') tables = extract_tables('select * from abc')
assert tables.to_dict('list') == \ assert tables == [(None, 'abc', None)]
{'schema': [None], 'table': ['abc'], 'alias': [None]}
def test_simple_select_single_table_schema_qualified(): def test_simple_select_single_table_schema_qualified():
tables = extract_tables('select * from abc.def') tables = extract_tables('select * from abc.def')
assert tables.to_dict('list') == \ assert tables == [('abc', 'def', None)]
{'schema': ['abc'], 'table': ['def'], 'alias': [None]}
def test_simple_select_multiple_tables(): def test_simple_select_multiple_tables():
tables = extract_tables('select * from abc, def') tables = extract_tables('select * from abc, def')
assert tables.to_dict('list') == \ assert sorted(tables) == [(None, 'abc', None), (None, 'def', None)]
{'schema': [None, None],
'table': ['abc', 'def'],
'alias': [None, None]}
def test_simple_select_multiple_tables_schema_qualified(): def test_simple_select_multiple_tables_schema_qualified():
tables = extract_tables('select * from abc.def, ghi.jkl') tables = extract_tables('select * from abc.def, ghi.jkl')
assert tables.to_dict('list') == \ assert sorted(tables) == [('abc', 'def', None), ('ghi', 'jkl', None)]
{'schema': ['abc', 'ghi'],
'table': ['def', 'jkl'],
'alias': [None, None]}
def test_simple_select_with_cols_single_table(): def test_simple_select_with_cols_single_table():
tables = extract_tables('select a,b from abc') tables = extract_tables('select a,b from abc')
assert tables.to_dict('list') == \ assert tables == [(None, 'abc', None)]
{'schema': [None], 'table': ['abc'], 'alias': [None]}
def test_simple_select_with_cols_single_table_schema_qualified(): def test_simple_select_with_cols_single_table_schema_qualified():
tables = extract_tables('select a,b from abc.def') tables = extract_tables('select a,b from abc.def')
assert tables.to_dict('list') == \ assert tables == [('abc', 'def', None)]
{'schema': ['abc'], 'table': ['def'], 'alias': [None]}
def test_simple_select_with_cols_multiple_tables(): def test_simple_select_with_cols_multiple_tables():
tables = extract_tables('select a,b from abc, def') tables = extract_tables('select a,b from abc, def')
assert tables.to_dict('list') == \ assert sorted(tables) == [(None, 'abc', None), (None, 'def', None)]
{'schema': [None, None],
'table': ['abc', 'def'],
'alias': [None, None]}
def test_simple_select_with_cols_multiple_tables(): def test_simple_select_with_cols_multiple_tables():
tables = extract_tables('select a,b from abc.def, def.ghi') tables = extract_tables('select a,b from abc.def, def.ghi')
assert tables.to_dict('list') == \ assert sorted(tables) == [('abc', 'def', None), ('def', 'ghi', None)]
{'schema': ['abc', 'def'],
'table': ['def', 'ghi'],
'alias': [None, None]}
def test_select_with_hanging_comma_single_table(): def test_select_with_hanging_comma_single_table():
tables = extract_tables('select a, from abc') tables = extract_tables('select a, from abc')
assert tables.to_dict('list') == \ assert tables == [(None, 'abc', None)]
{'schema': [None],
'table': ['abc'],
'alias': [None]}
def test_select_with_hanging_comma_multiple_tables(): def test_select_with_hanging_comma_multiple_tables():
tables = extract_tables('select a, from abc, def') tables = extract_tables('select a, from abc, def')
assert tables.to_dict('list') == \ assert sorted(tables) == [(None, 'abc', None), (None, 'def', None)]
{'schema': [None, None],
'table': ['abc', 'def'],
'alias': [None, None]}
def test_select_with_hanging_period_multiple_tables(): def test_select_with_hanging_period_multiple_tables():
tables = extract_tables('SELECT t1. FROM tabl1 t1, tabl2 t2') tables = extract_tables('SELECT t1. FROM tabl1 t1, tabl2 t2')
assert tables.to_dict('list') == \ assert sorted(tables) == [(None, 'tabl1', 't1'), (None, 'tabl2', 't2')]
{'schema': [None, None],
'table': ['tabl1', 'tabl2'],
'alias': ['t1', 't2']}
def test_simple_insert_single_table(): def test_simple_insert_single_table():
tables = extract_tables('insert into abc (id, name) values (1, "def")') tables = extract_tables('insert into abc (id, name) values (1, "def")')
assert tables.to_dict('list') == \
{'schema': [None], 'table': ['abc'], 'alias': ['abc']} # sqlparse mistakenly assigns an alias to the table
# assert tables == [(None, 'abc', None)]
assert tables == [(None, 'abc', 'abc')]
@pytest.mark.xfail @pytest.mark.xfail
def test_simple_insert_single_table_schema_qualified(): def test_simple_insert_single_table_schema_qualified():
tables = extract_tables('insert into abc.def (id, name) values (1, "def")') tables = extract_tables('insert into abc.def (id, name) values (1, "def")')
assert tables.to_dict('list') == \ assert tables == [('abc', 'def', None)]
{'schema': ['abc'], 'table': ['def'], 'alias': [None]}
def test_simple_update_table(): def test_simple_update_table():
tables = extract_tables('update abc set id = 1') tables = extract_tables('update abc set id = 1')
assert tables.to_dict('list') == \ assert tables == [(None, 'abc', None)]
{'schema': [None], 'table': ['abc'], 'alias': [None]}
def test_simple_update_table(): def test_simple_update_table():
tables = extract_tables('update abc.def set id = 1') tables = extract_tables('update abc.def set id = 1')
assert tables.to_dict('list') == \ assert tables == [('abc', 'def', None)]
{'schema': ['abc'], 'table': ['def'], 'alias': [None]}
def test_join_table(): def test_join_table():
tables = extract_tables('SELECT * FROM abc a JOIN def d ON a.id = d.num') tables = extract_tables('SELECT * FROM abc a JOIN def d ON a.id = d.num')
assert tables.to_dict('list') == \ assert sorted(tables) == [(None, 'abc', 'a'), (None, 'def', 'd')]
{'schema': [None, None],
'table': ['abc', 'def'],
'alias': ['a', 'd']}
def test_join_table_schema_qualified(): def test_join_table_schema_qualified():
tables = extract_tables('SELECT * FROM abc.def x JOIN ghi.jkl y ON x.id = y.num') tables = extract_tables('SELECT * FROM abc.def x JOIN ghi.jkl y ON x.id = y.num')
assert tables.to_dict('list') == \ assert tables == [('abc', 'def', 'x'), ('ghi', 'jkl', 'y')]
{'schema': ['abc', 'ghi'],
'table': ['def', 'jkl'],
'alias': ['x', 'y']}
def test_join_as_table(): def test_join_as_table():
tables = extract_tables('SELECT * FROM my_table AS m WHERE m.a > 5') tables = extract_tables('SELECT * FROM my_table AS m WHERE m.a > 5')
assert tables.to_dict('list') == \ assert tables == [(None, 'my_table', 'm')]
{'schema': [None], 'table': ['my_table'], 'alias': ['m']}

View File

@ -23,18 +23,15 @@ def test_schemata_table_and_columns_query(executor):
run(executor, "create table schema1.c (w text)") run(executor, "create table schema1.c (w text)")
run(executor, "create schema schema2") run(executor, "create schema schema2")
schemata, tables, columns = executor.get_metadata() assert executor.schemata() == ['public', 'schema1', 'schema2']
assert schemata.to_dict('list') == { assert executor.tables() == [
'schema': ['public', 'schema1', 'schema2']} ('public', 'a'), ('public', 'b'), ('schema1', 'c')]
assert tables.to_dict('list') == {
'schema': ['public', 'public', 'schema1'],
'table': ['a', 'b', 'c'],
'is_visible': [True, True, False]}
assert columns.to_dict('list') == { assert executor.columns() == [
'schema': ['public', 'public', 'public', 'schema1'], ('public', 'a', 'x'), ('public', 'a', 'y'),
'table': ['a', 'a', 'b', 'c'], ('public', 'b', 'z'), ('schema1', 'c', 'w')]
'column': ['x', 'y', 'z', 'w']}
assert executor.search_path() == ['public']
@dbtest @dbtest
def test_database_list(executor): def test_database_list(executor):

View File

@ -1,9 +1,8 @@
import pytest import pytest
from pandas import DataFrame
from prompt_toolkit.completion import Completion from prompt_toolkit.completion import Completion
from prompt_toolkit.document import Document from prompt_toolkit.document import Document
schemata = { metadata = {
'public': { 'public': {
'users': ['id', 'email', 'first_name', 'last_name'], 'users': ['id', 'email', 'first_name', 'last_name'],
'orders': ['id', 'ordered_date', 'status'], 'orders': ['id', 'ordered_date', 'status'],
@ -21,24 +20,19 @@ def completer():
import pgcli.pgcompleter as pgcompleter import pgcli.pgcompleter as pgcompleter
comp = pgcompleter.PGCompleter(smart_completion=True) comp = pgcompleter.PGCompleter(smart_completion=True)
# Table metadata is a dataframe with columns [schema, table, is_visible] schemata, tables, columns = [], [], []
tables = DataFrame.from_records(
((schema, table, schema=='public')
for schema, tables in schemata.items()
for table, columns in tables.items()),
columns=['schema', 'table', 'is_visible'])
# Column metadata is a dataframe with columns [schema, table, column] for schema, tbls in metadata.items():
columns = DataFrame.from_records( schemata.append(schema)
((schema, table, column)
for schema, tables in schemata.items()
for table, columns in tables.items()
for column in columns),
columns=['schema', 'table', 'column'])
comp.extend_schemata(tables[['schema']].drop_duplicates()) for table, cols in tbls.items():
tables.append((schema, table))
columns.extend([(schema, table, col) for col in cols])
comp.extend_schemata(schemata)
comp.extend_tables(tables) comp.extend_tables(tables)
comp.extend_columns(columns) comp.extend_columns(columns)
comp.set_search_path(['public'])
return comp return comp

View File

@ -1,82 +1,57 @@
from pgcli.packages.sqlcompletion import suggest_type from pgcli.packages.sqlcompletion import suggest_type
import pytest import pytest
def assert_equals(suggestions, expected_suggestions):
""" Wrapper to convert dataframes to structs
"""
for suggestion in suggestions:
if 'tables' in suggestion:
suggestion['tables'] = suggestion['tables'].to_dict('list')
assert sorted(suggestions) == sorted(expected_suggestions)
def test_select_suggests_cols_with_visible_table_scope(): def test_select_suggests_cols_with_visible_table_scope():
suggestions = suggest_type('SELECT FROM tabl', 'SELECT ') suggestions = suggest_type('SELECT FROM tabl', 'SELECT ')
assert_equals(suggestions, assert sorted(suggestions) == sorted([
[{'type': 'column', {'type': 'column', 'tables': [(None, 'tabl', None)]},
'tables': {'schema': [None], {'type': 'function'}])
'table': ['tabl'],
'alias': [None]}},
{'type': 'function'}])
def test_select_suggests_cols_with_qualified_table_scope(): def test_select_suggests_cols_with_qualified_table_scope():
suggestions = suggest_type('SELECT FROM sch.tabl', 'SELECT ') suggestions = suggest_type('SELECT FROM sch.tabl', 'SELECT ')
assert_equals(suggestions, assert sorted(suggestions) == sorted([
[{'type': 'column', {'type': 'column', 'tables': [('sch', 'tabl', None)]},
'tables': {'schema': ['sch'], {'type': 'function'}])
'table': ['tabl'],
'alias': [None]}},
{'type': 'function'}])
def test_where_suggests_columns_functions(): def test_where_suggests_columns_functions():
suggestions = suggest_type('SELECT * FROM tabl WHERE ', suggestions = suggest_type('SELECT * FROM tabl WHERE ',
'SELECT * FROM tabl WHERE ') 'SELECT * FROM tabl WHERE ')
assert_equals(suggestions, assert sorted(suggestions) == sorted([
[{'type': 'column', {'type': 'column', 'tables': [(None, 'tabl', None)]},
'tables': {'schema': [None], 'table': ['tabl'], 'alias': [None]}}, {'type': 'function'}])
{'type': 'function'}])
def test_lparen_suggests_cols(): def test_lparen_suggests_cols():
suggestion = suggest_type('SELECT MAX( FROM tbl', 'SELECT MAX(') suggestion = suggest_type('SELECT MAX( FROM tbl', 'SELECT MAX(')
assert_equals(suggestion, assert suggestion == [
[{'type': 'column', {'type': 'column', 'tables': [(None, 'tbl', None)]}]
'tables': {'schema': [None], 'table': ['tbl'], 'alias': [None]}}])
def test_select_suggests_cols_and_funcs(): def test_select_suggests_cols_and_funcs():
suggestions = suggest_type('SELECT ', 'SELECT ') suggestions = suggest_type('SELECT ', 'SELECT ')
assert_equals(suggestions, assert sorted(suggestions) == sorted([
[{'type': 'column', {'type': 'column', 'tables': []},
'tables': {'schema': [], 'table': [], 'alias': []}},
{'type': 'function'}]) {'type': 'function'}])
def test_from_suggests_tables_and_schemas(): def test_from_suggests_tables_and_schemas():
suggestion = suggest_type('SELECT * FROM ', 'SELECT * FROM ') suggestions = suggest_type('SELECT * FROM ', 'SELECT * FROM ')
assert sorted(suggestion) == sorted([ assert sorted(suggestions) == sorted([
{'type': 'table', 'schema':[]}, {'type': 'table', 'schema': []},
{'type': 'schema'}]) {'type': 'schema'}])
def test_distinct_suggests_cols(): def test_distinct_suggests_cols():
suggestions = suggest_type('SELECT DISTINCT ', 'SELECT DISTINCT ') suggestions = suggest_type('SELECT DISTINCT ', 'SELECT DISTINCT ')
assert_equals(suggestions, assert suggestions == [{'type': 'column', 'tables': []}]
[{'type': 'column',
'tables': {'schema': [], 'table': [], 'alias': []}}])
def test_col_comma_suggests_cols(): def test_col_comma_suggests_cols():
suggestion = suggest_type('SELECT a, b, FROM tbl', 'SELECT a, b,') suggestions = suggest_type('SELECT a, b, FROM tbl', 'SELECT a, b,')
assert_equals(suggestion, assert sorted(suggestions) == sorted([
[{'type': 'column', {'type': 'column', 'tables': [(None, 'tbl', None)]},
'tables': {'schema': [None],
'table': ['tbl'],
'alias': [None]}},
{'type': 'function'}]) {'type': 'function'}])
def test_table_comma_suggests_tables_and_schemas(): def test_table_comma_suggests_tables_and_schemas():
suggestion = suggest_type('SELECT a, b FROM tbl1, ', suggestions = suggest_type('SELECT a, b FROM tbl1, ',
'SELECT a, b FROM tbl1, ') 'SELECT a, b FROM tbl1, ')
assert sorted(suggestion) == sorted([ assert sorted(suggestions) == sorted([
{'type': 'table', 'schema':[]}, {'type': 'table', 'schema': []},
{'type': 'schema'}]) {'type': 'schema'}])
def test_into_suggests_tables_and_schemas(): def test_into_suggests_tables_and_schemas():
@ -87,68 +62,42 @@ def test_into_suggests_tables_and_schemas():
def test_insert_into_lparen_suggests_cols(): def test_insert_into_lparen_suggests_cols():
suggestions = suggest_type('INSERT INTO abc (', 'INSERT INTO abc (') suggestions = suggest_type('INSERT INTO abc (', 'INSERT INTO abc (')
assert_equals(suggestions, assert suggestions == [{'type': 'column', 'tables': [(None, 'abc', None)]}]
[{'type': 'column',
'tables': {'schema': [None],
'table': ['abc'],
'alias': [None]}}])
def test_insert_into_lparen_partial_text_suggests_cols(): def test_insert_into_lparen_partial_text_suggests_cols():
suggestions = suggest_type('INSERT INTO abc (i', 'INSERT INTO abc (i') suggestions = suggest_type('INSERT INTO abc (i', 'INSERT INTO abc (i')
assert_equals(suggestions, assert suggestions == [{'type': 'column', 'tables': [(None, 'abc', None)]}]
[{'type': 'column',
'tables': {'schema': [None],
'table': ['abc'],
'alias': [None]}}])
def test_insert_into_lparen_comma_suggests_cols(): def test_insert_into_lparen_comma_suggests_cols():
suggestions = suggest_type('INSERT INTO abc (id,', 'INSERT INTO abc (id,') suggestions = suggest_type('INSERT INTO abc (id,', 'INSERT INTO abc (id,')
assert_equals(suggestions, assert suggestions == [{'type': 'column', 'tables': [(None, 'abc', None)]}]
[{'type': 'column',
'tables': {'schema': [None],
'table': ['abc'],
'alias': [None]}}])
def test_partially_typed_col_name_suggests_col_names(): def test_partially_typed_col_name_suggests_col_names():
suggestion = suggest_type('SELECT * FROM tabl WHERE col_n', suggestions = suggest_type('SELECT * FROM tabl WHERE col_n',
'SELECT * FROM tabl WHERE col_n') 'SELECT * FROM tabl WHERE col_n')
assert_equals(suggestion, assert sorted(suggestions) == sorted([
[{'type': 'column', {'type': 'column', 'tables': [(None, 'tabl', None)]},
'tables': {'schema': [None], {'type': 'function'}])
'table': ['tabl'],
'alias': [None]}},
{'type': 'function'}])
def test_dot_suggests_cols_of_a_table_or_schema_qualified_table(): def test_dot_suggests_cols_of_a_table_or_schema_qualified_table():
suggestions = suggest_type('SELECT tabl. FROM tabl', 'SELECT tabl.') suggestions = suggest_type('SELECT tabl. FROM tabl', 'SELECT tabl.')
assert_equals(suggestions, assert sorted(suggestions) == sorted([
[{'type': 'column', {'type': 'column', 'tables': [(None, 'tabl', None)]},
'tables': {'schema': [None], 'table': ['tabl'], 'alias': [None]}}, {'type': 'table', 'schema': 'tabl'}])
{'type': 'table', 'schema': 'tabl'}])
suggestions = suggest_type('SELECT tabl. FROM tabl', 'SELECT tabl.')
assert_equals(suggestions,
[{'type': 'column',
'tables': {'schema': [None], 'table': ['tabl'], 'alias': [None]}},
{'type': 'table', 'schema': 'tabl'}])
def test_dot_suggests_cols_of_an_alias(): def test_dot_suggests_cols_of_an_alias():
suggestions = suggest_type('SELECT t1. FROM tabl1 t1, tabl2 t2', suggestions = suggest_type('SELECT t1. FROM tabl1 t1, tabl2 t2',
'SELECT t1.') 'SELECT t1.')
assert_equals(suggestions, assert sorted(suggestions) == sorted([
[{'type': 'table', 'schema': 't1'}, {'type': 'table', 'schema': 't1'},
{'type': 'column', {'type': 'column', 'tables': [(None, 'tabl1', 't1')]}])
'tables': {'schema': [None], 'table': ['tabl1'], 'alias': ['t1']}}])
def test_dot_col_comma_suggests_cols_or_schema_qualified_table(): def test_dot_col_comma_suggests_cols_or_schema_qualified_table():
suggestions = suggest_type('SELECT t1.a, t2. FROM tabl1 t1, tabl2 t2', suggestions = suggest_type('SELECT t1.a, t2. FROM tabl1 t1, tabl2 t2',
'SELECT t1.a, t2.') 'SELECT t1.a, t2.')
assert_equals(suggestions, assert sorted(suggestions) == sorted([
[{'type': 'column', {'type': 'column', 'tables': [(None, 'tabl2', 't2')]},
'tables': {'schema': [None], {'type': 'table', 'schema': 't2'}])
'table': ['tabl2'],
'alias': ['t2']}},
{'type': 'table', 'schema': 't2'}])
def test_sub_select_suggests_keyword(): def test_sub_select_suggests_keyword():
suggestion = suggest_type('SELECT * FROM (', 'SELECT * FROM (') suggestion = suggest_type('SELECT * FROM (', 'SELECT * FROM (')
@ -167,27 +116,24 @@ def test_sub_select_table_name_completion():
def test_sub_select_col_name_completion(): def test_sub_select_col_name_completion():
suggestions = suggest_type('SELECT * FROM (SELECT FROM abc', suggestions = suggest_type('SELECT * FROM (SELECT FROM abc',
'SELECT * FROM (SELECT ') 'SELECT * FROM (SELECT ')
assert_equals(suggestions, assert sorted(suggestions) == sorted([
[{'type': 'column', {'type': 'column', 'tables': [(None, 'abc', None)]},
'tables': {'schema': [None], 'table': ['abc'], 'alias': [None]}}, {'type': 'function'}])
{'type': 'function'}])
@pytest.mark.xfail @pytest.mark.xfail
def test_sub_select_multiple_col_name_completion(): def test_sub_select_multiple_col_name_completion():
suggestions = suggest_type('SELECT * FROM (SELECT a, FROM abc', suggestions = suggest_type('SELECT * FROM (SELECT a, FROM abc',
'SELECT * FROM (SELECT a, ') 'SELECT * FROM (SELECT a, ')
assert_equals(suggestions, assert sorted(suggestions) == sorted([
[{'type': 'column', {'type': 'column', 'tables': [(None, 'abc', None)]},
'tables': {'schema': [None], 'table': ['abc'], 'alias': [None]}}, {'type': 'function'}])
{'type': 'function'}])
def test_sub_select_dot_col_name_completion(): def test_sub_select_dot_col_name_completion():
suggestions = suggest_type('SELECT * FROM (SELECT t. FROM tabl t', suggestions = suggest_type('SELECT * FROM (SELECT t. FROM tabl t',
'SELECT * FROM (SELECT t.') 'SELECT * FROM (SELECT t.')
assert_equals(suggestions, assert sorted(suggestions) == sorted([
[{'type': 'column', {'type': 'column', 'tables': [(None, 'tabl', 't')]},
'tables': {'schema': [None], 'table': ['tabl'], 'alias': ['t']}}, {'type': 'table', 'schema': 't'}])
{'type': 'table', 'schema': 't'}])
def test_join_suggests_tables_and_schemas(): def test_join_suggests_tables_and_schemas():
suggestion = suggest_type('SELECT * FROM abc a JOIN ', suggestion = suggest_type('SELECT * FROM abc a JOIN ',
@ -199,43 +145,37 @@ def test_join_suggests_tables_and_schemas():
def test_join_alias_dot_suggests_cols1(): def test_join_alias_dot_suggests_cols1():
suggestions = suggest_type('SELECT * FROM abc a JOIN def d ON a.', suggestions = suggest_type('SELECT * FROM abc a JOIN def d ON a.',
'SELECT * FROM abc a JOIN def d ON a.') 'SELECT * FROM abc a JOIN def d ON a.')
assert_equals(suggestions, assert sorted(suggestions) == sorted([
[{'type': 'column', {'type': 'column', 'tables': [(None, 'abc', 'a')]},
'tables': {'schema': [None], 'table': ['abc'], 'alias': ['a']}}, {'type': 'table', 'schema': 'a'}])
{'type': 'table', 'schema': 'a'}])
def test_join_alias_dot_suggests_cols2(): def test_join_alias_dot_suggests_cols2():
suggestion = suggest_type('SELECT * FROM abc a JOIN def d ON a.', suggestion = suggest_type('SELECT * FROM abc a JOIN def d ON a.',
'SELECT * FROM abc a JOIN def d ON a.id = d.') 'SELECT * FROM abc a JOIN def d ON a.id = d.')
assert_equals(suggestion, assert sorted(suggestion) == sorted([
[{'type': 'column', {'type': 'column', 'tables': [(None, 'def', 'd')]},
'tables': {'schema': [None], 'table': ['def'], 'alias': ['d']}}, {'type': 'table', 'schema': 'd'}])
{'type': 'table', 'schema': 'd'}])
def test_on_suggests_aliases(): def test_on_suggests_aliases():
suggestions = suggest_type( suggestions = suggest_type(
'select a.x, b.y from abc a join bcd b on ', 'select a.x, b.y from abc a join bcd b on ',
'select a.x, b.y from abc a join bcd b on ') 'select a.x, b.y from abc a join bcd b on ')
assert_equals(suggestions, assert suggestions == [{'type': 'alias', 'aliases': ['a', 'b']}]
[{'type': 'alias', 'aliases': ['a', 'b']}])
def test_on_suggests_tables(): def test_on_suggests_tables():
suggestions = suggest_type( suggestions = suggest_type(
'select abc.x, bcd.y from abc join bcd on ', 'select abc.x, bcd.y from abc join bcd on ',
'select abc.x, bcd.y from abc join bcd on ') 'select abc.x, bcd.y from abc join bcd on ')
assert_equals(suggestions, assert suggestions == [{'type': 'alias', 'aliases': ['abc', 'bcd']}]
[{'type': 'alias', 'aliases': ['abc', 'bcd']}])
def test_on_suggests_aliases_right_side(): def test_on_suggests_aliases_right_side():
suggestions = suggest_type( suggestions = suggest_type(
'select a.x, b.y from abc a join bcd b on a.id = ', 'select a.x, b.y from abc a join bcd b on a.id = ',
'select a.x, b.y from abc a join bcd b on a.id = ') 'select a.x, b.y from abc a join bcd b on a.id = ')
assert_equals(suggestions, assert suggestions == [{'type': 'alias', 'aliases': ['a', 'b']}]
[{'type': 'alias', 'aliases': ['a', 'b']}])
def test_on_suggests_tables_right_side(): def test_on_suggests_tables_right_side():
suggestions = suggest_type( suggestions = suggest_type(
'select abc.x, bcd.y from abc join bcd on ', 'select abc.x, bcd.y from abc join bcd on ',
'select abc.x, bcd.y from abc join bcd on ') 'select abc.x, bcd.y from abc join bcd on ')
assert_equals(suggestions, assert suggestions == [{'type': 'alias', 'aliases': ['abc', 'bcd']}]
[{'type': 'alias', 'aliases': ['abc', 'bcd']}])