mirror of https://github.com/dbcli/pgcli
Merge branch 'master' into schema_autocomplete
This commit is contained in:
commit
7e7051fef7
34
DEVELOP.rst
34
DEVELOP.rst
|
@ -38,9 +38,14 @@ It is highly recommended to use virtualenv for development. If you don't know
|
|||
what a virtualenv is, this `guide <http://docs.python-guide.org/en/latest/dev/virtualenvs/#virtual-environments>`_
|
||||
will help you get started.
|
||||
|
||||
Create a virtualenv (let's call it pgcli-dev). Once the virtualenv is activated
|
||||
`cd` into the local clone of pgcli folder and install pgcli using pip as
|
||||
follows:
|
||||
Create a virtualenv (let's call it pgcli-dev). Activate it:
|
||||
|
||||
::
|
||||
|
||||
source ./pgcli-dev/bin/activate
|
||||
|
||||
Once the virtualenv is activated, `cd` into the local clone of pgcli folder
|
||||
and install pgcli using pip as follows:
|
||||
|
||||
::
|
||||
|
||||
|
@ -56,3 +61,26 @@ we've linked the pgcli installation with the working copy. So any changes made
|
|||
to the code is immediately available in the installed version of pgcli. This
|
||||
makes it easy to change something in the code, launch pgcli and check the
|
||||
effects of your change.
|
||||
|
||||
Adding PostgreSQL Special (Meta) Commands
|
||||
-----------------------------------------
|
||||
|
||||
If you want to work on adding new meta-commands (such as `\dp`, `\ds`, `dy`),
|
||||
you'll be changing the code of `packages/pgspecial.py`. Search for the
|
||||
dictionary called `CASE_SENSITIVE_COMMANDS`. The special command us used as
|
||||
the dictionary key, and the value is a tuple.
|
||||
|
||||
The first item in the tuple is either a string (sql statement) or a function.
|
||||
The second item in the tuple is a list of strings which is the documentation
|
||||
for that special command. The list will have two items, the first item is the
|
||||
command itself with possible options and the second item is the plain english
|
||||
description of that command.
|
||||
|
||||
For example, `\l` is a meta-command that lists all the databases. The way you
|
||||
can see the SQL statement issued by PostgreSQL when this command is executed
|
||||
is to launch `psql -E` and entering `\l`.
|
||||
|
||||
That will print the results and also print the sql statement that was executed
|
||||
to produce that result. In most cases it's a single sql statement, but sometimes
|
||||
it's a series of sql statements that feed the results to each other to get to
|
||||
the final result.
|
|
@ -83,7 +83,8 @@ Detailed Installation Instructions:
|
|||
OS X:
|
||||
=====
|
||||
|
||||
Easiest way to install pgcli is using brew.
|
||||
Easiest way to install pgcli is using brew. Please be aware that this will
|
||||
install postgres via brew if it wasn't installed via brew.
|
||||
|
||||
::
|
||||
|
||||
|
@ -91,6 +92,10 @@ Easiest way to install pgcli is using brew.
|
|||
|
||||
Done!
|
||||
|
||||
If you have postgres installed via a different means (such as PostgresApp), you
|
||||
can ``brew install --build-from-source pgcli`` which will skip installing
|
||||
postgres via brew if postgres is available in the path.
|
||||
|
||||
Alternatively, you can install ``pgcli`` as a python package using a package
|
||||
manager called called ``pip``. You will need postgres installed on your system
|
||||
for this to work. Check if ``pip`` is installed on the system.
|
||||
|
|
2
TODO
2
TODO
|
@ -6,7 +6,7 @@
|
|||
* [ ] Refactor to sqlcompletion to consume the text from left to right and use a state machine to suggest cols or tables instead of relying on hacks.
|
||||
* [ ] Add a few more special commands. (\l pattern, \dp, \ds, \dy, \z etc)
|
||||
* [ ] Refactor pgspecial.py to a class.
|
||||
* [ ] Write a doc about how to add new pgspecial commands.(psql -E)
|
||||
* [X] Write a doc about how to add new pgspecial commands.(psql -E)
|
||||
* [ ] Show/hide docs for a statement using a keybinding.
|
||||
* [ ] Check how to add the name of the table before printing the table.
|
||||
* [ ] Add a new trigger for M-/ that does naive completion.
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
current
|
||||
=======
|
||||
|
||||
Features:
|
||||
---------
|
||||
* Add alias completion support to ON keyword. (Thanks: `Iryna Cherniavska`_)
|
||||
* Add LIMIT keyword to completion.
|
||||
* Confirm before printing large tables.
|
||||
|
||||
Bug Fixes:
|
||||
----------
|
||||
|
||||
* Performance improvements to expanded view display (\x).
|
||||
* Cast bytea files to text while displaying. (Thanks: `Daniel Rocco`_)
|
||||
* Added a list of reserved words that should be auto-escaped.
|
||||
|
||||
0.13.0
|
||||
======
|
||||
|
||||
|
@ -63,3 +79,5 @@ Improvements:
|
|||
* Integration tests with Postgres!! (Thanks: https://github.com/macobo)
|
||||
|
||||
.. _darikg: https://github.com/darikg
|
||||
.. _`Iryna Cherniavska`: https://github.com/j-bennet
|
||||
.. _`Daniel Rocco`: https://github.com/drocco007
|
||||
|
|
|
@ -8,6 +8,7 @@ import logging
|
|||
import click
|
||||
|
||||
from prompt_toolkit import CommandLineInterface, AbortAction, Exit
|
||||
from prompt_toolkit.document import Document
|
||||
from prompt_toolkit.layout import Layout
|
||||
from prompt_toolkit.layout.prompt import DefaultPrompt
|
||||
from prompt_toolkit.layout.menus import CompletionsMenu
|
||||
|
@ -56,7 +57,6 @@ class PGCli(object):
|
|||
|
||||
# Load config.
|
||||
c = self.config = load_config('~/.pgclirc', default_config)
|
||||
self.smart_completion = c.getboolean('main', 'smart_completion')
|
||||
self.multi_line = c.getboolean('main', 'multi_line')
|
||||
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
@ -64,6 +64,13 @@ class PGCli(object):
|
|||
|
||||
self.query_history = []
|
||||
|
||||
# Initialize completer
|
||||
smart_completion = c.getboolean('main', 'smart_completion')
|
||||
completer = PGCompleter(smart_completion)
|
||||
completer.extend_special_commands(CASE_SENSITIVE_COMMANDS.keys())
|
||||
completer.extend_special_commands(NON_CASE_SENSITIVE_COMMANDS.keys())
|
||||
self.completer = completer
|
||||
|
||||
def initialize_logging(self):
|
||||
|
||||
log_file = self.config.get('main', 'log_file')
|
||||
|
@ -152,10 +159,9 @@ class PGCli(object):
|
|||
menus=[CompletionsMenu(max_height=10)],
|
||||
lexer=SqlLexer, bottom_toolbars=[PGToolbar()])
|
||||
|
||||
completer = PGCompleter(self.smart_completion)
|
||||
completer.extend_special_commands(CASE_SENSITIVE_COMMANDS.keys())
|
||||
completer.extend_special_commands(NON_CASE_SENSITIVE_COMMANDS.keys())
|
||||
refresh_completions(pgexecute, completer)
|
||||
completer = self.completer
|
||||
self.refresh_completions()
|
||||
|
||||
buf = PGBuffer(always_multiline=self.multi_line, completer=completer,
|
||||
history=FileHistory(os.path.expanduser('~/.pgcli-history')))
|
||||
cli = CommandLineInterface(style=PGStyle, layout=layout, buffer=buf,
|
||||
|
@ -197,11 +203,11 @@ class PGCli(object):
|
|||
logger.debug("status: %r", status)
|
||||
start = time()
|
||||
threshold = 1000
|
||||
if is_select(status) and cur.rowcount > threshold:
|
||||
if (is_select(status) and
|
||||
cur and cur.rowcount > threshold):
|
||||
click.secho('The result set has more than %s rows.'
|
||||
% threshold, fg='red')
|
||||
if not click.confirm('Do you want to continue?',
|
||||
default=True):
|
||||
if not click.confirm('Do you want to continue?'):
|
||||
click.secho("Aborted!", err=True, fg='red')
|
||||
break
|
||||
output.extend(format_output(cur, headers, status))
|
||||
|
@ -231,8 +237,7 @@ class PGCli(object):
|
|||
# Refresh the table names and column names if necessary.
|
||||
if need_completion_refresh(document.text):
|
||||
prompt = '%s> ' % pgexecute.dbname
|
||||
completer.reset_completions()
|
||||
refresh_completions(pgexecute, completer)
|
||||
self.refresh_completions()
|
||||
|
||||
query = Query(document.text, successful, mutating)
|
||||
self.query_history.append(query)
|
||||
|
@ -256,6 +261,22 @@ class PGCli(object):
|
|||
|
||||
return less_opts
|
||||
|
||||
def refresh_completions(self):
|
||||
completer = self.completer
|
||||
completer.reset_completions()
|
||||
|
||||
pgexecute = self.pgexecute
|
||||
|
||||
schemata, tables, columns = pgexecute.get_metadata()
|
||||
completer.extend_schemata(schemata)
|
||||
completer.extend_tables(tables)
|
||||
completer.extend_columns(columns)
|
||||
completer.extend_database_names(pgexecute.databases())
|
||||
|
||||
def get_completions(self, text, cursor_positition):
|
||||
return self.completer.get_completions(
|
||||
Document(text=text, cursor_position=cursor_positition), None)
|
||||
|
||||
@click.command()
|
||||
# Default host is '' so psycopg2 can default to either localhost or unix socket
|
||||
@click.option('-h', '--host', default='', envvar='PGHOST',
|
||||
|
@ -332,10 +353,11 @@ def quit_command(sql):
|
|||
or sql.strip() == ':q')
|
||||
|
||||
def refresh_completions(pgexecute, completer):
|
||||
schemata, tables, columns = pgexecute.get_metadata()
|
||||
completer.extend_schemata(schemata)
|
||||
completer.extend_tables(tables)
|
||||
completer.extend_columns(columns)
|
||||
tables, columns = pgexecute.tables()
|
||||
completer.extend_table_names(tables)
|
||||
for table in tables:
|
||||
table = table[1:-1] if table[0] == '"' and table[-1] == '"' else table
|
||||
completer.extend_column_names(table, columns[table])
|
||||
completer.extend_database_names(pgexecute.databases())
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -46,12 +46,6 @@ def suggest_type(full_text, text_before_cursor):
|
|||
return suggest_based_on_last_token(last_token, text_before_cursor, full_text)
|
||||
|
||||
def suggest_based_on_last_token(token, text_before_cursor, full_text):
|
||||
"""Returns a list of suggestion dicts
|
||||
|
||||
A suggestion dict is a dict with a mandatory field "type", and optional
|
||||
additional fields supplying additional scope information.
|
||||
"""
|
||||
|
||||
if isinstance(token, string_types):
|
||||
token_v = token
|
||||
else:
|
||||
|
@ -95,7 +89,7 @@ def suggest_based_on_last_token(token, text_before_cursor, full_text):
|
|||
return [{'type': 'schema'}, {'type': 'table', 'schema': []}]
|
||||
elif token_v.lower() in ('c', 'use'): # \c
|
||||
return [{'type': 'database'}]
|
||||
elif token_v.endswith(','):
|
||||
elif token_v.endswith(',') or token_v == '=':
|
||||
prev_keyword = find_prev_keyword(text_before_cursor)
|
||||
if prev_keyword:
|
||||
return suggest_based_on_last_token(
|
||||
|
|
|
@ -13,45 +13,15 @@ _logger = logging.getLogger(__name__)
|
|||
psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
|
||||
psycopg2.extensions.register_type(psycopg2.extensions.UNICODEARRAY)
|
||||
|
||||
# Cast bytea fields to text. By default, this will render as hex strings with
|
||||
# Postgres 9+ and as escaped binary in earlier versions.
|
||||
psycopg2.extensions.register_type(
|
||||
psycopg2.extensions.new_type((17,), 'BYTEA_TEXT', psycopg2.STRING))
|
||||
|
||||
# When running a query, make pressing CTRL+C raise a KeyboardInterrupt
|
||||
# See http://initd.org/psycopg/articles/2014/07/20/cancelling-postgresql-statements-python/
|
||||
psycopg2.extensions.set_wait_callback(psycopg2.extras.wait_select)
|
||||
|
||||
def _parse_dsn(dsn, default_user, default_password, default_host,
|
||||
default_port):
|
||||
"""
|
||||
This function parses a postgres url to get the different components.
|
||||
|
||||
"""
|
||||
|
||||
user = password = host = port = dbname = None
|
||||
|
||||
if dsn.startswith('postgres://'): # Check if the string is a database url.
|
||||
dsn = dsn[len('postgres://'):]
|
||||
elif dsn.startswith('postgresql://'):
|
||||
dsn = dsn[len('postgresql://'):]
|
||||
|
||||
if '/' in dsn:
|
||||
host, dbname = dsn.split('/', 1)
|
||||
if '@' in host:
|
||||
user, _, host = host.partition('@')
|
||||
if ':' in host:
|
||||
host, _, port = host.partition(':')
|
||||
if user and ':' in user:
|
||||
user, _, password = user.partition(':')
|
||||
|
||||
user = user or default_user
|
||||
password = password or default_password
|
||||
host = host or default_host
|
||||
port = port or default_port
|
||||
dbname = dbname or dsn
|
||||
|
||||
_logger.debug('Parsed connection params:'
|
||||
'dbname: %r, user: %r, password: %r, host: %r, port: %r',
|
||||
dbname, user, password, host, port)
|
||||
|
||||
return (dbname, user, password, host, port)
|
||||
|
||||
class PGExecute(object):
|
||||
|
||||
schemata_query = '''
|
||||
|
@ -99,10 +69,11 @@ class PGExecute(object):
|
|||
ORDER BY 1;"""
|
||||
|
||||
def __init__(self, database, user, password, host, port):
|
||||
(self.dbname, self.user, self.password, self.host, self.port) = \
|
||||
_parse_dsn(database, default_user=user,
|
||||
default_password=password, default_host=host,
|
||||
default_port=port)
|
||||
self.dbname = database
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.connect()
|
||||
|
||||
def connect(self, database=None, user=None, password=None, host=None,
|
||||
|
|
|
@ -1,47 +1,8 @@
|
|||
# coding=UTF-8
|
||||
|
||||
from pgcli.pgexecute import _parse_dsn
|
||||
from textwrap import dedent
|
||||
from utils import *
|
||||
|
||||
def test__parse_dsn():
|
||||
test_cases = [
|
||||
# Full dsn with all components.
|
||||
('postgres://user:password@host:5432/dbname',
|
||||
('dbname', 'user', 'password', 'host', '5432')),
|
||||
|
||||
# dsn without password.
|
||||
('postgres://user@host:5432/dbname',
|
||||
('dbname', 'user', 'fpasswd', 'host', '5432')),
|
||||
|
||||
# dsn without user or password.
|
||||
('postgres://localhost:5432/dbname',
|
||||
('dbname', 'fuser', 'fpasswd', 'localhost', '5432')),
|
||||
|
||||
# dsn without port.
|
||||
('postgres://user:password@host/dbname',
|
||||
('dbname', 'user', 'password', 'host', '1234')),
|
||||
|
||||
# dsn without password and port.
|
||||
('postgres://user@host/dbname',
|
||||
('dbname', 'user', 'fpasswd', 'host', '1234')),
|
||||
|
||||
# dsn without user, password, port.
|
||||
('postgres://localhost/dbname',
|
||||
('dbname', 'fuser', 'fpasswd', 'localhost', '1234')),
|
||||
|
||||
# dsn without user, password, port or host.
|
||||
('postgres:///dbname',
|
||||
('dbname', 'fuser', 'fpasswd', 'fhost', '1234')),
|
||||
|
||||
# Full dsn with all components but with postgresql:// prefix.
|
||||
('postgresql://user:password@host:5432/dbname',
|
||||
('dbname', 'user', 'password', 'host', '5432')),
|
||||
]
|
||||
|
||||
for dsn, expected in test_cases:
|
||||
assert _parse_dsn(dsn, 'fuser', 'fpasswd', 'fhost', '1234') == expected
|
||||
|
||||
@dbtest
|
||||
def test_conn(executor):
|
||||
run(executor, '''create table test(a text)''')
|
||||
|
@ -123,4 +84,13 @@ def test_multiple_queries_same_line_syntaxerror(executor):
|
|||
|
||||
@dbtest
|
||||
def test_special_command(executor):
|
||||
run(executor, '\\?')
|
||||
run(executor, '\\?')
|
||||
|
||||
|
||||
@dbtest
|
||||
def test_bytea_field_support_in_output(executor):
|
||||
run(executor, "create table binarydata(c bytea)")
|
||||
run(executor,
|
||||
"insert into binarydata (c) values (decode('DEADBEEF', 'hex'))")
|
||||
|
||||
assert u'\\xdeadbeef' in run(executor, "select * from binarydata", join=True)
|
||||
|
|
|
@ -260,6 +260,16 @@ def test_suggested_aliases_after_on(completer, complete_event):
|
|||
Completion(text='u', start_position=0),
|
||||
Completion(text='o', start_position=0)])
|
||||
|
||||
def test_suggested_aliases_after_on_right_side(completer, complete_event):
|
||||
text = 'SELECT u.name, o.id FROM users u JOIN orders o ON o.user_id = '
|
||||
position = len('SELECT u.name, o.id FROM users u JOIN orders o ON o.user_id = ')
|
||||
result = set(completer.get_completions(
|
||||
Document(text=text, cursor_position=position),
|
||||
complete_event))
|
||||
assert set(result) == set([
|
||||
Completion(text='u', start_position=0),
|
||||
Completion(text='o', start_position=0)])
|
||||
|
||||
def test_suggested_tables_after_on(completer, complete_event):
|
||||
text = 'SELECT users.name, orders.id FROM users JOIN orders ON '
|
||||
position = len('SELECT users.name, orders.id FROM users JOIN orders ON ')
|
||||
|
@ -270,6 +280,16 @@ def test_suggested_tables_after_on(completer, complete_event):
|
|||
Completion(text='users', start_position=0),
|
||||
Completion(text='orders', start_position=0)])
|
||||
|
||||
def test_suggested_tables_after_on_right_side(completer, complete_event):
|
||||
text = 'SELECT users.name, orders.id FROM users JOIN orders ON orders.user_id = '
|
||||
position = len('SELECT users.name, orders.id FROM users JOIN orders ON orders.user_id = ')
|
||||
result = set(completer.get_completions(
|
||||
Document(text=text, cursor_position=position),
|
||||
complete_event))
|
||||
assert set(result) == set([
|
||||
Completion(text='users', start_position=0),
|
||||
Completion(text='orders', start_position=0)])
|
||||
|
||||
def test_table_names_after_from(completer, complete_event):
|
||||
text = 'SELECT * FROM '
|
||||
position = len('SELECT * FROM ')
|
||||
|
|
|
@ -224,4 +224,18 @@ def test_on_suggests_tables():
|
|||
'select abc.x, bcd.y from abc join bcd on ',
|
||||
'select abc.x, bcd.y from abc join bcd on ')
|
||||
assert_equals(suggestions,
|
||||
[{'type': 'alias', 'aliases': ['abc', 'bcd']}])
|
||||
[{'type': 'alias', 'aliases': ['abc', 'bcd']}])
|
||||
|
||||
def test_on_suggests_aliases_right_side():
|
||||
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 = ')
|
||||
assert_equals(suggestions,
|
||||
[{'type': 'alias', 'aliases': ['a', 'b']}])
|
||||
|
||||
def test_on_suggests_tables_right_side():
|
||||
suggestions = suggest_type(
|
||||
'select abc.x, bcd.y from abc join bcd on ',
|
||||
'select abc.x, bcd.y from abc join bcd on ')
|
||||
assert_equals(suggestions,
|
||||
[{'type': 'alias', 'aliases': ['abc', 'bcd']}])
|
||||
|
|
Loading…
Reference in New Issue