1
0
Fork 0

Merge branch 'master' into schema_autocomplete

This commit is contained in:
Darik Gamble 2015-01-24 14:29:33 -05:00
commit 7e7051fef7
10 changed files with 148 additions and 106 deletions

View File

@ -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.

View File

@ -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
View File

@ -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.

View File

@ -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

View File

@ -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__":

View File

@ -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(

View File

@ -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,

View File

@ -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)

View File

@ -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 ')

View File

@ -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']}])