mirror of
https://github.com/dbcli/pgcli.git
synced 2024-09-19 17:58:44 +03:00
Include arguments in function completions
E.g. instead of suggesting `my_func()`, suggest `my_func(arg1 :=, arg2 :=)` or `my_func(arg1 text, arg2 bigint)`, depending on the context.
This commit is contained in:
parent
15b34c1dc9
commit
f355c30ef7
@ -6,6 +6,7 @@ Upcoming
|
||||
* Use dbcli's Homebrew tap for installing pgcli on macOS (issue #718) (Thanks: `Thomas Roten`_).
|
||||
* Only set `LESS` environment variable if it's unset. (Thanks: `Irina Truong`_)
|
||||
* Quote schema in `SET SCHEMA` statement (issue #469) (Thanks: `Irina Truong`_)
|
||||
* Include arguments in function suggestions (Thanks: `Joakim Koljonen`_)
|
||||
|
||||
1.6.0
|
||||
=====
|
||||
|
@ -119,12 +119,6 @@ def refresh_views(completer, executor):
|
||||
completer.extend_relations(executor.views(), kind='views')
|
||||
completer.extend_columns(executor.view_columns(), kind='views')
|
||||
|
||||
|
||||
@refresher('functions')
|
||||
def refresh_functions(completer, executor):
|
||||
completer.extend_functions(executor.functions())
|
||||
|
||||
|
||||
@refresher('types')
|
||||
def refresh_types(completer, executor):
|
||||
completer.extend_datatypes(executor.datatypes())
|
||||
@ -148,3 +142,8 @@ def refresh_casing(completer, executor):
|
||||
if os.path.isfile(casing_file):
|
||||
with open(casing_file, 'r') as f:
|
||||
completer.extend_casing([line.strip() for line in f])
|
||||
|
||||
|
||||
@refresher('functions')
|
||||
def refresh_functions(completer, executor):
|
||||
completer.extend_functions(executor.functions())
|
||||
|
@ -1,15 +1,59 @@
|
||||
from collections import namedtuple
|
||||
|
||||
ColumnMetadata = namedtuple('ColumnMetadata', ['name', 'datatype', 'foreignkeys'])
|
||||
ForeignKey = namedtuple('ForeignKey', ['parentschema', 'parenttable',
|
||||
'parentcolumn', 'childschema', 'childtable', 'childcolumn'])
|
||||
_ColumnMetadata = namedtuple(
|
||||
'ColumnMetadata',
|
||||
['name', 'datatype', 'foreignkeys', 'default', 'has_default']
|
||||
)
|
||||
|
||||
|
||||
def ColumnMetadata(
|
||||
name, datatype, foreignkeys=None, default=None, has_default=False
|
||||
):
|
||||
return _ColumnMetadata(
|
||||
name, datatype, foreignkeys or [], default, has_default
|
||||
)
|
||||
|
||||
|
||||
ForeignKey = namedtuple(
|
||||
'ForeignKey',
|
||||
'parentschema,parenttable,parentcolumn,childschema,childtable,childcolumn'
|
||||
)
|
||||
TableMetadata = namedtuple('TableMetadata', 'name columns')
|
||||
|
||||
|
||||
def parse_defaults(defaults_string):
|
||||
"""Yields default values for a function, given the string provided by
|
||||
pg_get_expr(pg_catalog.pg_proc.proargdefaults, 0)"""
|
||||
if not defaults_string:
|
||||
return
|
||||
current = ''
|
||||
in_quote = None
|
||||
for char in defaults_string:
|
||||
if current == '' and char == ' ':
|
||||
# Skip space after comma separating default expressions
|
||||
continue
|
||||
if char == '"' or char == '\'':
|
||||
if in_quote and char == in_quote:
|
||||
# End quote
|
||||
in_quote = None
|
||||
elif not in_quote:
|
||||
# Begin quote
|
||||
in_quote = char
|
||||
elif char == ',' and not in_quote:
|
||||
# End of expression
|
||||
yield current
|
||||
current = ''
|
||||
continue
|
||||
current += char
|
||||
yield current
|
||||
|
||||
|
||||
class FunctionMetadata(object):
|
||||
|
||||
def __init__(self, schema_name, func_name, arg_names, arg_types,
|
||||
arg_modes, return_type, is_aggregate, is_window, is_set_returning):
|
||||
def __init__(
|
||||
self, schema_name, func_name, arg_names, arg_types, arg_modes,
|
||||
return_type, is_aggregate, is_window, is_set_returning, arg_defaults
|
||||
):
|
||||
"""Class for describing a postgresql function"""
|
||||
|
||||
self.schema_name = schema_name
|
||||
@ -30,6 +74,8 @@ class FunctionMetadata(object):
|
||||
else:
|
||||
self.arg_types = None
|
||||
|
||||
self.arg_defaults = tuple(parse_defaults(arg_defaults))
|
||||
|
||||
self.return_type = return_type.strip()
|
||||
self.is_aggregate = is_aggregate
|
||||
self.is_window = is_window
|
||||
@ -42,19 +88,51 @@ class FunctionMetadata(object):
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def _signature(self):
|
||||
return (
|
||||
self.schema_name, self.func_name, self.arg_names, self.arg_types,
|
||||
self.arg_modes, self.return_type, self.is_aggregate,
|
||||
self.is_window, self.is_set_returning, self.arg_defaults
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.schema_name, self.func_name, self.arg_names,
|
||||
self.arg_types, self.arg_modes, self.return_type,
|
||||
self.is_aggregate, self.is_window, self.is_set_returning))
|
||||
return hash(self._signature())
|
||||
|
||||
def __repr__(self):
|
||||
return (('%s(schema_name=%r, func_name=%r, arg_names=%r, '
|
||||
'arg_types=%r, arg_modes=%r, return_type=%r, is_aggregate=%r, '
|
||||
'is_window=%r, is_set_returning=%r)')
|
||||
% (self.__class__.__name__, self.schema_name, self.func_name,
|
||||
self.arg_names, self.arg_types, self.arg_modes,
|
||||
self.return_type, self.is_aggregate, self.is_window,
|
||||
self.is_set_returning))
|
||||
return (
|
||||
(
|
||||
'%s(schema_name=%r, func_name=%r, arg_names=%r, '
|
||||
'arg_types=%r, arg_modes=%r, return_type=%r, is_aggregate=%r, '
|
||||
'is_window=%r, is_set_returning=%r, arg_defaults=%r)'
|
||||
) % (self.__class__.__name__,) + self._signature()
|
||||
)
|
||||
|
||||
def has_variadic(self):
|
||||
return self.arg_modes and any(arg_mode == 'v' for arg_mode in self.arg_modes)
|
||||
|
||||
def args(self):
|
||||
"""Returns a list of input-parameter ColumnMetadata namedtuples."""
|
||||
if not self.arg_names:
|
||||
return []
|
||||
modes = self.arg_modes or ['i'] * len(self.arg_names)
|
||||
args = [
|
||||
(name, typ)
|
||||
for name, typ, mode in zip(self.arg_names, self.arg_types, modes)
|
||||
if mode in ('i', 'b', 'v') # IN, INOUT, VARIADIC
|
||||
]
|
||||
|
||||
def arg(name, typ, num):
|
||||
num_args = len(args)
|
||||
num_defaults = len(self.arg_defaults)
|
||||
has_default = num + num_defaults >= num_args
|
||||
default = (
|
||||
self.arg_defaults[num - num_args + num_defaults] if has_default
|
||||
else None
|
||||
)
|
||||
return ColumnMetadata(name, typ, [], default, has_default)
|
||||
|
||||
return [arg(name, typ, num) for num, (name, typ) in enumerate(args)]
|
||||
|
||||
|
||||
def fields(self):
|
||||
"""Returns a list of output-field ColumnMetadata namedtuples"""
|
||||
|
@ -112,6 +112,9 @@ class SqlStatement(object):
|
||||
tables = tables[1:]
|
||||
return tables
|
||||
|
||||
def get_previous_token(self, token):
|
||||
return self.parsed.token_prev(self.parsed.token_index(token))[1]
|
||||
|
||||
def get_identifier_schema(self):
|
||||
schema = (self.identifier and self.identifier.get_parent_name()) or None
|
||||
# If schema name is unquoted, lower-case it
|
||||
@ -432,8 +435,19 @@ def suggest_based_on_last_token(token, stmt):
|
||||
|
||||
return tuple(suggest)
|
||||
|
||||
elif token_v in ('table', 'view', 'function'):
|
||||
# E.g. 'DROP FUNCTION <funcname>', 'ALTER TABLE <tablname>'
|
||||
elif token_v == 'function':
|
||||
schema = stmt.get_identifier_schema()
|
||||
# stmt.get_previous_token will fail for e.g. `SELECT 1 FROM functions WHERE function:`
|
||||
try:
|
||||
if stmt.get_previous_token(token).value.lower() in('drop', 'alter'):
|
||||
return (Function(schema=schema, usage='signature'),)
|
||||
else:
|
||||
return (Function(schema=schema),)
|
||||
except ValueError:
|
||||
return tuple()
|
||||
|
||||
elif token_v in ('table', 'view'):
|
||||
# E.g. 'ALTER TABLE <tablname>'
|
||||
rel_type = {'table': Table, 'view': View, 'function': Function}[token_v]
|
||||
schema = stmt.get_identifier_schema()
|
||||
if schema:
|
||||
|
@ -29,17 +29,31 @@ NamedQueries.instance = NamedQueries.from_config(
|
||||
load_config(config_location() + 'config'))
|
||||
|
||||
Match = namedtuple('Match', ['completion', 'priority'])
|
||||
_SchemaObject = namedtuple('SchemaObject', ['name', 'schema', 'function'])
|
||||
|
||||
_SchemaObject = namedtuple('SchemaObject', 'name schema meta')
|
||||
|
||||
|
||||
def SchemaObject(name, schema=None, function=False):
|
||||
return _SchemaObject(name, schema, function)
|
||||
def SchemaObject(name, schema=None, meta=None):
|
||||
return _SchemaObject(name, schema, meta)
|
||||
|
||||
|
||||
_Candidate = namedtuple(
|
||||
'Candidate', ['completion', 'prio', 'meta', 'synonyms', 'prio2']
|
||||
'Candidate', 'completion prio meta synonyms prio2 display'
|
||||
)
|
||||
def Candidate(completion, prio=None, meta=None, synonyms=None, prio2=None):
|
||||
return _Candidate(completion, prio, meta, synonyms or [completion], prio2)
|
||||
|
||||
|
||||
def Candidate(
|
||||
completion, prio=None, meta=None, synonyms=None, prio2=None,
|
||||
display=None
|
||||
):
|
||||
return _Candidate(
|
||||
completion, prio, meta, synonyms or [completion], prio2,
|
||||
display or completion
|
||||
)
|
||||
|
||||
|
||||
# Used to strip trailing '::some_type' from default-value expressions
|
||||
arg_default_type_strip_regex = re.compile(r'::[\w\.]+(\[\])?$')
|
||||
|
||||
normalize_ref = lambda ref: ref if ref[0] == '"' else '"' + ref.lower() + '"'
|
||||
|
||||
@ -67,6 +81,16 @@ class PGCompleter(Completer):
|
||||
self.pgspecial = pgspecial
|
||||
self.prioritizer = PrevalenceCounter()
|
||||
settings = settings or {}
|
||||
self.signature_arg_style = settings.get(
|
||||
'signature_arg_style', '{arg_name} {arg_type}'
|
||||
)
|
||||
self.call_arg_style = settings.get(
|
||||
'call_arg_style', '{arg_name: <{max_arg_len}} := {arg_default}'
|
||||
)
|
||||
self.call_arg_display_style = settings.get(
|
||||
'call_arg_display_style', '{arg_name}'
|
||||
)
|
||||
self.call_arg_oneliner_max = settings.get('call_arg_oneliner_max', 2)
|
||||
self.search_path_filter = settings.get('search_path_filter')
|
||||
self.generate_aliases = settings.get('generate_aliases')
|
||||
self.casing_file = settings.get('casing_file')
|
||||
@ -186,8 +210,6 @@ class PGCompleter(Completer):
|
||||
def extend_functions(self, func_data):
|
||||
|
||||
# func_data is a list of function metadata namedtuples
|
||||
# with fields schema_name, func_name, arg_list, result,
|
||||
# is_aggregate, is_window, is_set_returning
|
||||
|
||||
# dbmetadata['schema_name']['functions']['function_name'] should return
|
||||
# the function metadata namedtuple for the corresponding function
|
||||
@ -203,6 +225,22 @@ class PGCompleter(Completer):
|
||||
|
||||
self.all_completions.add(func)
|
||||
|
||||
self._refresh_arg_list_cache()
|
||||
|
||||
def _refresh_arg_list_cache(self):
|
||||
# We keep a cache of {function_usage:{function_metadata: function_arg_list_string}}
|
||||
# This is used when suggesting functions, to avoid the latency that would result
|
||||
# if we'd recalculate the arg lists each time we suggest functions (in large DBs)
|
||||
self._arg_list_cache = {
|
||||
usage: {
|
||||
meta: self._arg_list(meta, usage)
|
||||
for sch, funcs in self.dbmetadata['functions'].items()
|
||||
for func, metas in funcs.items()
|
||||
for meta in metas
|
||||
}
|
||||
for usage in ('call', 'call_display', 'signature')
|
||||
}
|
||||
|
||||
def extend_foreignkeys(self, fk_data):
|
||||
|
||||
# fk_data is a list of ForeignKey namedtuples, with fields
|
||||
@ -329,7 +367,7 @@ class PGCompleter(Completer):
|
||||
matches = []
|
||||
for cand in collection:
|
||||
if isinstance(cand, _Candidate):
|
||||
item, prio, display_meta, synonyms, prio2 = cand
|
||||
item, prio, display_meta, synonyms, prio2, display = cand
|
||||
if display_meta is None:
|
||||
display_meta = meta
|
||||
syn_matches = (_match(x) for x in synonyms)
|
||||
@ -337,7 +375,7 @@ class PGCompleter(Completer):
|
||||
syn_matches = [m for m in syn_matches if m]
|
||||
sort_key = max(syn_matches) if syn_matches else None
|
||||
else:
|
||||
item, display_meta, prio, prio2 = cand, meta, 0, 0
|
||||
item, display_meta, prio, prio2, display = cand, meta, 0, 0, cand
|
||||
sort_key = _match(cand)
|
||||
|
||||
if sort_key:
|
||||
@ -359,15 +397,22 @@ class PGCompleter(Completer):
|
||||
+ tuple(c for c in item))
|
||||
|
||||
item = self.case(item)
|
||||
display = self.case(display)
|
||||
priority = (
|
||||
sort_key, type_priority, prio, priority_func(item),
|
||||
prio2, lexical_priority
|
||||
)
|
||||
|
||||
matches.append(Match(
|
||||
completion=Completion(item, -text_len,
|
||||
display_meta=display_meta),
|
||||
priority=priority))
|
||||
matches.append(
|
||||
Match(
|
||||
completion=Completion(
|
||||
text=item,
|
||||
start_position=-text_len,
|
||||
display_meta=display_meta,
|
||||
display=display
|
||||
),
|
||||
priority=priority
|
||||
)
|
||||
)
|
||||
return matches
|
||||
|
||||
def case(self, word):
|
||||
@ -405,7 +450,6 @@ class PGCompleter(Completer):
|
||||
|
||||
return [m.completion for m in matches]
|
||||
|
||||
|
||||
def get_column_matches(self, suggestion, word_before_cursor):
|
||||
tables = suggestion.table_refs
|
||||
do_qualify = suggestion.qualifiable and {'always': True, 'never': False,
|
||||
@ -569,15 +613,16 @@ class PGCompleter(Completer):
|
||||
def get_function_matches(self, suggestion, word_before_cursor, alias=False):
|
||||
if suggestion.usage == 'from':
|
||||
# Only suggest functions allowed in FROM clause
|
||||
filt = lambda f: not f.is_aggregate and not f.is_window
|
||||
def filt(f): return not f.is_aggregate and not f.is_window
|
||||
else:
|
||||
alias = False
|
||||
filt = lambda f: True
|
||||
|
||||
def filt(f): return True
|
||||
arg_mode = 'signature' if suggestion.usage == 'signature' else 'call'
|
||||
# Function overloading means we way have multiple functions of the same
|
||||
# name at this point, so keep unique names only
|
||||
funcs = set(
|
||||
self._make_cand(f, alias, suggestion)
|
||||
self._make_cand(f, alias, suggestion, arg_mode)
|
||||
for f in self.populate_functions(suggestion.schema, filt)
|
||||
)
|
||||
|
||||
@ -613,22 +658,84 @@ class PGCompleter(Completer):
|
||||
t_sug = Table(s.schema, s.table_refs, s.local_tables)
|
||||
v_sug = View(s.schema, s.table_refs)
|
||||
f_sug = Function(s.schema, s.table_refs, usage='from')
|
||||
return (self.get_table_matches(t_sug, word_before_cursor, alias)
|
||||
return (
|
||||
self.get_table_matches(t_sug, word_before_cursor, alias)
|
||||
+ self.get_view_matches(v_sug, word_before_cursor, alias)
|
||||
+ self.get_function_matches(f_sug, word_before_cursor, alias))
|
||||
+ self.get_function_matches(f_sug, word_before_cursor, alias)
|
||||
)
|
||||
|
||||
# Note: tbl is a SchemaObject
|
||||
def _make_cand(self, tbl, do_alias, suggestion):
|
||||
def _arg_list(self, func, usage):
|
||||
"""Returns a an arg list string, e.g. `(_foo:=23)` for a func.
|
||||
|
||||
:param func is a FunctionMetadata object
|
||||
:param usage is 'call', 'call_display' or 'signature'
|
||||
|
||||
"""
|
||||
template = {
|
||||
'call': self.call_arg_style,
|
||||
'call_display': self.call_arg_display_style,
|
||||
'signature': self.signature_arg_style
|
||||
}[usage]
|
||||
args = func.args()
|
||||
if not template:
|
||||
return '()'
|
||||
elif usage == 'call' and len(args) < 2:
|
||||
return '()'
|
||||
elif usage == 'call' and func.has_variadic():
|
||||
return '()'
|
||||
multiline = usage == 'call' and len(args) > self.call_arg_oneliner_max
|
||||
max_arg_len = max(len(a.name) for a in args) if multiline else 0
|
||||
args = (
|
||||
self._format_arg(template, arg, arg_num + 1, max_arg_len)
|
||||
for arg_num, arg in enumerate(args)
|
||||
)
|
||||
if multiline:
|
||||
return '(' + ','.join('\n ' + a for a in args if a) + '\n)'
|
||||
else:
|
||||
return '(' + ', '.join(a for a in args if a) + ')'
|
||||
|
||||
def _format_arg(self, template, arg, arg_num, max_arg_len):
|
||||
if not template:
|
||||
return None
|
||||
if arg.has_default:
|
||||
arg_default = 'NULL' if arg.default is None else arg.default
|
||||
# Remove trailing ::(schema.)type
|
||||
arg_default = arg_default_type_strip_regex.sub('', arg_default)
|
||||
else:
|
||||
arg_default = ''
|
||||
return template.format(
|
||||
max_arg_len=max_arg_len,
|
||||
arg_name=self.case(arg.name),
|
||||
arg_num=arg_num,
|
||||
arg_type=arg.datatype,
|
||||
arg_default=arg_default
|
||||
)
|
||||
|
||||
def _make_cand(self, tbl, do_alias, suggestion, arg_mode=None):
|
||||
"""Returns a Candidate namedtuple.
|
||||
|
||||
:param tbl is a SchemaObject
|
||||
:param arg_mode determines what type of arg list to suffix for functions.
|
||||
Possible values: call, signature
|
||||
|
||||
"""
|
||||
cased_tbl = self.case(tbl.name)
|
||||
if do_alias:
|
||||
alias = self.alias(cased_tbl, suggestion.table_refs)
|
||||
synonyms = (cased_tbl, generate_alias(cased_tbl))
|
||||
maybe_parens = '()' if tbl.function else ''
|
||||
maybe_alias = (' ' + alias) if do_alias else ''
|
||||
maybe_schema = (self.case(tbl.schema) + '.') if tbl.schema else ''
|
||||
item = maybe_schema + cased_tbl + maybe_parens + maybe_alias
|
||||
suffix = self._arg_list_cache[arg_mode][tbl.meta] if arg_mode else ''
|
||||
if arg_mode == 'call':
|
||||
display_suffix = self._arg_list_cache['call_display'][tbl.meta]
|
||||
elif arg_mode == 'signature':
|
||||
display_suffix = self._arg_list_cache['signature'][tbl.meta]
|
||||
else:
|
||||
display_suffix = ''
|
||||
item = maybe_schema + cased_tbl + suffix + maybe_alias
|
||||
display = maybe_schema + cased_tbl + display_suffix + maybe_alias
|
||||
prio2 = 0 if tbl.schema else 1
|
||||
return Candidate(item, synonyms=synonyms, prio2=prio2)
|
||||
return Candidate(item, synonyms=synonyms, prio2=prio2, display=display)
|
||||
|
||||
def get_table_matches(self, suggestion, word_before_cursor, alias=False):
|
||||
tables = self.populate_schema_objects(suggestion.schema, 'tables')
|
||||
@ -736,10 +843,12 @@ class PGCompleter(Completer):
|
||||
}
|
||||
|
||||
def populate_scoped_cols(self, scoped_tbls, local_tbls=()):
|
||||
""" Find all columns in a set of scoped_tables
|
||||
"""Find all columns in a set of scoped_tables.
|
||||
|
||||
:param scoped_tbls: list of TableReference namedtuples
|
||||
:param local_tbls: tuple(TableMetadata)
|
||||
:return: {TableReference:{colname:ColumnMetaData}}
|
||||
|
||||
"""
|
||||
ctes = dict((normalize_ref(t.name), t.columns) for t in local_tbls)
|
||||
columns = OrderedDict()
|
||||
@ -780,8 +889,10 @@ class PGCompleter(Completer):
|
||||
return columns
|
||||
|
||||
def _get_schemas(self, obj_typ, schema):
|
||||
""" Returns a list of schemas from which to suggest objects
|
||||
schema is the schema qualification input by the user (if any)
|
||||
"""Returns a list of schemas from which to suggest objects.
|
||||
|
||||
:param schema is the schema qualification input by the user (if any)
|
||||
|
||||
"""
|
||||
metadata = self.dbmetadata[obj_typ]
|
||||
if schema:
|
||||
@ -793,8 +904,10 @@ class PGCompleter(Completer):
|
||||
return None if parent or schema in self.search_path else schema
|
||||
|
||||
def populate_schema_objects(self, schema, obj_type):
|
||||
"""Returns a list of SchemaObjects representing tables or views
|
||||
schema is the schema qualification input by the user (if any)
|
||||
"""Returns a list of SchemaObjects representing tables or views.
|
||||
|
||||
:param schema is the schema qualification input by the user (if any)
|
||||
|
||||
"""
|
||||
|
||||
return [
|
||||
@ -807,11 +920,12 @@ class PGCompleter(Completer):
|
||||
]
|
||||
|
||||
def populate_functions(self, schema, filter_func):
|
||||
"""Returns a list of function SchemaObjects
|
||||
"""Returns a list of function SchemaObjects.
|
||||
|
||||
:param filter_func is a function that accepts a FunctionMetadata
|
||||
namedtuple and returns a boolean indicating whether that
|
||||
function should be kept or discarded
|
||||
|
||||
filter_func is a function that accepts a FunctionMetadata namedtuple
|
||||
and returns a boolean indicating whether that function should be
|
||||
kept or discarded
|
||||
"""
|
||||
|
||||
# Because of multiple dispatch, we can have multiple functions
|
||||
@ -821,7 +935,7 @@ class PGCompleter(Completer):
|
||||
SchemaObject(
|
||||
name=func,
|
||||
schema=(self._maybe_schema(schema=sch, parent=schema)),
|
||||
function=True
|
||||
meta=meta
|
||||
)
|
||||
for sch in self._get_schemas('functions', schema)
|
||||
for (func, metas) in self.dbmetadata['functions'][sch].items()
|
||||
|
@ -517,7 +517,8 @@ class PGExecute(object):
|
||||
prorettype::regtype::text return_type,
|
||||
p.proisagg is_aggregate,
|
||||
p.proiswindow is_window,
|
||||
p.proretset is_set_returning
|
||||
p.proretset is_set_returning,
|
||||
pg_get_expr(proargdefaults, 0) AS arg_defaults
|
||||
FROM pg_catalog.pg_proc p
|
||||
INNER JOIN pg_catalog.pg_namespace n
|
||||
ON n.oid = p.pronamespace
|
||||
@ -534,7 +535,8 @@ class PGExecute(object):
|
||||
prorettype::regtype::text,
|
||||
p.proisagg is_aggregate,
|
||||
false is_window,
|
||||
p.proretset is_set_returning
|
||||
p.proretset is_set_returning,
|
||||
NULL AS arg_defaults
|
||||
FROM pg_catalog.pg_proc p
|
||||
INNER JOIN pg_catalog.pg_namespace n
|
||||
ON n.oid = p.pronamespace
|
||||
@ -551,7 +553,8 @@ class PGExecute(object):
|
||||
'' ret_type,
|
||||
p.proisagg is_aggregate,
|
||||
false is_window,
|
||||
p.proretset is_set_returning
|
||||
p.proretset is_set_returning,
|
||||
NULL AS arg_defaults
|
||||
FROM pg_catalog.pg_proc p
|
||||
INNER JOIN pg_catalog.pg_namespace n
|
||||
ON n.oid = p.pronamespace
|
||||
@ -646,6 +649,9 @@ class PGExecute(object):
|
||||
UNION -- Schema names
|
||||
SELECT nspname
|
||||
FROM pg_catalog.pg_namespace
|
||||
UNION -- Parameter names
|
||||
SELECT unnest(proargnames)
|
||||
FROM pg_proc
|
||||
)
|
||||
SELECT Word
|
||||
FROM OrderWords
|
||||
|
@ -22,6 +22,15 @@ def completion(display_meta, text, pos=0):
|
||||
return Completion(text, start_position=pos, display_meta=display_meta)
|
||||
|
||||
|
||||
def function(text, pos=0, display=None):
|
||||
return Completion(
|
||||
text,
|
||||
display=display or text,
|
||||
start_position=pos,
|
||||
display_meta='function'
|
||||
)
|
||||
|
||||
|
||||
def get_result(completer, text, position=None):
|
||||
position = len(text) if position is None else position
|
||||
return completer.get_completions(
|
||||
@ -40,7 +49,6 @@ def result_set(completer, text, position=None):
|
||||
schema = partial(completion, 'schema')
|
||||
table = partial(completion, 'table')
|
||||
view = partial(completion, 'view')
|
||||
function = partial(completion, 'function')
|
||||
column = partial(completion, 'column')
|
||||
keyword = partial(completion, 'keyword')
|
||||
datatype = partial(completion, 'datatype')
|
||||
@ -93,8 +101,21 @@ class MetaData(object):
|
||||
|
||||
def functions(self, parent='public', pos=0):
|
||||
return [
|
||||
function(escape(x[0] + '()'), pos)
|
||||
for x in self.metadata.get('functions', {}).get(parent, [])]
|
||||
function(
|
||||
escape(x[0]) + '(' + ', '.join(
|
||||
arg_name + ' := '
|
||||
for (arg_name, arg_mode) in zip(x[1], x[3])
|
||||
if arg_mode in ('b', 'i')
|
||||
) + ')',
|
||||
pos,
|
||||
escape(x[0]) + '(' + ', '.join(
|
||||
arg_name
|
||||
for (arg_name, arg_mode) in zip(x[1], x[3])
|
||||
if arg_mode in ('b', 'i')
|
||||
) + ')'
|
||||
)
|
||||
for x in self.metadata.get('functions', {}).get(parent, [])
|
||||
]
|
||||
|
||||
def schemas(self, pos=0):
|
||||
schemas = set(sch for schs in self.metadata.values() for sch in schs)
|
||||
@ -191,7 +212,7 @@ class MetaData(object):
|
||||
view_cols.extend([(sch, tbl, col, 'text') for col in cols])
|
||||
|
||||
functions = [
|
||||
FunctionMetadata(sch, *func_meta)
|
||||
FunctionMetadata(sch, *func_meta, arg_defaults=None)
|
||||
for sch, funcs in metadata['functions'].items()
|
||||
for func_meta in funcs]
|
||||
|
||||
|
@ -2,12 +2,15 @@ from pgcli.packages.parseutils.meta import FunctionMetadata
|
||||
|
||||
|
||||
def test_function_metadata_eq():
|
||||
f1 = FunctionMetadata('s', 'f', ['x'], ['integer'], [], 'int', False,
|
||||
False, False)
|
||||
f2 = FunctionMetadata('s', 'f', ['x'], ['integer'], [], 'int', False,
|
||||
False, False)
|
||||
f3 = FunctionMetadata('s', 'g', ['x'], ['integer'], [], 'int', False,
|
||||
False, False)
|
||||
f1 = FunctionMetadata(
|
||||
's', 'f', ['x'], ['integer'], [], 'int', False, False, False, None
|
||||
)
|
||||
f2 = FunctionMetadata(
|
||||
's', 'f', ['x'], ['integer'], [], 'int', False, False, False, None
|
||||
)
|
||||
f3 = FunctionMetadata(
|
||||
's', 'g', ['x'], ['integer'], [], 'int', False, False, False, None
|
||||
)
|
||||
assert f1 == f2
|
||||
assert f1 != f3
|
||||
assert not (f1 != f2)
|
||||
|
@ -17,8 +17,8 @@ def test_ctor(refresher):
|
||||
"""
|
||||
assert len(refresher.refreshers) > 0
|
||||
actual_handlers = list(refresher.refreshers.keys())
|
||||
expected_handlers = ['schemata', 'tables', 'views', 'functions',
|
||||
'types', 'databases', 'casing']
|
||||
expected_handlers = ['schemata', 'tables', 'views',
|
||||
'types', 'databases', 'casing', 'functions']
|
||||
assert expected_handlers == actual_handlers
|
||||
|
||||
|
||||
|
@ -8,6 +8,16 @@ from textwrap import dedent
|
||||
from utils import run, dbtest, requires_json, requires_jsonb
|
||||
|
||||
|
||||
def function_meta_data(
|
||||
func_name, schema_name='public', arg_names=None, arg_types=None,
|
||||
arg_modes=None, return_type=None, is_aggregate=False, is_window=False,
|
||||
is_set_returning=False, arg_defaults=None
|
||||
):
|
||||
return FunctionMetadata(
|
||||
schema_name, func_name, arg_names, arg_types, arg_modes, return_type,
|
||||
is_aggregate, is_window, is_set_returning, arg_defaults
|
||||
)
|
||||
|
||||
@dbtest
|
||||
def test_conn(executor):
|
||||
run(executor, '''create table test(a text)''')
|
||||
@ -94,15 +104,32 @@ def test_functions_query(executor):
|
||||
|
||||
funcs = set(executor.functions())
|
||||
assert funcs >= set([
|
||||
FunctionMetadata('public', 'func1', None, [], [],
|
||||
'integer', False, False, False),
|
||||
FunctionMetadata('public', 'func3', ['x', 'y'],
|
||||
['integer', 'integer'], ['t', 't'], 'record', False, False, True),
|
||||
FunctionMetadata('public', 'func4', ('x',), ('integer',), [],
|
||||
'integer', False, False, True),
|
||||
FunctionMetadata('schema1', 'func2', None, [], [],
|
||||
'integer', False, False, False),
|
||||
])
|
||||
function_meta_data(
|
||||
func_name='func1',
|
||||
return_type='integer'
|
||||
),
|
||||
function_meta_data(
|
||||
func_name='func3',
|
||||
arg_names=['x', 'y'],
|
||||
arg_types=['integer', 'integer'],
|
||||
arg_modes=['t', 't'],
|
||||
return_type='record',
|
||||
is_set_returning=True
|
||||
),
|
||||
function_meta_data(
|
||||
schema_name='public',
|
||||
func_name='func4',
|
||||
arg_names=('x',),
|
||||
arg_types=('integer',),
|
||||
return_type='integer',
|
||||
is_set_returning=True
|
||||
),
|
||||
function_meta_data(
|
||||
schema_name='schema1',
|
||||
func_name='func2',
|
||||
return_type='integer'
|
||||
),
|
||||
])
|
||||
|
||||
|
||||
@dbtest
|
||||
|
@ -512,14 +512,20 @@ def test_join_alias_search_without_aliases2(completer):
|
||||
def test_function_alias_search_without_aliases(completer):
|
||||
text = 'SELECT blog.ees'
|
||||
result = get_result(completer, text)
|
||||
assert result[0] == function('extract_entry_symbols()', -3)
|
||||
first = result[0]
|
||||
assert first.start_position == -3
|
||||
assert first.text == 'extract_entry_symbols()'
|
||||
assert first.display == 'extract_entry_symbols(_entryid)'
|
||||
|
||||
|
||||
@parametrize('completer', completers())
|
||||
def test_function_alias_search_with_aliases(completer):
|
||||
text = 'SELECT blog.ee'
|
||||
result = get_result(completer, text)
|
||||
assert result[0] == function('enter_entry()', -2)
|
||||
first = result[0]
|
||||
assert first.start_position == -2
|
||||
assert first.text == 'enter_entry(_title := , _text := )'
|
||||
assert first.display == 'enter_entry(_title, _text)'
|
||||
|
||||
|
||||
@parametrize('completer',completers(filtr=True, casing=True, qualify=no_qual))
|
||||
|
@ -12,14 +12,16 @@ metadata = {
|
||||
'orders': ['id', 'ordered_date', 'status', 'email'],
|
||||
'select': ['id', 'insert', 'ABC']},
|
||||
'views': {
|
||||
'user_emails': ['id', 'email']},
|
||||
'user_emails': ['id', 'email'],
|
||||
'functions': ['function'],
|
||||
},
|
||||
'functions': [
|
||||
['custom_fun', [''], [''], [''], '', False, False, False],
|
||||
['_custom_fun', [''], [''], [''], '', False, False, False],
|
||||
['custom_func1', [''], [''], [''], '', False, False, False],
|
||||
['custom_func2', [''], [''], [''], '', False, False, False],
|
||||
['custom_fun', [], [], [], '', False, False, False],
|
||||
['_custom_fun', [], [], [], '', False, False, False],
|
||||
['custom_func1', [], [], [], '', False, False, False],
|
||||
['custom_func2', [], [], [], '', False, False, False],
|
||||
['set_returning_func', ['x', 'y'], ['integer', 'integer'],
|
||||
['o', 'o'], '', False, False, True]],
|
||||
['b', 'b'], '', False, False, True]],
|
||||
'datatypes': ['custom_type1', 'custom_type2'],
|
||||
'foreignkeys': [
|
||||
('public', 'users', 'id', 'public', 'users', 'parentid'),
|
||||
@ -33,28 +35,66 @@ testdata = MetaData(metadata)
|
||||
|
||||
cased_users_col_names = ['ID', 'PARENTID', 'Email', 'First_Name', 'last_name']
|
||||
cased_users2_col_names = ['UserID', 'UserName']
|
||||
cased_funcs = ['Custom_Fun', '_custom_fun', 'Custom_Func1',
|
||||
'custom_func2', 'set_returning_func']
|
||||
cased_funcs = [
|
||||
'Custom_Fun', '_custom_fun', 'Custom_Func1', 'custom_func2', 'set_returning_func'
|
||||
]
|
||||
cased_tbls = ['Users', 'Orders']
|
||||
cased_views = ['User_Emails']
|
||||
casing = (['SELECT', 'PUBLIC'] + cased_funcs + cased_tbls + cased_views
|
||||
+ cased_users_col_names + cased_users2_col_names)
|
||||
cased_views = ['User_Emails', 'Functions']
|
||||
casing = (
|
||||
['SELECT', 'PUBLIC'] + cased_funcs + cased_tbls + cased_views
|
||||
+ cased_users_col_names + cased_users2_col_names
|
||||
)
|
||||
# Lists for use in assertions
|
||||
cased_funcs = [function(f + '()') for f in cased_funcs]
|
||||
cased_funcs = [
|
||||
function(f) for f in ('Custom_Fun()', '_custom_fun()', 'Custom_Func1()', 'custom_func2()')
|
||||
] + [function('set_returning_func(x := , y := )', display='set_returning_func(x, y)')]
|
||||
cased_tbls = [table(t) for t in (cased_tbls + ['"Users"', '"select"'])]
|
||||
cased_rels = [view(t) for t in cased_views] + cased_funcs + cased_tbls
|
||||
cased_users_cols = [column(c) for c in cased_users_col_names]
|
||||
aliased_rels = [table(t) for t in ('users u', '"Users" U', 'orders o',
|
||||
'"select" s')] + [view('user_emails ue')] + [function(f) for f in (
|
||||
'_custom_fun() cf', 'custom_fun() cf', 'custom_func1() cf',
|
||||
'custom_func2() cf', 'set_returning_func() srf')]
|
||||
cased_aliased_rels = [table(t) for t in ('Users U', '"Users" U', 'Orders O',
|
||||
'"select" s')] + [view('User_Emails UE')] + [function(f) for f in (
|
||||
'_custom_fun() cf', 'Custom_Fun() CF', 'Custom_Func1() CF',
|
||||
'custom_func2() cf', 'set_returning_func() srf')]
|
||||
aliased_rels = [
|
||||
table(t) for t in ('users u', '"Users" U', 'orders o', '"select" s')
|
||||
] + [view('user_emails ue'), view('functions f')] + [
|
||||
function(f) for f in (
|
||||
'_custom_fun() cf', 'custom_fun() cf', 'custom_func1() cf',
|
||||
'custom_func2() cf'
|
||||
)
|
||||
] + [function(
|
||||
'set_returning_func(x := , y := ) srf',
|
||||
display='set_returning_func(x, y) srf'
|
||||
)]
|
||||
cased_aliased_rels = [
|
||||
table(t) for t in ('Users U', '"Users" U', 'Orders O', '"select" s')
|
||||
] + [view('User_Emails UE'), view('Functions F')] + [
|
||||
function(f) for f in (
|
||||
'_custom_fun() cf', 'Custom_Fun() CF', 'Custom_Func1() CF', 'custom_func2() cf'
|
||||
)
|
||||
] + [function(
|
||||
'set_returning_func(x := , y := ) srf',
|
||||
display='set_returning_func(x, y) srf'
|
||||
)]
|
||||
completers = testdata.get_completers(casing)
|
||||
|
||||
|
||||
# Just to make sure that this doesn't crash
|
||||
@parametrize('completer', completers())
|
||||
def test_function_column_name(completer):
|
||||
for l in range(
|
||||
len('SELECT * FROM Functions WHERE function:'),
|
||||
len('SELECT * FROM Functions WHERE function:text') + 1
|
||||
):
|
||||
assert [] == get_result(
|
||||
completer, 'SELECT * FROM Functions WHERE function:text'[:l]
|
||||
)
|
||||
|
||||
|
||||
@parametrize('text', ['ALTER FUNCTION set_ret', 'DROP FUNCTION set_ret'])
|
||||
@parametrize('completer', completers())
|
||||
def test_drop_alter_function(completer, text):
|
||||
assert get_result(completer, text) == [
|
||||
function('set_returning_func(x integer, y integer)', -len('set_ret'))
|
||||
]
|
||||
|
||||
|
||||
@parametrize('completer', completers())
|
||||
def test_empty_string_completion(completer):
|
||||
result = result_set(completer, '')
|
||||
@ -496,12 +536,13 @@ def test_table_names_after_from(completer, text):
|
||||
'"select"',
|
||||
'users',
|
||||
'"Users"',
|
||||
'functions',
|
||||
'user_emails',
|
||||
'_custom_fun()',
|
||||
'custom_fun()',
|
||||
'custom_func1()',
|
||||
'custom_func2()',
|
||||
'set_returning_func()',
|
||||
'set_returning_func(x := , y := )',
|
||||
]
|
||||
|
||||
|
||||
@ -747,11 +788,16 @@ def test_duplicate_table_aliases(completer, text):
|
||||
table('"Users" U'),
|
||||
table('"select" s'),
|
||||
view('user_emails ue'),
|
||||
view('functions f'),
|
||||
function('_custom_fun() cf'),
|
||||
function('custom_fun() cf'),
|
||||
function('custom_func1() cf'),
|
||||
function('custom_func2() cf'),
|
||||
function('set_returning_func() srf')])
|
||||
function(
|
||||
'set_returning_func(x := , y := ) srf',
|
||||
display='set_returning_func(x, y) srf'
|
||||
),
|
||||
])
|
||||
|
||||
|
||||
@parametrize('completer', completers(casing=True, aliasing=True))
|
||||
@ -765,11 +811,16 @@ def test_duplicate_aliases_with_casing(completer, text):
|
||||
table('"Users" U'),
|
||||
table('"select" s'),
|
||||
view('User_Emails UE'),
|
||||
view('Functions F'),
|
||||
function('_custom_fun() cf'),
|
||||
function('Custom_Fun() CF'),
|
||||
function('Custom_Func1() CF'),
|
||||
function('custom_func2() cf'),
|
||||
function('set_returning_func() srf')])
|
||||
function(
|
||||
'set_returning_func(x := , y := ) srf',
|
||||
display='set_returning_func(x, y) srf'
|
||||
),
|
||||
])
|
||||
|
||||
|
||||
@parametrize('completer', completers(casing=True, aliasing=True))
|
||||
|
Loading…
Reference in New Issue
Block a user