2014-11-21 07:15:50 +00:00
|
|
|
from __future__ import print_function
|
2018-10-31 17:42:27 +00:00
|
|
|
from __future__ import unicode_literals
|
2014-10-12 22:07:34 +00:00
|
|
|
|
2018-06-01 21:00:57 +00:00
|
|
|
import warnings
|
2018-10-31 17:42:27 +00:00
|
|
|
|
|
|
|
from pgspecial.namedqueries import NamedQueries
|
|
|
|
|
2018-06-01 21:00:57 +00:00
|
|
|
warnings.filterwarnings("ignore", category=UserWarning, module='psycopg2')
|
|
|
|
|
2014-12-20 07:12:43 +00:00
|
|
|
import os
|
2015-10-18 20:55:56 +00:00
|
|
|
import re
|
2015-02-14 01:02:24 +00:00
|
|
|
import sys
|
2015-01-06 21:52:01 +00:00
|
|
|
import traceback
|
2015-01-04 08:31:17 +00:00
|
|
|
import logging
|
2015-08-28 06:10:48 +00:00
|
|
|
import threading
|
2015-09-20 21:57:12 +00:00
|
|
|
import shutil
|
2015-11-01 22:36:55 +00:00
|
|
|
import functools
|
2015-11-24 20:56:31 +00:00
|
|
|
import humanize
|
2017-05-09 23:20:08 +00:00
|
|
|
import datetime as dt
|
2017-08-18 18:40:16 +00:00
|
|
|
import itertools
|
2016-08-03 16:57:23 +00:00
|
|
|
from time import time, sleep
|
2015-08-17 04:34:27 +00:00
|
|
|
from codecs import open
|
2014-10-12 17:31:54 +00:00
|
|
|
|
2015-11-01 22:36:55 +00:00
|
|
|
|
2017-06-12 02:07:40 +00:00
|
|
|
from cli_helpers.tabular_output import TabularOutputFormatter
|
2017-06-22 18:44:27 +00:00
|
|
|
from cli_helpers.tabular_output.preprocessors import (align_decimals,
|
|
|
|
format_numbers)
|
2015-01-31 21:46:32 +00:00
|
|
|
import click
|
2015-10-23 09:49:20 +00:00
|
|
|
try:
|
|
|
|
import setproctitle
|
|
|
|
except ImportError:
|
|
|
|
setproctitle = None
|
2018-12-20 18:07:50 +00:00
|
|
|
from prompt_toolkit.completion import DynamicCompleter, ThreadedCompleter
|
2016-05-05 18:10:59 +00:00
|
|
|
from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
|
2018-09-28 21:18:40 +00:00
|
|
|
from prompt_toolkit.shortcuts import PromptSession, CompleteStyle
|
2015-01-24 03:19:07 +00:00
|
|
|
from prompt_toolkit.document import Document
|
2018-09-28 21:18:40 +00:00
|
|
|
from prompt_toolkit.filters import HasFocus, IsDone
|
|
|
|
from prompt_toolkit.lexers import PygmentsLexer
|
2015-06-04 23:55:49 +00:00
|
|
|
from prompt_toolkit.layout.processors import (ConditionalProcessor,
|
2018-07-06 17:02:52 +00:00
|
|
|
HighlightMatchingBracketProcessor,
|
|
|
|
TabsProcessor)
|
2014-11-23 23:31:34 +00:00
|
|
|
from prompt_toolkit.history import FileHistory
|
2017-07-16 22:06:25 +00:00
|
|
|
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
2015-03-27 04:20:20 +00:00
|
|
|
from pygments.lexers.sql import PostgresLexer
|
2014-12-05 16:56:59 +00:00
|
|
|
|
2018-05-15 17:42:21 +00:00
|
|
|
from pgspecial.main import (PGSpecial, NO_QUERY, PAGER_OFF, PAGER_LONG_OUTPUT)
|
2015-09-21 23:52:36 +00:00
|
|
|
import pgspecial as special
|
2018-07-22 05:34:56 +00:00
|
|
|
try:
|
|
|
|
import keyring
|
|
|
|
except ImportError:
|
|
|
|
keyring = None
|
2014-12-05 16:56:59 +00:00
|
|
|
from .pgcompleter import PGCompleter
|
2015-05-02 15:01:57 +00:00
|
|
|
from .pgtoolbar import create_toolbar_tokens_func
|
2018-03-04 10:41:10 +00:00
|
|
|
from .pgstyle import style_factory, style_factory_output
|
2015-10-21 20:56:11 +00:00
|
|
|
from .pgexecute import PGExecute
|
2018-09-28 21:18:40 +00:00
|
|
|
from .pgbuffer import pg_is_multiline
|
2015-09-04 05:37:31 +00:00
|
|
|
from .completion_refresher import CompletionRefresher
|
2016-05-20 10:06:09 +00:00
|
|
|
from .config import (get_casing_file,
|
|
|
|
load_config, config_location, ensure_dir_exists, get_config)
|
2014-12-11 08:26:32 +00:00
|
|
|
from .key_bindings import pgcli_bindings
|
2015-01-27 07:07:10 +00:00
|
|
|
from .encodingutils import utf8tounicode
|
2017-08-06 23:16:56 +00:00
|
|
|
from .encodingutils import text_type
|
2018-05-17 09:54:50 +00:00
|
|
|
from .packages.prompt_utils import confirm_destructive_query
|
2015-03-01 05:37:59 +00:00
|
|
|
from .__init__ import __version__
|
2014-10-12 17:31:54 +00:00
|
|
|
|
2015-08-18 16:11:22 +00:00
|
|
|
click.disable_unicode_literals_warning = True
|
2015-01-16 07:41:43 +00:00
|
|
|
|
2015-01-09 08:24:17 +00:00
|
|
|
try:
|
2017-05-25 10:39:01 +00:00
|
|
|
from urlparse import urlparse, unquote, parse_qs
|
2015-01-09 08:24:17 +00:00
|
|
|
except ImportError:
|
2017-05-25 10:39:01 +00:00
|
|
|
from urllib.parse import urlparse, unquote, parse_qs
|
2015-08-18 16:11:22 +00:00
|
|
|
|
2015-01-07 21:50:48 +00:00
|
|
|
from getpass import getuser
|
2017-07-11 08:54:24 +00:00
|
|
|
from psycopg2 import OperationalError, InterfaceError
|
2017-08-19 06:48:41 +00:00
|
|
|
import psycopg2
|
2015-01-07 21:50:48 +00:00
|
|
|
|
2015-01-16 07:41:43 +00:00
|
|
|
from collections import namedtuple
|
2015-01-09 00:44:24 +00:00
|
|
|
|
2018-07-22 05:34:56 +00:00
|
|
|
from textwrap import dedent
|
2017-08-06 22:53:36 +00:00
|
|
|
|
2018-05-15 17:42:21 +00:00
|
|
|
# Ref: https://stackoverflow.com/questions/30425105/filter-special-chars-such-as-color-codes-from-shell-output
|
|
|
|
COLOR_CODE_REGEX = re.compile(r'\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))')
|
|
|
|
|
2015-01-16 07:41:43 +00:00
|
|
|
# Query tuples are used for maintaining history
|
2015-10-28 13:23:23 +00:00
|
|
|
MetaQuery = namedtuple(
|
|
|
|
'Query',
|
|
|
|
[
|
|
|
|
'query', # The entire text of the command
|
|
|
|
'successful', # True If all subqueries were successful
|
2018-12-02 02:23:50 +00:00
|
|
|
'total_time', # Time elapsed executing the query and formatting results
|
|
|
|
'execution_time', # Time elapsed executing the query
|
2015-10-28 13:23:23 +00:00
|
|
|
'meta_changed', # True if any subquery executed create/alter/drop
|
|
|
|
'db_changed', # True if any subquery changed the database
|
|
|
|
'path_changed', # True if any subquery changed the search path
|
|
|
|
'mutated', # True if any subquery executed insert/update/delete
|
2018-05-15 05:25:33 +00:00
|
|
|
'is_special', # True if the query is a special command
|
2015-10-28 13:23:23 +00:00
|
|
|
])
|
2018-12-02 02:23:50 +00:00
|
|
|
MetaQuery.__new__.__defaults__ = ('', False, 0, 0, False, False, False, False)
|
2015-08-06 05:36:45 +00:00
|
|
|
|
2017-01-16 04:27:19 +00:00
|
|
|
OutputSettings = namedtuple(
|
|
|
|
'OutputSettings',
|
2018-01-11 20:47:56 +00:00
|
|
|
'table_format dcmlfmt floatfmt missingval expanded max_width case_function style_output'
|
2017-01-16 04:27:19 +00:00
|
|
|
)
|
|
|
|
OutputSettings.__new__.__defaults__ = (
|
2018-01-11 20:47:56 +00:00
|
|
|
None, None, None, '<null>', False, None, lambda x: x, None
|
2017-01-16 04:27:19 +00:00
|
|
|
)
|
2016-06-19 21:16:11 +00:00
|
|
|
|
2017-06-09 18:13:40 +00:00
|
|
|
|
2018-05-14 16:14:14 +00:00
|
|
|
class PgCliQuitError(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
class PGCli(object):
|
2015-08-06 05:36:45 +00:00
|
|
|
|
2016-07-16 20:46:52 +00:00
|
|
|
default_prompt = '\\u@\\h:\\d> '
|
2017-02-08 09:32:01 +00:00
|
|
|
max_len_prompt = 30
|
2016-07-16 20:46:52 +00:00
|
|
|
|
2016-02-02 16:28:10 +00:00
|
|
|
def set_default_pager(self, config):
|
|
|
|
configured_pager = config['main'].get('pager')
|
|
|
|
os_environ_pager = os.environ.get('PAGER')
|
|
|
|
|
|
|
|
if configured_pager:
|
2017-06-09 18:32:06 +00:00
|
|
|
self.logger.info(
|
|
|
|
'Default pager found in config file: "{}"'.format(configured_pager))
|
2016-02-02 16:28:10 +00:00
|
|
|
os.environ['PAGER'] = configured_pager
|
2016-06-02 22:04:50 +00:00
|
|
|
elif os_environ_pager:
|
2017-06-09 18:13:40 +00:00
|
|
|
self.logger.info('Default pager found in PAGER environment variable: "{}"'.format(
|
|
|
|
os_environ_pager))
|
2016-02-02 16:28:10 +00:00
|
|
|
os.environ['PAGER'] = os_environ_pager
|
|
|
|
else:
|
2017-06-09 18:32:06 +00:00
|
|
|
self.logger.info(
|
|
|
|
'No default pager found in environment. Using os default pager')
|
2017-06-09 18:13:40 +00:00
|
|
|
|
|
|
|
# Set default set of less recommended options, if they are not already set.
|
|
|
|
# They are ignored if pager is different than less.
|
|
|
|
if not os.environ.get('LESS'):
|
|
|
|
os.environ['LESS'] = '-SRXF'
|
2016-02-02 16:28:10 +00:00
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
def __init__(self, force_passwd_prompt=False, never_passwd_prompt=False,
|
2016-07-21 09:50:21 +00:00
|
|
|
pgexecute=None, pgclirc_file=None, row_limit=None,
|
2017-11-17 05:33:28 +00:00
|
|
|
single_connection=False, less_chatty=None, prompt=None, prompt_dsn=None,
|
2018-05-17 09:54:50 +00:00
|
|
|
auto_vertical_output=False, warn=None):
|
2015-01-09 00:27:28 +00:00
|
|
|
|
|
|
|
self.force_passwd_prompt = force_passwd_prompt
|
|
|
|
self.never_passwd_prompt = never_passwd_prompt
|
|
|
|
self.pgexecute = pgexecute
|
2017-12-11 03:47:33 +00:00
|
|
|
self.dsn_alias = None
|
2018-07-20 01:21:33 +00:00
|
|
|
self.watch_command = None
|
2015-01-09 00:27:28 +00:00
|
|
|
|
|
|
|
# Load config.
|
2016-05-20 10:06:09 +00:00
|
|
|
c = self.config = get_config(pgclirc_file)
|
2016-02-02 16:28:10 +00:00
|
|
|
|
2018-10-31 17:42:27 +00:00
|
|
|
NamedQueries.instance = NamedQueries.from_config(self.config)
|
|
|
|
|
2016-02-02 16:28:10 +00:00
|
|
|
self.logger = logging.getLogger(__name__)
|
|
|
|
self.initialize_logging()
|
|
|
|
|
|
|
|
self.set_default_pager(c)
|
2016-04-05 14:52:56 +00:00
|
|
|
self.output_file = None
|
2016-02-02 16:28:10 +00:00
|
|
|
self.pgspecial = PGSpecial()
|
|
|
|
|
2015-05-28 11:59:18 +00:00
|
|
|
self.multi_line = c['main'].as_bool('multi_line')
|
2016-09-04 19:48:10 +00:00
|
|
|
self.multiline_mode = c['main'].get('multi_line_mode', 'psql')
|
2015-05-28 11:59:18 +00:00
|
|
|
self.vi_mode = c['main'].as_bool('vi')
|
2017-09-05 18:39:13 +00:00
|
|
|
self.auto_expand = auto_vertical_output or c['main'].as_bool(
|
|
|
|
'auto_expand')
|
2017-01-21 17:51:12 +00:00
|
|
|
self.expanded_output = c['main'].as_bool('expand')
|
2015-06-30 21:59:02 +00:00
|
|
|
self.pgspecial.timing_enabled = c['main'].as_bool('timing')
|
2016-06-02 22:07:40 +00:00
|
|
|
if row_limit is not None:
|
|
|
|
self.row_limit = row_limit
|
|
|
|
else:
|
|
|
|
self.row_limit = c['main'].as_int('row_limit')
|
2016-02-02 16:28:10 +00:00
|
|
|
|
2017-02-25 14:13:41 +00:00
|
|
|
self.min_num_menu_lines = c['main'].as_int('min_num_menu_lines')
|
2017-03-30 10:12:25 +00:00
|
|
|
self.multiline_continuation_char = c['main']['multiline_continuation_char']
|
2015-05-28 11:59:18 +00:00
|
|
|
self.table_format = c['main']['table_format']
|
|
|
|
self.syntax_style = c['main']['syntax_style']
|
2015-08-07 04:14:49 +00:00
|
|
|
self.cli_style = c['colors']
|
2015-08-02 23:25:05 +00:00
|
|
|
self.wider_completion_menu = c['main'].as_bool('wider_completion_menu')
|
2018-05-17 09:54:50 +00:00
|
|
|
c_dest_warning = c['main'].as_bool('destructive_warning')
|
|
|
|
self.destructive_warning = c_dest_warning if warn is None else warn
|
2017-03-11 08:31:02 +00:00
|
|
|
self.less_chatty = bool(less_chatty) or c['main'].as_bool('less_chatty')
|
2016-07-30 00:52:17 +00:00
|
|
|
self.null_string = c['main'].get('null_string', '<null>')
|
2016-11-15 01:19:28 +00:00
|
|
|
self.prompt_format = prompt if prompt is not None else c['main'].get('prompt', self.default_prompt)
|
2017-12-11 03:47:33 +00:00
|
|
|
self.prompt_dsn_format = prompt_dsn
|
2015-10-21 20:56:11 +00:00
|
|
|
self.on_error = c['main']['on_error'].upper()
|
2016-09-27 10:43:05 +00:00
|
|
|
self.decimal_format = c['data_formats']['decimal']
|
|
|
|
self.float_format = c['data_formats']['float']
|
2018-07-26 05:05:44 +00:00
|
|
|
self.keyring_enabled = c["main"].as_bool("keyring")
|
2017-02-25 14:13:41 +00:00
|
|
|
|
2018-01-17 18:57:12 +00:00
|
|
|
self.pgspecial.pset_pager(self.config['main'].as_bool(
|
|
|
|
'enable_pager') and "on" or "off")
|
2018-03-04 10:41:10 +00:00
|
|
|
|
|
|
|
self.style_output = style_factory_output(
|
|
|
|
self.syntax_style, c['colors'])
|
2018-01-06 10:58:59 +00:00
|
|
|
|
2017-05-09 23:20:08 +00:00
|
|
|
self.now = dt.datetime.today()
|
2017-05-09 21:55:12 +00:00
|
|
|
|
2015-09-04 05:37:31 +00:00
|
|
|
self.completion_refresher = CompletionRefresher()
|
2015-01-09 00:27:28 +00:00
|
|
|
|
2015-01-09 00:44:24 +00:00
|
|
|
self.query_history = []
|
|
|
|
|
2015-01-24 03:18:40 +00:00
|
|
|
# Initialize completer
|
2015-05-28 11:59:18 +00:00
|
|
|
smart_completion = c['main'].as_bool('smart_completion')
|
2016-09-04 12:41:15 +00:00
|
|
|
keyword_casing = c['main']['keyword_casing']
|
|
|
|
self.settings = {
|
|
|
|
'casing_file': get_casing_file(c),
|
|
|
|
'generate_casing_file': c['main'].as_bool('generate_casing_file'),
|
|
|
|
'generate_aliases': c['main'].as_bool('generate_aliases'),
|
|
|
|
'asterisk_column_order': c['main']['asterisk_column_order'],
|
2016-06-08 23:30:34 +00:00
|
|
|
'qualify_columns': c['main']['qualify_columns'],
|
2017-01-16 04:58:52 +00:00
|
|
|
'case_column_headers': c['main'].as_bool('case_column_headers'),
|
2017-03-04 01:08:47 +00:00
|
|
|
'search_path_filter': c['main'].as_bool('search_path_filter'),
|
2016-09-04 12:41:15 +00:00
|
|
|
'single_connection': single_connection,
|
2017-03-10 08:44:24 +00:00
|
|
|
'less_chatty': less_chatty,
|
2016-09-04 12:41:15 +00:00
|
|
|
'keyword_casing': keyword_casing,
|
|
|
|
}
|
|
|
|
|
2016-05-24 15:51:07 +00:00
|
|
|
completer = PGCompleter(smart_completion, pgspecial=self.pgspecial,
|
2016-06-02 20:20:00 +00:00
|
|
|
settings=self.settings)
|
2015-01-24 03:18:40 +00:00
|
|
|
self.completer = completer
|
2015-09-01 05:07:32 +00:00
|
|
|
self._completer_lock = threading.Lock()
|
2015-06-08 16:51:03 +00:00
|
|
|
self.register_special_commands()
|
|
|
|
|
2018-09-28 21:18:40 +00:00
|
|
|
self.prompt_app = None
|
2015-09-05 15:05:30 +00:00
|
|
|
|
2018-05-14 16:14:14 +00:00
|
|
|
def quit(self):
|
|
|
|
raise PgCliQuitError
|
|
|
|
|
2015-06-08 16:51:03 +00:00
|
|
|
def register_special_commands(self):
|
2015-06-25 10:27:29 +00:00
|
|
|
|
2015-11-01 22:36:55 +00:00
|
|
|
self.pgspecial.register(
|
|
|
|
self.change_db, '\\c', '\\c[onnect] database_name',
|
|
|
|
'Change to a new database.', aliases=('use', '\\connect', 'USE'))
|
|
|
|
|
|
|
|
refresh_callback = lambda: self.refresh_completions(
|
|
|
|
persist_priorities='all')
|
|
|
|
|
2018-05-14 16:14:14 +00:00
|
|
|
self.pgspecial.register(self.quit, '\\q', '\\q',
|
|
|
|
'Quit pgcli.', arg_type=NO_QUERY, case_sensitive=True,
|
|
|
|
aliases=(':q',))
|
|
|
|
self.pgspecial.register(self.quit, 'quit', 'quit',
|
|
|
|
'Quit pgcli.', arg_type=NO_QUERY, case_sensitive=False,
|
|
|
|
aliases=('exit',))
|
2015-11-01 22:36:55 +00:00
|
|
|
self.pgspecial.register(refresh_callback, '\\#', '\\#',
|
|
|
|
'Refresh auto-completions.', arg_type=NO_QUERY)
|
|
|
|
self.pgspecial.register(refresh_callback, '\\refresh', '\\refresh',
|
|
|
|
'Refresh auto-completions.', arg_type=NO_QUERY)
|
2015-08-17 04:34:27 +00:00
|
|
|
self.pgspecial.register(self.execute_from_file, '\\i', '\\i filename',
|
2015-11-01 22:36:55 +00:00
|
|
|
'Execute commands from file.')
|
2016-04-05 14:52:56 +00:00
|
|
|
self.pgspecial.register(self.write_to_file, '\\o', '\\o [filename]',
|
|
|
|
'Send all query results to file.')
|
2017-08-02 06:47:18 +00:00
|
|
|
self.pgspecial.register(self.info_connection, '\\conninfo',
|
|
|
|
'\\conninfo', 'Get connection details')
|
2018-01-03 20:25:03 +00:00
|
|
|
self.pgspecial.register(self.change_table_format, '\\T', '\\T [format]',
|
2018-01-03 10:07:04 +00:00
|
|
|
'Change the table format used to output results')
|
|
|
|
|
|
|
|
def change_table_format(self, pattern, **_):
|
|
|
|
try:
|
|
|
|
if pattern not in TabularOutputFormatter().supported_formats:
|
|
|
|
raise ValueError()
|
|
|
|
self.table_format = pattern
|
|
|
|
yield (None, None, None,
|
|
|
|
'Changed table format to {}'.format(pattern))
|
|
|
|
except ValueError:
|
|
|
|
msg = 'Table format {} not recognized. Allowed formats:'.format(
|
|
|
|
pattern)
|
|
|
|
for table_type in TabularOutputFormatter().supported_formats:
|
|
|
|
msg += "\n\t{}".format(table_type)
|
2018-01-03 21:05:45 +00:00
|
|
|
msg += '\nCurrently set to: %s' % self.table_format
|
2018-01-03 10:07:04 +00:00
|
|
|
yield (None, None, None, msg)
|
|
|
|
|
2017-08-02 06:47:18 +00:00
|
|
|
def info_connection(self, **_):
|
|
|
|
if self.pgexecute.host.startswith('/'):
|
|
|
|
host = 'socket "%s"' % self.pgexecute.host
|
|
|
|
else:
|
|
|
|
host = 'host "%s"' % self.pgexecute.host
|
|
|
|
|
|
|
|
yield (None, None, None, 'You are connected to database "%s" as user '
|
|
|
|
'"%s" on %s at port "%s".' % (self.pgexecute.dbname,
|
|
|
|
self.pgexecute.user,
|
|
|
|
host,
|
|
|
|
self.pgexecute.port))
|
2015-06-25 10:27:29 +00:00
|
|
|
|
2015-06-08 09:07:55 +00:00
|
|
|
def change_db(self, pattern, **_):
|
2015-06-18 02:46:46 +00:00
|
|
|
if pattern:
|
2017-08-06 18:06:00 +00:00
|
|
|
# Get all the parameters in pattern, handling double quotes if any.
|
|
|
|
infos = re.findall(r'"[^"]*"|[^"\'\s]+', pattern)
|
|
|
|
# Now removing quotes.
|
|
|
|
list(map(lambda s: s.strip('"'), infos))
|
|
|
|
|
|
|
|
infos.extend([None] * (4 - len(infos)))
|
|
|
|
db, user, host, port = infos
|
|
|
|
try:
|
|
|
|
self.pgexecute.connect(database=db, user=user, host=host,
|
2018-04-26 11:58:43 +00:00
|
|
|
port=port, application_name='pgcli')
|
2017-08-06 18:06:00 +00:00
|
|
|
except OperationalError as e:
|
|
|
|
click.secho(str(e), err=True, fg='red')
|
|
|
|
click.echo("Previous connection kept")
|
2015-06-08 09:07:55 +00:00
|
|
|
else:
|
2015-06-18 02:46:46 +00:00
|
|
|
self.pgexecute.connect()
|
2015-06-08 09:07:55 +00:00
|
|
|
|
|
|
|
yield (None, None, None, 'You are now connected to database "%s" as '
|
2017-08-06 22:24:00 +00:00
|
|
|
'user "%s"' % (self.pgexecute.dbname, self.pgexecute.user))
|
2015-01-24 03:18:40 +00:00
|
|
|
|
2015-08-17 04:34:27 +00:00
|
|
|
def execute_from_file(self, pattern, **_):
|
|
|
|
if not pattern:
|
|
|
|
message = '\\i: missing required argument'
|
2018-05-15 04:35:07 +00:00
|
|
|
return [(None, None, None, message, '', False, True)]
|
2015-08-17 04:34:27 +00:00
|
|
|
try:
|
|
|
|
with open(os.path.expanduser(pattern), encoding='utf-8') as f:
|
|
|
|
query = f.read()
|
|
|
|
except IOError as e:
|
2018-05-15 04:35:07 +00:00
|
|
|
return [(None, None, None, str(e), '', False, True)]
|
2015-08-17 04:34:27 +00:00
|
|
|
|
2018-05-17 09:54:50 +00:00
|
|
|
if (self.destructive_warning and
|
|
|
|
confirm_destructive_query(query) is False):
|
|
|
|
message = 'Wise choice. Command execution stopped.'
|
|
|
|
return [(None, None, None, message)]
|
|
|
|
|
2016-01-25 19:10:02 +00:00
|
|
|
on_error_resume = (self.on_error == 'RESUME')
|
|
|
|
return self.pgexecute.run(
|
|
|
|
query, self.pgspecial, on_error_resume=on_error_resume
|
|
|
|
)
|
2015-08-17 04:34:27 +00:00
|
|
|
|
2016-04-05 14:52:56 +00:00
|
|
|
def write_to_file(self, pattern, **_):
|
|
|
|
if not pattern:
|
|
|
|
self.output_file = None
|
2016-04-11 18:20:06 +00:00
|
|
|
message = 'File output disabled'
|
2018-05-15 15:57:19 +00:00
|
|
|
return [(None, None, None, message, '', True, True)]
|
2016-04-05 14:52:56 +00:00
|
|
|
filename = os.path.abspath(os.path.expanduser(pattern))
|
|
|
|
if not os.path.isfile(filename):
|
|
|
|
try:
|
|
|
|
open(filename, 'w').close()
|
|
|
|
except IOError as e:
|
|
|
|
self.output_file = None
|
2016-04-11 18:20:06 +00:00
|
|
|
message = str(e) + '\nFile output disabled'
|
2018-05-15 15:57:19 +00:00
|
|
|
return [(None, None, None, message, '', False, True)]
|
2016-04-05 14:52:56 +00:00
|
|
|
self.output_file = filename
|
2016-04-11 18:20:06 +00:00
|
|
|
message = 'Writing to file "%s"' % self.output_file
|
2018-05-15 15:57:19 +00:00
|
|
|
return [(None, None, None, message, '', True, True)]
|
2016-04-05 14:52:56 +00:00
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
def initialize_logging(self):
|
|
|
|
|
2015-05-28 23:06:55 +00:00
|
|
|
log_file = self.config['main']['log_file']
|
2015-11-05 02:21:59 +00:00
|
|
|
if log_file == 'default':
|
|
|
|
log_file = config_location() + 'log'
|
2016-02-03 22:28:04 +00:00
|
|
|
ensure_dir_exists(log_file)
|
2015-05-28 23:06:55 +00:00
|
|
|
log_level = self.config['main']['log_level']
|
2015-01-09 00:27:28 +00:00
|
|
|
|
2016-06-19 21:16:11 +00:00
|
|
|
# Disable logging if value is NONE by switching to a no-op handler.
|
|
|
|
# Set log level to a high value so it doesn't even waste cycles getting called.
|
|
|
|
if log_level.upper() == 'NONE':
|
2017-06-15 07:49:53 +00:00
|
|
|
handler = logging.NullHandler()
|
2016-06-19 21:16:11 +00:00
|
|
|
else:
|
|
|
|
handler = logging.FileHandler(os.path.expanduser(log_file))
|
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
level_map = {'CRITICAL': logging.CRITICAL,
|
|
|
|
'ERROR': logging.ERROR,
|
|
|
|
'WARNING': logging.WARNING,
|
|
|
|
'INFO': logging.INFO,
|
2016-06-19 21:16:11 +00:00
|
|
|
'DEBUG': logging.DEBUG,
|
|
|
|
'NONE': logging.CRITICAL
|
2015-01-09 00:27:28 +00:00
|
|
|
}
|
|
|
|
|
2016-06-19 21:16:11 +00:00
|
|
|
log_level = level_map[log_level.upper()]
|
2015-01-09 00:27:28 +00:00
|
|
|
|
|
|
|
formatter = logging.Formatter(
|
|
|
|
'%(asctime)s (%(process)d/%(threadName)s) '
|
|
|
|
'%(name)s %(levelname)s - %(message)s')
|
|
|
|
|
|
|
|
handler.setFormatter(formatter)
|
2015-01-08 12:58:44 +00:00
|
|
|
|
2015-01-10 23:01:14 +00:00
|
|
|
root_logger = logging.getLogger('pgcli')
|
|
|
|
root_logger.addHandler(handler)
|
2016-06-04 14:15:18 +00:00
|
|
|
root_logger.setLevel(log_level)
|
2015-01-09 00:27:28 +00:00
|
|
|
|
2015-01-10 23:01:14 +00:00
|
|
|
root_logger.debug('Initializing pgcli logging.')
|
2015-01-16 07:41:43 +00:00
|
|
|
root_logger.debug('Log file %r.', log_file)
|
2015-01-09 00:27:28 +00:00
|
|
|
|
2016-06-04 14:15:18 +00:00
|
|
|
pgspecial_logger = logging.getLogger('pgspecial')
|
|
|
|
pgspecial_logger.addHandler(handler)
|
|
|
|
pgspecial_logger.setLevel(log_level)
|
|
|
|
|
2015-08-07 23:32:39 +00:00
|
|
|
def connect_dsn(self, dsn):
|
|
|
|
self.connect(dsn=dsn)
|
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
def connect_uri(self, uri):
|
2019-01-03 22:27:47 +00:00
|
|
|
kwargs = psycopg2.extensions.parse_dsn(uri)
|
|
|
|
remap = {
|
|
|
|
'dbname': 'database',
|
|
|
|
'password': 'passwd',
|
|
|
|
}
|
|
|
|
kwargs = {remap.get(k, k): v for k, v in kwargs.items()}
|
|
|
|
self.connect(**kwargs)
|
2015-01-09 00:27:28 +00:00
|
|
|
|
2015-08-07 23:32:39 +00:00
|
|
|
def connect(self, database='', host='', user='', port='', passwd='',
|
2017-05-25 10:39:01 +00:00
|
|
|
dsn='', **kwargs):
|
2015-01-09 00:27:28 +00:00
|
|
|
# Connect to the database.
|
|
|
|
|
2015-06-05 00:26:02 +00:00
|
|
|
if not user:
|
|
|
|
user = getuser()
|
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
if not database:
|
2015-06-05 00:26:02 +00:00
|
|
|
database = user
|
2015-01-09 00:27:28 +00:00
|
|
|
|
2015-06-23 02:23:35 +00:00
|
|
|
# If password prompt is not forced but no password is provided, try
|
|
|
|
# getting it from environment variable.
|
|
|
|
if not self.force_passwd_prompt and not passwd:
|
|
|
|
passwd = os.environ.get('PGPASSWORD', '')
|
|
|
|
|
2018-05-14 15:30:50 +00:00
|
|
|
# Find password from store
|
|
|
|
key = '%s@%s' % (user, host)
|
2018-07-22 05:34:56 +00:00
|
|
|
keyring_error_message = dedent("""\
|
|
|
|
{}
|
|
|
|
{}
|
|
|
|
To remove this message do one of the following:
|
|
|
|
- prepare keyring as described at: https://keyring.readthedocs.io/en/stable/
|
|
|
|
- uninstall keyring: pip uninstall keyring
|
|
|
|
- disable keyring in our configuration: add keyring = False to [main]""")
|
2018-07-26 05:05:44 +00:00
|
|
|
if not passwd and keyring and self.keyring_enabled:
|
2018-06-25 20:55:54 +00:00
|
|
|
try:
|
|
|
|
passwd = keyring.get_password('pgcli', key)
|
2018-07-22 05:34:56 +00:00
|
|
|
except (
|
|
|
|
RuntimeError,
|
|
|
|
keyring.errors.InitError
|
|
|
|
) as e:
|
|
|
|
click.secho(
|
|
|
|
keyring_error_message.format(
|
|
|
|
"Load your password from keyring returned:",
|
|
|
|
str(e)
|
|
|
|
),
|
|
|
|
err=True,
|
|
|
|
fg='red'
|
|
|
|
)
|
2018-05-14 15:30:50 +00:00
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
# Prompt for a password immediately if requested via the -W flag. This
|
|
|
|
# avoids wasting time trying to connect to the database and catching a
|
|
|
|
# no-password exception.
|
|
|
|
# If we successfully parsed a password from a URI, there's no need to
|
|
|
|
# prompt for it, even with the -W flag
|
|
|
|
if self.force_passwd_prompt and not passwd:
|
2017-12-20 19:48:26 +00:00
|
|
|
passwd = click.prompt('Password for %s' % user, hide_input=True,
|
2015-01-09 00:27:28 +00:00
|
|
|
show_default=False, type=str)
|
|
|
|
|
|
|
|
# Prompt for a password after 1st attempt to connect without a password
|
|
|
|
# fails. Don't prompt if the -w flag is supplied
|
|
|
|
auto_passwd_prompt = not passwd and not self.never_passwd_prompt
|
|
|
|
|
|
|
|
# Attempt to connect to the database.
|
|
|
|
# Note that passwd may be empty on the first attempt. If connection
|
|
|
|
# fails because of a missing password, but we're allowed to prompt for
|
|
|
|
# a password (no -w flag), prompt for a passwd and try again.
|
|
|
|
try:
|
|
|
|
try:
|
2017-05-25 10:39:01 +00:00
|
|
|
pgexecute = PGExecute(database, user, passwd, host, port, dsn,
|
2018-04-26 11:58:43 +00:00
|
|
|
application_name='pgcli', **kwargs)
|
2017-07-06 04:58:52 +00:00
|
|
|
except (OperationalError, InterfaceError) as e:
|
2015-01-27 07:07:10 +00:00
|
|
|
if ('no password supplied' in utf8tounicode(e.args[0]) and
|
|
|
|
auto_passwd_prompt):
|
2017-12-20 19:48:26 +00:00
|
|
|
passwd = click.prompt('Password for %s' % user,
|
|
|
|
hide_input=True, show_default=False,
|
|
|
|
type=str)
|
2015-08-07 23:32:39 +00:00
|
|
|
pgexecute = PGExecute(database, user, passwd, host, port,
|
2018-04-26 11:58:43 +00:00
|
|
|
dsn, application_name='pgcli',
|
|
|
|
**kwargs)
|
2015-01-09 00:27:28 +00:00
|
|
|
else:
|
|
|
|
raise e
|
2018-07-26 05:05:44 +00:00
|
|
|
if passwd and keyring and self.keyring_enabled:
|
2018-07-22 05:34:56 +00:00
|
|
|
try:
|
|
|
|
keyring.set_password('pgcli', key, passwd)
|
|
|
|
except (
|
|
|
|
keyring.errors.InitError,
|
|
|
|
RuntimeError,
|
|
|
|
keyring.errors.KeyringLocked
|
|
|
|
) as e:
|
|
|
|
click.secho(
|
|
|
|
keyring_error_message.format(
|
|
|
|
"Set password in keyring returned:",
|
|
|
|
str(e)
|
|
|
|
),
|
|
|
|
err=True,
|
|
|
|
fg='red'
|
|
|
|
)
|
2015-01-09 00:27:28 +00:00
|
|
|
|
|
|
|
except Exception as e: # Connecting to a database could fail.
|
|
|
|
self.logger.debug('Database connection failed: %r.', e)
|
2015-01-27 07:07:10 +00:00
|
|
|
self.logger.error("traceback: %r", traceback.format_exc())
|
2015-01-09 00:27:28 +00:00
|
|
|
click.secho(str(e), err=True, fg='red')
|
|
|
|
exit(1)
|
|
|
|
|
|
|
|
self.pgexecute = pgexecute
|
|
|
|
|
2018-09-28 21:18:40 +00:00
|
|
|
def handle_editor_command(self, text):
|
2017-07-27 12:08:12 +00:00
|
|
|
r"""
|
2015-04-23 18:25:27 +00:00
|
|
|
Editor command is any query that is prefixed or suffixed
|
|
|
|
by a '\e'. The reason for a while loop is because a user
|
|
|
|
might edit a query multiple times.
|
|
|
|
For eg:
|
|
|
|
"select * from \e"<enter> to edit it in vim, then come
|
|
|
|
back to the prompt with the edited query "select * from
|
|
|
|
blah where q = 'abc'\e" to edit it again.
|
2018-09-28 21:18:40 +00:00
|
|
|
:param text: Document
|
2015-04-23 18:25:27 +00:00
|
|
|
:return: Document
|
|
|
|
"""
|
2018-09-28 21:18:40 +00:00
|
|
|
editor_command = special.editor_command(text)
|
|
|
|
while editor_command:
|
|
|
|
if editor_command == '\\e':
|
|
|
|
filename = special.get_filename(text)
|
|
|
|
query = special.get_editor_query(
|
|
|
|
text) or self.get_last_query()
|
|
|
|
else: # \ev or \ef
|
|
|
|
filename = None
|
|
|
|
spec = text.split()[1]
|
|
|
|
if editor_command == '\\ev':
|
|
|
|
query = self.pgexecute.view_definition(spec)
|
|
|
|
elif editor_command == '\\ef':
|
|
|
|
query = self.pgexecute.function_definition(spec)
|
|
|
|
sql, message = special.open_external_editor(
|
|
|
|
filename, sql=query)
|
|
|
|
if message:
|
|
|
|
# Something went wrong. Raise an exception and bail.
|
|
|
|
raise RuntimeError(message)
|
|
|
|
while True:
|
|
|
|
try:
|
2018-11-11 13:16:32 +00:00
|
|
|
text = self.prompt_app.prompt(default=sql)
|
2018-09-28 21:18:40 +00:00
|
|
|
break
|
|
|
|
except KeyboardInterrupt:
|
2018-11-11 13:16:32 +00:00
|
|
|
sql = ""
|
2018-09-28 21:18:40 +00:00
|
|
|
|
|
|
|
editor_command = special.editor_command(text)
|
|
|
|
return text
|
2015-04-23 18:25:27 +00:00
|
|
|
|
2018-05-15 00:37:16 +00:00
|
|
|
def execute_command(self, text):
|
2016-08-03 16:57:23 +00:00
|
|
|
logger = self.logger
|
|
|
|
|
2018-05-15 14:54:07 +00:00
|
|
|
query = MetaQuery(query=text, successful=False)
|
2018-05-15 14:45:36 +00:00
|
|
|
|
2016-08-03 16:57:23 +00:00
|
|
|
try:
|
2018-05-17 09:54:50 +00:00
|
|
|
if (self.destructive_warning):
|
|
|
|
destroy = confirm = confirm_destructive_query(text)
|
2018-06-09 17:37:35 +00:00
|
|
|
if destroy is False:
|
2018-05-17 09:54:50 +00:00
|
|
|
click.secho('Wise choice!')
|
|
|
|
raise KeyboardInterrupt
|
2018-06-09 17:37:35 +00:00
|
|
|
elif destroy:
|
|
|
|
click.secho('Your call!')
|
|
|
|
output, query = self._evaluate_command(text)
|
2016-08-03 16:57:23 +00:00
|
|
|
except KeyboardInterrupt:
|
|
|
|
# Restart connection to the database
|
|
|
|
self.pgexecute.connect()
|
|
|
|
logger.debug("cancelled query, sql: %r", text)
|
|
|
|
click.secho("cancelled query", err=True, fg='red')
|
|
|
|
except NotImplementedError:
|
|
|
|
click.secho('Not Yet Implemented.', fg="yellow")
|
|
|
|
except OperationalError as e:
|
2017-03-03 06:01:08 +00:00
|
|
|
logger.error("sql: %r, error: %r", text, e)
|
|
|
|
logger.error("traceback: %r", traceback.format_exc())
|
|
|
|
self._handle_server_closed_connection()
|
2018-05-14 16:14:14 +00:00
|
|
|
except PgCliQuitError as e:
|
|
|
|
raise
|
2016-08-03 16:57:23 +00:00
|
|
|
except Exception as e:
|
|
|
|
logger.error("sql: %r, error: %r", text, e)
|
|
|
|
logger.error("traceback: %r", traceback.format_exc())
|
|
|
|
click.secho(str(e), err=True, fg='red')
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
if self.output_file and not text.startswith(('\\o ', '\\? ')):
|
|
|
|
try:
|
|
|
|
with open(self.output_file, 'a', encoding='utf-8') as f:
|
|
|
|
click.echo(text, file=f)
|
|
|
|
click.echo('\n'.join(output), file=f)
|
|
|
|
click.echo('', file=f) # extra newline
|
|
|
|
except IOError as e:
|
|
|
|
click.secho(str(e), err=True, fg='red')
|
|
|
|
else:
|
2018-01-06 10:58:59 +00:00
|
|
|
self.echo_via_pager('\n'.join(output))
|
2016-08-03 16:57:23 +00:00
|
|
|
except KeyboardInterrupt:
|
|
|
|
pass
|
|
|
|
|
|
|
|
if self.pgspecial.timing_enabled:
|
|
|
|
# Only add humanized time display if > 1 second
|
|
|
|
if query.total_time > 1:
|
2018-12-02 02:23:50 +00:00
|
|
|
print('Time: %0.03fs (%s), executed in: %0.03fs (%s)' % (query.total_time,
|
2018-12-03 17:01:32 +00:00
|
|
|
humanize.time.naturaldelta(
|
|
|
|
query.total_time),
|
|
|
|
query.execution_time,
|
|
|
|
humanize.time.naturaldelta(query.execution_time)))
|
2016-08-03 16:57:23 +00:00
|
|
|
else:
|
|
|
|
print('Time: %0.03fs' % query.total_time)
|
|
|
|
|
|
|
|
# Check if we need to update completions, in order of most
|
|
|
|
# to least drastic changes
|
|
|
|
if query.db_changed:
|
|
|
|
with self._completer_lock:
|
|
|
|
self.completer.reset_completions()
|
|
|
|
self.refresh_completions(persist_priorities='keywords')
|
|
|
|
elif query.meta_changed:
|
|
|
|
self.refresh_completions(persist_priorities='all')
|
|
|
|
elif query.path_changed:
|
|
|
|
logger.debug('Refreshing search path')
|
|
|
|
with self._completer_lock:
|
|
|
|
self.completer.set_search_path(
|
|
|
|
self.pgexecute.search_path())
|
|
|
|
logger.debug('Search path: %r',
|
|
|
|
self.completer.search_path)
|
|
|
|
return query
|
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
def run_cli(self):
|
|
|
|
logger = self.logger
|
2015-11-22 12:29:43 +00:00
|
|
|
|
2015-11-01 22:36:55 +00:00
|
|
|
history_file = self.config['main']['history_file']
|
|
|
|
if history_file == 'default':
|
|
|
|
history_file = config_location() + 'history'
|
|
|
|
history = FileHistory(os.path.expanduser(history_file))
|
2015-11-22 12:29:43 +00:00
|
|
|
self.refresh_completions(history=history,
|
2015-11-01 22:36:55 +00:00
|
|
|
persist_priorities='none')
|
2015-01-09 00:27:28 +00:00
|
|
|
|
2018-09-28 21:18:40 +00:00
|
|
|
self.prompt_app = self._build_cli(history)
|
2015-05-31 13:59:20 +00:00
|
|
|
|
2016-06-19 21:16:11 +00:00
|
|
|
if not self.less_chatty:
|
2018-07-20 22:47:40 +00:00
|
|
|
print('Server: PostgreSQL', self.pgexecute.get_server_version())
|
2016-06-19 21:16:11 +00:00
|
|
|
print('Version:', __version__)
|
|
|
|
print('Chat: https://gitter.im/dbcli/pgcli')
|
|
|
|
print('Mail: https://groups.google.com/forum/#!forum/pgcli')
|
|
|
|
print('Home: http://pgcli.com')
|
2015-01-24 03:18:40 +00:00
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
try:
|
|
|
|
while True:
|
2018-09-28 21:18:40 +00:00
|
|
|
try:
|
2018-11-11 13:16:32 +00:00
|
|
|
text = self.prompt_app.prompt()
|
2018-09-28 21:18:40 +00:00
|
|
|
except KeyboardInterrupt:
|
|
|
|
continue
|
2015-01-09 00:27:28 +00:00
|
|
|
|
2015-04-23 07:26:48 +00:00
|
|
|
try:
|
2018-09-28 21:18:40 +00:00
|
|
|
text = self.handle_editor_command(text)
|
2015-04-23 07:26:48 +00:00
|
|
|
except RuntimeError as e:
|
2018-09-28 21:18:40 +00:00
|
|
|
logger.error("sql: %r, error: %r", text, e)
|
2015-04-23 07:26:48 +00:00
|
|
|
logger.error("traceback: %r", traceback.format_exc())
|
|
|
|
click.secho(str(e), err=True, fg='red')
|
|
|
|
continue
|
2015-04-18 17:07:06 +00:00
|
|
|
|
2018-09-28 21:18:40 +00:00
|
|
|
# Initialize default metaquery in case execution fails
|
|
|
|
self.watch_command, timing = special.get_watch_command(text)
|
2018-05-14 18:20:37 +00:00
|
|
|
if self.watch_command:
|
|
|
|
while self.watch_command:
|
2016-08-03 16:57:23 +00:00
|
|
|
try:
|
2018-05-15 14:45:36 +00:00
|
|
|
query = self.execute_command(self.watch_command)
|
2018-05-14 18:58:17 +00:00
|
|
|
click.echo(
|
2018-05-15 14:52:51 +00:00
|
|
|
'Waiting for {0} seconds before repeating'
|
|
|
|
.format(timing))
|
2016-08-03 16:57:23 +00:00
|
|
|
sleep(timing)
|
|
|
|
except KeyboardInterrupt:
|
2018-05-14 18:20:37 +00:00
|
|
|
self.watch_command = None
|
2015-01-16 07:41:43 +00:00
|
|
|
else:
|
2018-09-28 21:18:40 +00:00
|
|
|
query = self.execute_command(text)
|
2015-11-22 12:29:43 +00:00
|
|
|
|
2017-05-09 23:20:08 +00:00
|
|
|
self.now = dt.datetime.today()
|
2017-05-09 21:55:12 +00:00
|
|
|
|
2015-11-01 22:36:55 +00:00
|
|
|
# Allow PGCompleter to learn user's preferred keywords, etc.
|
|
|
|
with self._completer_lock:
|
2018-09-28 21:18:40 +00:00
|
|
|
self.completer.extend_query_history(text)
|
2015-01-31 21:46:32 +00:00
|
|
|
|
2015-01-10 22:52:50 +00:00
|
|
|
self.query_history.append(query)
|
2015-01-09 00:44:24 +00:00
|
|
|
|
2018-07-11 05:51:38 +00:00
|
|
|
except (PgCliQuitError, EOFError):
|
2016-06-19 21:16:11 +00:00
|
|
|
if not self.less_chatty:
|
|
|
|
print ('Goodbye!')
|
2015-01-09 00:27:28 +00:00
|
|
|
|
2015-11-01 22:36:55 +00:00
|
|
|
def _build_cli(self, history):
|
2018-09-28 21:18:40 +00:00
|
|
|
key_bindings = pgcli_bindings(self)
|
2015-10-28 13:23:23 +00:00
|
|
|
|
2018-09-28 21:18:40 +00:00
|
|
|
def get_message():
|
2017-12-11 03:47:33 +00:00
|
|
|
if self.dsn_alias and self.prompt_dsn_format is not None:
|
|
|
|
prompt_format = self.prompt_dsn_format
|
2017-11-17 05:33:28 +00:00
|
|
|
else:
|
2017-12-11 03:47:33 +00:00
|
|
|
prompt_format = self.prompt_format
|
|
|
|
|
|
|
|
prompt = self.get_prompt(prompt_format)
|
|
|
|
|
|
|
|
if (prompt_format == self.default_prompt and
|
2017-12-16 22:00:26 +00:00
|
|
|
len(prompt) > self.max_len_prompt):
|
2017-02-08 09:32:01 +00:00
|
|
|
prompt = self.get_prompt('\\d> ')
|
2017-11-17 05:33:28 +00:00
|
|
|
|
2018-09-28 21:18:40 +00:00
|
|
|
return [('class:prompt', prompt)]
|
|
|
|
|
|
|
|
def get_continuation(width, line_number, is_soft_wrap):
|
|
|
|
continuation = self.multiline_continuation_char * (width - 1) + ' '
|
|
|
|
return [('class:continuation', continuation)]
|
|
|
|
|
|
|
|
get_toolbar_tokens = create_toolbar_tokens_func(self)
|
|
|
|
|
|
|
|
if self.wider_completion_menu:
|
|
|
|
complete_style = CompleteStyle.MULTI_COLUMN
|
|
|
|
else:
|
|
|
|
complete_style = CompleteStyle.COLUMN
|
2015-10-28 13:23:23 +00:00
|
|
|
|
|
|
|
with self._completer_lock:
|
2018-09-28 21:18:40 +00:00
|
|
|
prompt_app = PromptSession(
|
|
|
|
lexer=PygmentsLexer(PostgresLexer),
|
|
|
|
reserve_space_for_menu=self.min_num_menu_lines,
|
|
|
|
message=get_message,
|
|
|
|
prompt_continuation=get_continuation,
|
|
|
|
bottom_toolbar=get_toolbar_tokens,
|
|
|
|
complete_style=complete_style,
|
|
|
|
input_processors=[
|
|
|
|
# Highlight matching brackets while editing.
|
|
|
|
ConditionalProcessor(
|
|
|
|
processor=HighlightMatchingBracketProcessor(
|
|
|
|
chars='[](){}'),
|
|
|
|
filter=HasFocus(DEFAULT_BUFFER) & ~IsDone()),
|
|
|
|
# Render \t as 4 spaces instead of "^I"
|
|
|
|
TabsProcessor(char1=' ', char2=' ')],
|
2017-07-16 22:06:25 +00:00
|
|
|
auto_suggest=AutoSuggestFromHistory(),
|
2018-09-28 21:18:40 +00:00
|
|
|
tempfile_suffix='.sql',
|
|
|
|
multiline=pg_is_multiline(self),
|
2015-11-01 22:36:55 +00:00
|
|
|
history=history,
|
2018-12-27 23:56:22 +00:00
|
|
|
completer=ThreadedCompleter(
|
|
|
|
DynamicCompleter(lambda: self.completer)),
|
2018-09-28 21:18:40 +00:00
|
|
|
complete_while_typing=True,
|
2015-10-28 13:23:23 +00:00
|
|
|
style=style_factory(self.syntax_style, self.cli_style),
|
2018-09-28 21:18:40 +00:00
|
|
|
include_default_pygments_style=False,
|
|
|
|
key_bindings=key_bindings,
|
|
|
|
enable_open_in_editor=True,
|
|
|
|
enable_system_prompt=True,
|
2018-11-18 03:41:34 +00:00
|
|
|
enable_suspend=True,
|
2018-09-28 21:18:40 +00:00
|
|
|
editing_mode=EditingMode.VI if self.vi_mode else EditingMode.EMACS,
|
|
|
|
search_ignore_case=True)
|
2015-10-28 13:23:23 +00:00
|
|
|
|
2018-09-28 21:18:40 +00:00
|
|
|
return prompt_app
|
2015-10-28 13:23:23 +00:00
|
|
|
|
2016-06-02 19:36:48 +00:00
|
|
|
def _should_show_limit_prompt(self, status, cur):
|
2016-06-02 19:37:03 +00:00
|
|
|
"""returns True if limit prompt should be shown, False otherwise."""
|
2016-06-02 19:36:48 +00:00
|
|
|
if not is_select(status):
|
|
|
|
return False
|
2018-09-28 21:18:40 +00:00
|
|
|
return self.row_limit > 0 and cur and (cur.rowcount > self.row_limit)
|
2016-06-02 19:36:48 +00:00
|
|
|
|
2015-10-28 13:23:23 +00:00
|
|
|
def _evaluate_command(self, text):
|
|
|
|
"""Used to run a command entered by the user during CLI operation
|
|
|
|
(Puts the E in REPL)
|
|
|
|
|
|
|
|
returns (results, MetaQuery)
|
|
|
|
"""
|
|
|
|
logger = self.logger
|
|
|
|
logger.debug('sql: %r', text)
|
|
|
|
|
|
|
|
all_success = True
|
|
|
|
meta_changed = False # CREATE, ALTER, DROP, etc
|
|
|
|
mutated = False # INSERT, DELETE, etc
|
|
|
|
db_changed = False
|
|
|
|
path_changed = False
|
|
|
|
output = []
|
|
|
|
total = 0
|
2018-12-02 02:23:50 +00:00
|
|
|
execution = 0
|
2015-10-28 13:23:23 +00:00
|
|
|
|
|
|
|
# Run the query.
|
|
|
|
start = time()
|
|
|
|
on_error_resume = self.on_error == 'RESUME'
|
|
|
|
res = self.pgexecute.run(text, self.pgspecial,
|
|
|
|
exception_formatter, on_error_resume)
|
|
|
|
|
2018-05-15 04:35:07 +00:00
|
|
|
for title, cur, headers, status, sql, success, is_special in res:
|
2015-10-28 13:23:23 +00:00
|
|
|
logger.debug("headers: %r", headers)
|
|
|
|
logger.debug("rows: %r", cur)
|
|
|
|
logger.debug("status: %r", status)
|
2016-06-02 19:36:48 +00:00
|
|
|
threshold = self.row_limit
|
2016-06-02 22:04:50 +00:00
|
|
|
if self._should_show_limit_prompt(status, cur):
|
2015-10-28 13:23:23 +00:00
|
|
|
click.secho('The result set has more than %s rows.'
|
|
|
|
% threshold, fg='red')
|
|
|
|
if not click.confirm('Do you want to continue?'):
|
|
|
|
click.secho("Aborted!", err=True, fg='red')
|
|
|
|
break
|
|
|
|
|
2017-01-21 17:51:12 +00:00
|
|
|
if self.pgspecial.auto_expand or self.auto_expand:
|
2018-09-28 21:18:40 +00:00
|
|
|
max_width = self.prompt_app.output.get_size().columns
|
2015-10-28 13:23:23 +00:00
|
|
|
else:
|
|
|
|
max_width = None
|
|
|
|
|
2017-01-21 17:51:12 +00:00
|
|
|
expanded = self.pgspecial.expanded_output or self.expanded_output
|
2017-01-16 04:27:19 +00:00
|
|
|
settings = OutputSettings(
|
|
|
|
table_format=self.table_format,
|
|
|
|
dcmlfmt=self.decimal_format,
|
|
|
|
floatfmt=self.float_format,
|
|
|
|
missingval=self.null_string,
|
|
|
|
expanded=expanded,
|
2017-01-16 04:58:52 +00:00
|
|
|
max_width=max_width,
|
|
|
|
case_function=(
|
|
|
|
self.completer.case if self.settings['case_column_headers']
|
|
|
|
else lambda x: x
|
2018-01-11 20:47:56 +00:00
|
|
|
),
|
|
|
|
style_output=self.style_output
|
2017-01-16 04:27:19 +00:00
|
|
|
)
|
2018-12-02 02:23:50 +00:00
|
|
|
execution = time() - start
|
2017-06-26 15:10:41 +00:00
|
|
|
formatted = format_output(title, cur, headers, status, settings)
|
2015-10-28 13:23:23 +00:00
|
|
|
|
|
|
|
output.extend(formatted)
|
2016-09-06 03:25:11 +00:00
|
|
|
total = time() - start
|
2015-10-28 13:23:23 +00:00
|
|
|
|
|
|
|
# Keep track of whether any of the queries are mutating or changing
|
|
|
|
# the database
|
|
|
|
if success:
|
|
|
|
mutated = mutated or is_mutating(status)
|
|
|
|
db_changed = db_changed or has_change_db_cmd(sql)
|
|
|
|
meta_changed = meta_changed or has_meta_cmd(sql)
|
|
|
|
path_changed = path_changed or has_change_path_cmd(sql)
|
|
|
|
else:
|
|
|
|
all_success = False
|
|
|
|
|
2018-12-02 02:23:50 +00:00
|
|
|
meta_query = MetaQuery(text, all_success, total, execution, meta_changed,
|
2018-05-15 05:25:33 +00:00
|
|
|
db_changed, path_changed, mutated, is_special)
|
2015-10-28 13:23:23 +00:00
|
|
|
|
|
|
|
return output, meta_query
|
|
|
|
|
|
|
|
def _handle_server_closed_connection(self):
|
|
|
|
"""Used during CLI execution"""
|
|
|
|
reconnect = click.prompt(
|
|
|
|
'Connection reset. Reconnect (Y/n)',
|
|
|
|
show_default=False, type=bool, default=True)
|
|
|
|
if reconnect:
|
|
|
|
try:
|
|
|
|
self.pgexecute.connect()
|
|
|
|
click.secho('Reconnected!\nTry the command again.', fg='green')
|
|
|
|
except OperationalError as e:
|
|
|
|
click.secho(str(e), err=True, fg='red')
|
|
|
|
|
2015-11-01 22:36:55 +00:00
|
|
|
def refresh_completions(self, history=None, persist_priorities='all'):
|
|
|
|
""" Refresh outdated completions
|
|
|
|
|
|
|
|
:param history: A prompt_toolkit.history.FileHistory object. Used to
|
|
|
|
load keyword and identifier preferences
|
|
|
|
|
|
|
|
:param persist_priorities: 'all' or 'keywords'
|
|
|
|
"""
|
|
|
|
|
|
|
|
callback = functools.partial(self._on_completions_refreshed,
|
|
|
|
persist_priorities=persist_priorities)
|
2016-05-24 15:51:07 +00:00
|
|
|
self.completion_refresher.refresh(self.pgexecute, self.pgspecial,
|
2016-06-02 20:20:00 +00:00
|
|
|
callback, history=history, settings=self.settings)
|
2015-09-04 05:37:31 +00:00
|
|
|
return [(None, None, None,
|
|
|
|
'Auto-completion refresh started in the background.')]
|
2015-09-01 07:22:40 +00:00
|
|
|
|
2015-11-01 22:36:55 +00:00
|
|
|
def _on_completions_refreshed(self, new_completer, persist_priorities):
|
|
|
|
self._swap_completer_objects(new_completer, persist_priorities)
|
2015-09-07 12:40:32 +00:00
|
|
|
|
2018-09-28 21:18:40 +00:00
|
|
|
if self.prompt_app:
|
2015-09-07 12:40:32 +00:00
|
|
|
# After refreshing, redraw the CLI to clear the statusbar
|
|
|
|
# "Refreshing completions..." indicator
|
2018-09-28 21:18:40 +00:00
|
|
|
self.prompt_app.app.invalidate()
|
2015-09-07 12:40:32 +00:00
|
|
|
|
2015-11-01 22:36:55 +00:00
|
|
|
def _swap_completer_objects(self, new_completer, persist_priorities):
|
2018-09-28 21:18:40 +00:00
|
|
|
"""Swap the completer object with the newly created completer.
|
|
|
|
|
|
|
|
persist_priorities is a string specifying how the old completer's
|
|
|
|
learned prioritizer should be transferred to the new completer.
|
2015-11-01 22:36:55 +00:00
|
|
|
|
2018-09-28 21:18:40 +00:00
|
|
|
'none' - The new prioritizer is left in a new/clean state
|
2015-11-01 22:36:55 +00:00
|
|
|
|
2018-09-28 21:18:40 +00:00
|
|
|
'all' - The new prioritizer is updated to exactly reflect
|
|
|
|
the old one
|
2015-11-01 22:36:55 +00:00
|
|
|
|
2018-09-28 21:18:40 +00:00
|
|
|
'keywords' - The new prioritizer is updated with old keyword
|
|
|
|
priorities, but not any other.
|
2015-11-01 22:36:55 +00:00
|
|
|
|
2015-09-04 05:37:31 +00:00
|
|
|
"""
|
|
|
|
with self._completer_lock:
|
2015-11-01 22:36:55 +00:00
|
|
|
old_completer = self.completer
|
2015-09-04 05:53:06 +00:00
|
|
|
self.completer = new_completer
|
2015-11-01 22:36:55 +00:00
|
|
|
|
|
|
|
if persist_priorities == 'all':
|
|
|
|
# Just swap over the entire prioritizer
|
|
|
|
new_completer.prioritizer = old_completer.prioritizer
|
|
|
|
elif persist_priorities == 'keywords':
|
|
|
|
# Swap over the entire prioritizer, but clear name priorities,
|
|
|
|
# leaving learned keyword priorities alone
|
|
|
|
new_completer.prioritizer = old_completer.prioritizer
|
|
|
|
new_completer.prioritizer.clear_names()
|
|
|
|
elif persist_priorities == 'none':
|
|
|
|
# Leave the new prioritizer as is
|
|
|
|
pass
|
2018-09-28 21:18:40 +00:00
|
|
|
self.completer = new_completer
|
2015-09-01 05:07:32 +00:00
|
|
|
|
2015-01-24 03:19:07 +00:00
|
|
|
def get_completions(self, text, cursor_positition):
|
2015-09-01 05:07:32 +00:00
|
|
|
with self._completer_lock:
|
|
|
|
return self.completer.get_completions(
|
|
|
|
Document(text=text, cursor_position=cursor_positition), None)
|
2015-01-24 03:19:07 +00:00
|
|
|
|
2016-07-15 22:21:20 +00:00
|
|
|
def get_prompt(self, string):
|
2017-12-11 03:47:33 +00:00
|
|
|
# should be before replacing \\d
|
|
|
|
string = string.replace('\\dsn_alias', self.dsn_alias or '')
|
2017-05-09 23:20:08 +00:00
|
|
|
string = string.replace('\\t', self.now.strftime('%x %X'))
|
2016-07-15 22:21:20 +00:00
|
|
|
string = string.replace('\\u', self.pgexecute.user or '(none)')
|
2019-01-03 22:27:47 +00:00
|
|
|
string = string.replace('\\H', self.pgexecute.host or '(none)')
|
|
|
|
string = string.replace('\\h', self.pgexecute.short_host or '(none)')
|
2016-07-15 22:21:20 +00:00
|
|
|
string = string.replace('\\d', self.pgexecute.dbname or '(none)')
|
2018-10-30 14:28:28 +00:00
|
|
|
string = string.replace('\\p', str(
|
|
|
|
self.pgexecute.port) if self.pgexecute.port is not None else '5432')
|
2016-08-25 18:53:39 +00:00
|
|
|
string = string.replace('\\i', str(self.pgexecute.pid) or '(none)')
|
|
|
|
string = string.replace('\\#', "#" if (self.pgexecute.superuser) else ">")
|
2016-07-15 22:21:20 +00:00
|
|
|
string = string.replace('\\n', "\n")
|
|
|
|
return string
|
|
|
|
|
2017-05-03 04:31:29 +00:00
|
|
|
def get_last_query(self):
|
|
|
|
"""Get the last query executed or None."""
|
|
|
|
return self.query_history[-1][0] if self.query_history else None
|
|
|
|
|
2018-10-03 00:10:43 +00:00
|
|
|
def is_too_wide(self, line):
|
2018-09-28 21:18:40 +00:00
|
|
|
"""Will this line be too wide to fit into terminal?"""
|
2018-10-03 00:10:43 +00:00
|
|
|
if not self.prompt_app:
|
|
|
|
return False
|
2018-09-28 21:18:40 +00:00
|
|
|
return len(COLOR_CODE_REGEX.sub('', line)) > self.prompt_app.output.get_size().columns
|
|
|
|
|
2018-10-03 00:10:43 +00:00
|
|
|
def is_too_tall(self, lines):
|
|
|
|
"""Are there too many lines to fit into terminal?"""
|
|
|
|
if not self.prompt_app:
|
|
|
|
return False
|
|
|
|
return len(lines) >= (self.prompt_app.output.get_size().rows - 4)
|
|
|
|
|
2018-01-06 10:58:59 +00:00
|
|
|
def echo_via_pager(self, text, color=None):
|
2018-05-14 18:20:37 +00:00
|
|
|
if self.pgspecial.pager_config == PAGER_OFF or self.watch_command:
|
2018-01-06 10:58:59 +00:00
|
|
|
click.echo(text, color=color)
|
2018-05-15 17:42:21 +00:00
|
|
|
elif self.pgspecial.pager_config == PAGER_LONG_OUTPUT:
|
|
|
|
lines = text.split('\n')
|
|
|
|
|
|
|
|
# The last 4 lines are reserved for the pgcli menu and padding
|
2018-10-03 00:10:43 +00:00
|
|
|
if self.is_too_tall(lines) or any(self.is_too_wide(l) for l in lines):
|
2018-05-15 17:42:21 +00:00
|
|
|
click.echo_via_pager(text, color=color)
|
|
|
|
else:
|
|
|
|
click.echo(text, color=color)
|
2018-01-06 10:58:59 +00:00
|
|
|
else:
|
|
|
|
click.echo_via_pager(text, color)
|
2015-06-25 10:27:29 +00:00
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
@click.command()
|
2015-01-08 12:58:44 +00:00
|
|
|
# Default host is '' so psycopg2 can default to either localhost or unix socket
|
|
|
|
@click.option('-h', '--host', default='', envvar='PGHOST',
|
2015-01-06 20:06:27 +00:00
|
|
|
help='Host address of the postgres database.')
|
2014-12-11 18:10:26 +00:00
|
|
|
@click.option('-p', '--port', default=5432, help='Port number at which the '
|
2018-01-06 01:27:51 +00:00
|
|
|
'postgres instance is listening.', envvar='PGPORT', type=click.INT)
|
2018-10-30 22:19:23 +00:00
|
|
|
@click.option('-U', '--username', 'username_opt', help='Username to connect to the postgres database.')
|
2018-11-30 23:21:12 +00:00
|
|
|
@click.option('-u', '--user', 'username_opt', help='Username to connect to the postgres database.')
|
2015-01-07 21:50:48 +00:00
|
|
|
@click.option('-W', '--password', 'prompt_passwd', is_flag=True, default=False,
|
2018-10-30 22:19:23 +00:00
|
|
|
help='Force password prompt.')
|
2015-01-07 21:50:48 +00:00
|
|
|
@click.option('-w', '--no-password', 'never_prompt', is_flag=True,
|
2018-10-30 22:19:23 +00:00
|
|
|
default=False, help='Never prompt for password.')
|
2016-07-21 09:50:21 +00:00
|
|
|
@click.option('--single-connection', 'single_connection', is_flag=True,
|
2018-10-30 22:19:23 +00:00
|
|
|
default=False,
|
|
|
|
help='Do not use a separate connection for completions.')
|
2015-02-14 01:02:24 +00:00
|
|
|
@click.option('-v', '--version', is_flag=True, help='Version of pgcli.')
|
2018-10-30 22:19:23 +00:00
|
|
|
@click.option('-d', '--dbname', 'dbname_opt', help='database name to connect to.')
|
2015-11-05 02:21:59 +00:00
|
|
|
@click.option('--pgclirc', default=config_location() + 'config',
|
2018-01-06 01:27:51 +00:00
|
|
|
envvar='PGCLIRC', help='Location of pgclirc file.', type=click.Path(dir_okay=False))
|
2016-03-25 02:50:17 +00:00
|
|
|
@click.option('-D', '--dsn', default='', envvar='DSN',
|
|
|
|
help='Use DSN configured into the [alias_dsn] section of pgclirc file.')
|
2018-02-10 16:00:44 +00:00
|
|
|
@click.option('--list-dsn', 'list_dsn', is_flag=True,
|
2018-02-17 21:02:06 +00:00
|
|
|
help='list of DSN configured into the [alias_dsn] section of pgclirc file.')
|
2017-04-05 13:25:04 +00:00
|
|
|
@click.option('--row-limit', default=None, envvar='PGROWLIMIT', type=click.INT,
|
2016-06-02 19:36:48 +00:00
|
|
|
help='Set threshold for row limit prompt. Use 0 to disable prompt.')
|
2017-03-10 08:44:24 +00:00
|
|
|
@click.option('--less-chatty', 'less_chatty', is_flag=True,
|
2017-03-11 08:31:02 +00:00
|
|
|
default=False,
|
2017-03-10 08:44:24 +00:00
|
|
|
help='Skip intro on startup and goodbye on exit.')
|
2016-11-15 01:19:28 +00:00
|
|
|
@click.option('--prompt', help='Prompt format (Default: "\\u@\\h:\\d> ").')
|
2017-12-15 07:46:00 +00:00
|
|
|
@click.option('--prompt-dsn', help='Prompt format for connections using DSN aliases (Default: "\\u@\\h:\\d> ").')
|
2017-07-14 22:57:37 +00:00
|
|
|
@click.option('-l', '--list', 'list_databases', is_flag=True, help='list '
|
|
|
|
'available databases, then exit.')
|
2017-09-05 18:39:13 +00:00
|
|
|
@click.option('--auto-vertical-output', is_flag=True,
|
|
|
|
help='Automatically switch to vertical output mode if the result is wider than the terminal width.')
|
2018-05-17 09:54:50 +00:00
|
|
|
@click.option('--warn/--no-warn', default=None,
|
|
|
|
help='Warn before running a destructive query.')
|
2018-10-30 22:19:23 +00:00
|
|
|
@click.argument('dbname', default=lambda: None, envvar='PGDATABASE', nargs=1)
|
2015-01-14 18:07:35 +00:00
|
|
|
@click.argument('username', default=lambda: None, envvar='PGUSER', nargs=1)
|
2018-10-30 22:19:23 +00:00
|
|
|
def cli(dbname, username_opt, host, port, prompt_passwd, never_prompt,
|
|
|
|
single_connection, dbname_opt, username, version, pgclirc, dsn, row_limit,
|
2018-02-10 16:00:44 +00:00
|
|
|
less_chatty, prompt, prompt_dsn, list_databases, auto_vertical_output,
|
2018-05-17 09:54:50 +00:00
|
|
|
list_dsn, warn):
|
2015-02-14 01:02:24 +00:00
|
|
|
|
|
|
|
if version:
|
|
|
|
print('Version:', __version__)
|
|
|
|
sys.exit(0)
|
|
|
|
|
2015-09-23 05:05:03 +00:00
|
|
|
config_dir = os.path.dirname(config_location())
|
|
|
|
if not os.path.exists(config_dir):
|
|
|
|
os.makedirs(config_dir)
|
2015-09-20 21:57:12 +00:00
|
|
|
|
2015-09-23 05:05:03 +00:00
|
|
|
# Migrate the config file from old location.
|
2015-11-05 16:15:32 +00:00
|
|
|
config_full_path = config_location() + 'config'
|
2015-09-20 21:57:12 +00:00
|
|
|
if os.path.exists(os.path.expanduser('~/.pgclirc')):
|
2015-11-05 16:15:32 +00:00
|
|
|
if not os.path.exists(config_full_path):
|
|
|
|
shutil.move(os.path.expanduser('~/.pgclirc'), config_full_path)
|
2015-09-20 21:57:12 +00:00
|
|
|
print ('Config file (~/.pgclirc) moved to new location',
|
2015-11-05 16:15:32 +00:00
|
|
|
config_full_path)
|
2015-09-20 21:57:12 +00:00
|
|
|
else:
|
2015-11-05 16:15:32 +00:00
|
|
|
print ('Config file is now located at', config_full_path)
|
2015-09-20 21:57:12 +00:00
|
|
|
print ('Please move the existing config file ~/.pgclirc to',
|
2015-11-05 16:15:32 +00:00
|
|
|
config_full_path)
|
2018-02-17 21:02:06 +00:00
|
|
|
if list_dsn:
|
2018-02-10 16:00:44 +00:00
|
|
|
try:
|
|
|
|
cfg = load_config(pgclirc, config_full_path)
|
|
|
|
for alias in cfg['alias_dsn']:
|
|
|
|
click.secho(alias + " : " + cfg['alias_dsn'][alias])
|
|
|
|
sys.exit(0)
|
|
|
|
except Exception as err:
|
2018-02-17 21:02:06 +00:00
|
|
|
click.secho('Invalid DSNs found in the config file. '
|
|
|
|
'Please check the "[alias_dsn]" section in pgclirc.',
|
|
|
|
err=True, fg='red')
|
2018-02-10 16:00:44 +00:00
|
|
|
exit(1)
|
2018-02-17 21:02:06 +00:00
|
|
|
|
2016-06-02 19:36:48 +00:00
|
|
|
pgcli = PGCli(prompt_passwd, never_prompt, pgclirc_file=pgclirc,
|
2017-03-10 08:44:24 +00:00
|
|
|
row_limit=row_limit, single_connection=single_connection,
|
2017-11-17 05:33:28 +00:00
|
|
|
less_chatty=less_chatty, prompt=prompt, prompt_dsn=prompt_dsn,
|
2018-05-17 09:54:50 +00:00
|
|
|
auto_vertical_output=auto_vertical_output, warn=warn)
|
2015-01-09 00:27:28 +00:00
|
|
|
|
2015-01-14 18:07:35 +00:00
|
|
|
# Choose which ever one has a valid value.
|
2018-10-30 22:19:23 +00:00
|
|
|
if dbname_opt and dbname:
|
|
|
|
# work as psql: when database is given as option and argument use the argument as user
|
|
|
|
username = dbname
|
|
|
|
database = dbname_opt or dbname or ''
|
2017-04-30 23:42:32 +00:00
|
|
|
user = username_opt or username
|
2015-01-14 05:53:53 +00:00
|
|
|
|
2017-12-16 15:34:10 +00:00
|
|
|
# because option --list or -l are not supposed to have a db name
|
|
|
|
if list_databases:
|
|
|
|
database = 'postgres'
|
|
|
|
|
2016-03-25 02:50:17 +00:00
|
|
|
if dsn is not '':
|
|
|
|
try:
|
2017-05-28 22:03:31 +00:00
|
|
|
cfg = load_config(pgclirc, config_full_path)
|
2016-03-25 02:50:17 +00:00
|
|
|
dsn_config = cfg['alias_dsn'][dsn]
|
|
|
|
except:
|
|
|
|
click.secho('Invalid DSNs found in the config file. '\
|
|
|
|
'Please check the "[alias_dsn]" section in pgclirc.',
|
|
|
|
err=True, fg='red')
|
|
|
|
exit(1)
|
|
|
|
pgcli.connect_uri(dsn_config)
|
2017-12-11 03:47:33 +00:00
|
|
|
pgcli.dsn_alias = dsn
|
2016-03-25 02:50:17 +00:00
|
|
|
elif '://' in database:
|
2015-01-09 00:27:28 +00:00
|
|
|
pgcli.connect_uri(database)
|
2015-08-07 23:32:39 +00:00
|
|
|
elif "=" in database:
|
|
|
|
pgcli.connect_dsn(database)
|
|
|
|
elif os.environ.get('PGSERVICE', None):
|
|
|
|
pgcli.connect_dsn('service={0}'.format(os.environ['PGSERVICE']))
|
2015-01-09 00:27:28 +00:00
|
|
|
else:
|
|
|
|
pgcli.connect(database, host, user, port)
|
|
|
|
|
2017-07-14 22:57:37 +00:00
|
|
|
if list_databases:
|
|
|
|
cur, headers, status = pgcli.pgexecute.full_databases()
|
|
|
|
|
|
|
|
title = 'List of databases'
|
|
|
|
settings = OutputSettings(
|
|
|
|
table_format='ascii',
|
|
|
|
missingval='<null>'
|
|
|
|
)
|
|
|
|
formatted = format_output(title, cur, headers, status, settings)
|
2018-03-22 10:06:06 +00:00
|
|
|
pgcli.echo_via_pager('\n'.join(formatted))
|
2017-07-14 22:57:37 +00:00
|
|
|
|
|
|
|
sys.exit(0)
|
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
pgcli.logger.debug('Launch Params: \n'
|
2015-01-04 22:52:04 +00:00
|
|
|
'\tdatabase: %r'
|
|
|
|
'\tuser: %r'
|
|
|
|
'\thost: %r'
|
2015-01-09 00:27:28 +00:00
|
|
|
'\tport: %r', database, user, host, port)
|
2014-10-12 17:31:54 +00:00
|
|
|
|
2015-10-23 09:49:20 +00:00
|
|
|
if setproctitle:
|
|
|
|
obfuscate_process_password()
|
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
pgcli.run_cli()
|
2014-12-05 16:56:59 +00:00
|
|
|
|
2015-10-28 13:23:23 +00:00
|
|
|
|
2015-10-19 17:40:50 +00:00
|
|
|
def obfuscate_process_password():
|
2015-10-18 20:55:56 +00:00
|
|
|
process_title = setproctitle.getproctitle()
|
|
|
|
if '://' in process_title:
|
|
|
|
process_title = re.sub(r":(.*):(.*)@", r":\1:xxxx@", process_title)
|
|
|
|
elif "=" in process_title:
|
2015-10-23 09:49:20 +00:00
|
|
|
process_title = re.sub(r"password=(.+?)((\s[a-zA-Z]+=)|$)", r"password=xxxx\2", process_title)
|
2015-10-18 20:55:56 +00:00
|
|
|
|
|
|
|
setproctitle.setproctitle(process_title)
|
|
|
|
|
2016-09-27 10:43:05 +00:00
|
|
|
|
2015-10-28 13:23:23 +00:00
|
|
|
def has_meta_cmd(query):
|
2015-01-31 21:46:32 +00:00
|
|
|
"""Determines if the completion needs a refresh by checking if the sql
|
2017-05-29 12:59:16 +00:00
|
|
|
statement is an alter, create, drop, commit or rollback."""
|
2015-10-28 13:23:23 +00:00
|
|
|
try:
|
|
|
|
first_token = query.split()[0]
|
2017-05-29 00:56:48 +00:00
|
|
|
if first_token.lower() in ('alter', 'create', 'drop', 'commit', 'rollback'):
|
2015-10-28 13:23:23 +00:00
|
|
|
return True
|
|
|
|
except Exception:
|
|
|
|
return False
|
2015-01-31 21:46:32 +00:00
|
|
|
|
2015-10-03 18:41:31 +00:00
|
|
|
return False
|
|
|
|
|
2015-10-18 14:50:08 +00:00
|
|
|
|
2015-10-28 13:23:23 +00:00
|
|
|
def has_change_db_cmd(query):
|
|
|
|
"""Determines if the statement is a database switch such as 'use' or '\\c'"""
|
|
|
|
try:
|
|
|
|
first_token = query.split()[0]
|
|
|
|
if first_token.lower() in ('use', '\\c', '\\connect'):
|
|
|
|
return True
|
|
|
|
except Exception:
|
|
|
|
return False
|
2015-10-18 14:50:08 +00:00
|
|
|
|
2015-10-28 13:23:23 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def has_change_path_cmd(sql):
|
2015-01-31 21:46:32 +00:00
|
|
|
"""Determines if the search_path should be refreshed by checking if the
|
|
|
|
sql has 'set search_path'."""
|
|
|
|
return 'set search_path' in sql.lower()
|
2015-01-26 13:05:39 +00:00
|
|
|
|
2015-10-28 13:23:23 +00:00
|
|
|
|
2015-01-10 22:52:50 +00:00
|
|
|
def is_mutating(status):
|
2015-01-18 08:25:36 +00:00
|
|
|
"""Determines if the statement is mutating based on the status."""
|
2015-01-14 12:51:16 +00:00
|
|
|
if not status:
|
|
|
|
return False
|
2015-01-16 07:41:43 +00:00
|
|
|
|
2015-10-28 13:23:23 +00:00
|
|
|
mutating = set(['insert', 'update', 'delete'])
|
2015-01-10 22:52:50 +00:00
|
|
|
return status.split(None, 1)[0].lower() in mutating
|
|
|
|
|
2015-10-28 13:23:23 +00:00
|
|
|
|
2015-01-18 08:25:36 +00:00
|
|
|
def is_select(status):
|
|
|
|
"""Returns true if the first word in status is 'select'."""
|
|
|
|
if not status:
|
|
|
|
return False
|
|
|
|
return status.split(None, 1)[0].lower() == 'select'
|
|
|
|
|
2015-10-28 13:23:23 +00:00
|
|
|
|
2015-10-21 20:56:11 +00:00
|
|
|
def exception_formatter(e):
|
2015-10-24 17:43:28 +00:00
|
|
|
return click.style(utf8tounicode(str(e)), fg='red')
|
2015-10-21 20:56:11 +00:00
|
|
|
|
|
|
|
|
2017-06-26 15:10:41 +00:00
|
|
|
def format_output(title, cur, headers, status, settings):
|
|
|
|
output = []
|
|
|
|
expanded = (settings.expanded or settings.table_format == 'vertical')
|
|
|
|
table_format = ('vertical' if settings.expanded else
|
|
|
|
settings.table_format)
|
|
|
|
max_width = settings.max_width
|
|
|
|
case_function = settings.case_function
|
|
|
|
formatter = TabularOutputFormatter(format_name=table_format)
|
|
|
|
|
2017-08-06 17:13:46 +00:00
|
|
|
def format_array(val):
|
|
|
|
if val is None:
|
|
|
|
return settings.missingval
|
|
|
|
if not isinstance(val, list):
|
|
|
|
return val
|
2017-08-06 23:16:56 +00:00
|
|
|
return '{' + ','.join(text_type(format_array(e)) for e in val) + '}'
|
2017-08-06 17:13:46 +00:00
|
|
|
|
2017-08-07 00:34:39 +00:00
|
|
|
def format_arrays(data, headers, **_):
|
2017-08-07 00:09:16 +00:00
|
|
|
data = list(data)
|
2017-08-06 17:13:46 +00:00
|
|
|
for row in data:
|
2017-08-07 01:31:18 +00:00
|
|
|
row[:] = [
|
|
|
|
format_array(val) if isinstance(val, list) else val
|
|
|
|
for val in row
|
|
|
|
]
|
2017-08-06 17:13:46 +00:00
|
|
|
|
|
|
|
return data, headers
|
|
|
|
|
2017-06-26 15:10:41 +00:00
|
|
|
output_kwargs = {
|
|
|
|
'sep_title': 'RECORD {n}',
|
|
|
|
'sep_character': '-',
|
|
|
|
'sep_length': (1, 25),
|
|
|
|
'missing_value': settings.missingval,
|
|
|
|
'integer_format': settings.dcmlfmt,
|
|
|
|
'float_format': settings.floatfmt,
|
2017-08-06 17:13:46 +00:00
|
|
|
'preprocessors': (format_numbers, format_arrays),
|
2017-06-26 15:10:41 +00:00
|
|
|
'disable_numparse': True,
|
2018-01-11 20:47:56 +00:00
|
|
|
'preserve_whitespace': True,
|
|
|
|
'style': settings.style_output
|
2017-06-26 15:10:41 +00:00
|
|
|
}
|
2017-06-26 16:33:45 +00:00
|
|
|
if not settings.floatfmt:
|
2017-06-26 15:10:41 +00:00
|
|
|
output_kwargs['preprocessors'] = (align_decimals, )
|
|
|
|
|
|
|
|
if title: # Only print the title if it's not None.
|
|
|
|
output.append(title)
|
|
|
|
|
|
|
|
if cur:
|
|
|
|
headers = [case_function(utf8tounicode(x)) for x in headers]
|
2017-08-19 06:48:41 +00:00
|
|
|
if max_width is not None:
|
|
|
|
cur = list(cur)
|
|
|
|
column_types = None
|
|
|
|
if hasattr(cur, 'description'):
|
|
|
|
column_types = []
|
|
|
|
for d in cur.description:
|
|
|
|
if d[1] in psycopg2.extensions.DECIMAL.values or \
|
|
|
|
d[1] in psycopg2.extensions.FLOAT.values:
|
|
|
|
column_types.append(float)
|
|
|
|
if d[1] == psycopg2.extensions.INTEGER.values or \
|
|
|
|
d[1] in psycopg2.extensions.LONGINTEGER.values:
|
|
|
|
column_types.append(int)
|
|
|
|
else:
|
|
|
|
column_types.append(text_type)
|
|
|
|
formatted = formatter.format_output(cur, headers, **output_kwargs)
|
2017-08-18 18:40:16 +00:00
|
|
|
if isinstance(formatted, (text_type)):
|
|
|
|
formatted = iter(formatted.splitlines())
|
|
|
|
first_line = next(formatted)
|
|
|
|
formatted = itertools.chain([first_line], formatted)
|
|
|
|
|
2018-09-28 21:18:40 +00:00
|
|
|
if not expanded and max_width and len(first_line) > max_width and headers:
|
2017-06-26 15:10:41 +00:00
|
|
|
formatted = formatter.format_output(
|
2017-08-19 06:48:41 +00:00
|
|
|
cur, headers, format_name='vertical', column_types=None, **output_kwargs)
|
2017-08-18 18:40:16 +00:00
|
|
|
if isinstance(formatted, (text_type)):
|
|
|
|
formatted = iter(formatted.splitlines())
|
2017-06-26 15:10:41 +00:00
|
|
|
|
2017-08-18 18:40:16 +00:00
|
|
|
output = itertools.chain(output, formatted)
|
2017-06-26 15:10:41 +00:00
|
|
|
|
|
|
|
if status: # Only print the status if it's not None.
|
2017-08-18 18:40:16 +00:00
|
|
|
output = itertools.chain(output, [status])
|
2017-06-26 15:10:41 +00:00
|
|
|
|
|
|
|
return output
|
|
|
|
|
|
|
|
|
2015-01-06 19:49:29 +00:00
|
|
|
if __name__ == "__main__":
|
|
|
|
cli()
|