1
0
Fork 0

Merging upstream version 1.29.2.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-09 19:16:42 +01:00
parent 3180ec4213
commit f15fe4f59f
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
68 changed files with 3723 additions and 3336 deletions

View file

@ -34,6 +34,7 @@ def set_timing_enabled(val):
global TIMING_ENABLED
TIMING_ENABLED = val
@export
def set_pager_enabled(val):
global PAGER_ENABLED
@ -44,33 +45,35 @@ def set_pager_enabled(val):
def is_pager_enabled():
return PAGER_ENABLED
@export
@special_command('pager', '\\P [command]',
'Set PAGER. Print the query results via PAGER.',
arg_type=PARSED_QUERY, aliases=('\\P', ), case_sensitive=True)
@special_command(
"pager", "\\P [command]", "Set PAGER. Print the query results via PAGER.", arg_type=PARSED_QUERY, aliases=("\\P",), case_sensitive=True
)
def set_pager(arg, **_):
if arg:
os.environ['PAGER'] = arg
msg = 'PAGER set to %s.' % arg
os.environ["PAGER"] = arg
msg = "PAGER set to %s." % arg
set_pager_enabled(True)
else:
if 'PAGER' in os.environ:
msg = 'PAGER set to %s.' % os.environ['PAGER']
if "PAGER" in os.environ:
msg = "PAGER set to %s." % os.environ["PAGER"]
else:
# This uses click's default per echo_via_pager.
msg = 'Pager enabled.'
msg = "Pager enabled."
set_pager_enabled(True)
return [(None, None, None, msg)]
@export
@special_command('nopager', '\\n', 'Disable pager, print to stdout.',
arg_type=NO_QUERY, aliases=('\\n', ), case_sensitive=True)
@special_command("nopager", "\\n", "Disable pager, print to stdout.", arg_type=NO_QUERY, aliases=("\\n",), case_sensitive=True)
def disable_pager():
set_pager_enabled(False)
return [(None, None, None, 'Pager disabled.')]
return [(None, None, None, "Pager disabled.")]
@special_command('\\timing', '\\t', 'Toggle timing of commands.', arg_type=NO_QUERY, aliases=('\\t', ), case_sensitive=True)
@special_command("\\timing", "\\t", "Toggle timing of commands.", arg_type=NO_QUERY, aliases=("\\t",), case_sensitive=True)
def toggle_timing():
global TIMING_ENABLED
TIMING_ENABLED = not TIMING_ENABLED
@ -78,21 +81,26 @@ def toggle_timing():
message += "on." if TIMING_ENABLED else "off."
return [(None, None, None, message)]
@export
def is_timing_enabled():
return TIMING_ENABLED
@export
def set_expanded_output(val):
global use_expanded_output
use_expanded_output = val
@export
def is_expanded_output():
return use_expanded_output
_logger = logging.getLogger(__name__)
@export
def editor_command(command):
"""
@ -101,12 +109,13 @@ def editor_command(command):
"""
# It is possible to have `\e filename` or `SELECT * FROM \e`. So we check
# for both conditions.
return command.strip().endswith('\\e') or command.strip().startswith('\\e')
return command.strip().endswith("\\e") or command.strip().startswith("\\e")
@export
def get_filename(sql):
if sql.strip().startswith('\\e'):
command, _, filename = sql.partition(' ')
if sql.strip().startswith("\\e"):
command, _, filename = sql.partition(" ")
return filename.strip() or None
@ -118,9 +127,9 @@ def get_editor_query(sql):
# The reason we can't simply do .strip('\e') is that it strips characters,
# not a substring. So it'll strip "e" in the end of the sql also!
# Ex: "select * from style\e" -> "select * from styl".
pattern = re.compile(r'(^\\e|\\e$)')
pattern = re.compile(r"(^\\e|\\e$)")
while pattern.search(sql):
sql = pattern.sub('', sql)
sql = pattern.sub("", sql)
return sql
@ -135,25 +144,24 @@ def open_external_editor(filename=None, sql=None):
"""
message = None
filename = filename.strip().split(' ', 1)[0] if filename else None
filename = filename.strip().split(" ", 1)[0] if filename else None
sql = sql or ''
MARKER = '# Type your query above this line.\n'
sql = sql or ""
MARKER = "# Type your query above this line.\n"
# Populate the editor buffer with the partial sql (if available) and a
# placeholder comment.
query = click.edit(u'{sql}\n\n{marker}'.format(sql=sql, marker=MARKER),
filename=filename, extension='.sql')
query = click.edit("{sql}\n\n{marker}".format(sql=sql, marker=MARKER), filename=filename, extension=".sql")
if filename:
try:
with open(filename) as f:
query = f.read()
except IOError:
message = 'Error reading file: %s.' % filename
message = "Error reading file: %s." % filename
if query is not None:
query = query.split(MARKER, 1)[0].rstrip('\n')
query = query.split(MARKER, 1)[0].rstrip("\n")
else:
# Don't return None for the caller to deal with.
# Empty string is ok.
@ -171,7 +179,7 @@ def clip_command(command):
"""
# It is possible to have `\clip` or `SELECT * FROM \clip`. So we check
# for both conditions.
return command.strip().endswith('\\clip') or command.strip().startswith('\\clip')
return command.strip().endswith("\\clip") or command.strip().startswith("\\clip")
@export
@ -181,9 +189,9 @@ def get_clip_query(sql):
# The reason we can't simply do .strip('\clip') is that it strips characters,
# not a substring. So it'll strip "c" in the end of the sql also!
pattern = re.compile(r'(^\\clip|\\clip$)')
pattern = re.compile(r"(^\\clip|\\clip$)")
while pattern.search(sql):
sql = pattern.sub('', sql)
sql = pattern.sub("", sql)
return sql
@ -192,26 +200,26 @@ def get_clip_query(sql):
def copy_query_to_clipboard(sql=None):
"""Send query to the clipboard."""
sql = sql or ''
sql = sql or ""
message = None
try:
pyperclip.copy(u'{sql}'.format(sql=sql))
pyperclip.copy("{sql}".format(sql=sql))
except RuntimeError as e:
message = 'Error clipping query: %s.' % e.strerror
message = "Error clipping query: %s." % e.strerror
return message
@special_command('\\f', '\\f [name [args..]]', 'List or execute favorite queries.', arg_type=PARSED_QUERY, case_sensitive=True)
@special_command("\\f", "\\f [name [args..]]", "List or execute favorite queries.", arg_type=PARSED_QUERY, case_sensitive=True)
def execute_favorite_query(cur, arg, **_):
"""Returns (title, rows, headers, status)"""
if arg == '':
if arg == "":
for result in list_favorite_queries():
yield result
"""Parse out favorite name and optional substitution parameters"""
name, _, arg_str = arg.partition(' ')
name, _, arg_str = arg.partition(" ")
args = shlex.split(arg_str)
query = FavoriteQueries.instance.get(name)
@ -224,8 +232,8 @@ def execute_favorite_query(cur, arg, **_):
yield (None, None, None, arg_error)
else:
for sql in sqlparse.split(query):
sql = sql.rstrip(';')
title = '> %s' % (sql)
sql = sql.rstrip(";")
title = "> %s" % (sql)
cur.execute(sql)
if cur.description:
headers = [x[0] for x in cur.description]
@ -233,60 +241,60 @@ def execute_favorite_query(cur, arg, **_):
else:
yield (title, None, None, None)
def list_favorite_queries():
"""List of all favorite queries.
Returns (title, rows, headers, status)"""
headers = ["Name", "Query"]
rows = [(r, FavoriteQueries.instance.get(r))
for r in FavoriteQueries.instance.list()]
rows = [(r, FavoriteQueries.instance.get(r)) for r in FavoriteQueries.instance.list()]
if not rows:
status = '\nNo favorite queries found.' + FavoriteQueries.instance.usage
status = "\nNo favorite queries found." + FavoriteQueries.instance.usage
else:
status = ''
return [('', rows, headers, status)]
status = ""
return [("", rows, headers, status)]
def subst_favorite_query_args(query, args):
"""replace positional parameters ($1...$N) in query."""
for idx, val in enumerate(args):
subst_var = '$' + str(idx + 1)
subst_var = "$" + str(idx + 1)
if subst_var not in query:
return [None, 'query does not have substitution parameter ' + subst_var + ':\n ' + query]
return [None, "query does not have substitution parameter " + subst_var + ":\n " + query]
query = query.replace(subst_var, val)
match = re.search(r'\$\d+', query)
match = re.search(r"\$\d+", query)
if match:
return[None, 'missing substitution for ' + match.group(0) + ' in query:\n ' + query]
return [None, "missing substitution for " + match.group(0) + " in query:\n " + query]
return [query, None]
@special_command('\\fs', '\\fs name query', 'Save a favorite query.')
@special_command("\\fs", "\\fs name query", "Save a favorite query.")
def save_favorite_query(arg, **_):
"""Save a new favorite query.
Returns (title, rows, headers, status)"""
usage = 'Syntax: \\fs name query.\n\n' + FavoriteQueries.instance.usage
usage = "Syntax: \\fs name query.\n\n" + FavoriteQueries.instance.usage
if not arg:
return [(None, None, None, usage)]
name, _, query = arg.partition(' ')
name, _, query = arg.partition(" ")
# If either name or query is missing then print the usage and complain.
if (not name) or (not query):
return [(None, None, None,
usage + 'Err: Both name and query are required.')]
return [(None, None, None, usage + "Err: Both name and query are required.")]
FavoriteQueries.instance.save(name, query)
return [(None, None, None, "Saved.")]
@special_command('\\fd', '\\fd [name]', 'Delete a favorite query.')
@special_command("\\fd", "\\fd [name]", "Delete a favorite query.")
def delete_favorite_query(arg, **_):
"""Delete an existing favorite query."""
usage = 'Syntax: \\fd name.\n\n' + FavoriteQueries.instance.usage
usage = "Syntax: \\fd name.\n\n" + FavoriteQueries.instance.usage
if not arg:
return [(None, None, None, usage)]
@ -295,8 +303,7 @@ def delete_favorite_query(arg, **_):
return [(None, None, None, status)]
@special_command('system', 'system [command]',
'Execute a system shell commmand.')
@special_command("system", "system [command]", "Execute a system shell commmand.")
def execute_system_command(arg, **_):
"""Execute a system shell command."""
usage = "Syntax: system [command].\n"
@ -306,13 +313,13 @@ def execute_system_command(arg, **_):
try:
command = arg.strip()
if command.startswith('cd'):
if command.startswith("cd"):
ok, error_message = handle_cd_command(arg)
if not ok:
return [(None, None, None, error_message)]
return [(None, None, None, '')]
return [(None, None, None, "")]
args = arg.split(' ')
args = arg.split(" ")
process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, error = process.communicate()
response = output if not error else error
@ -324,25 +331,24 @@ def execute_system_command(arg, **_):
return [(None, None, None, response)]
except OSError as e:
return [(None, None, None, 'OSError: %s' % e.strerror)]
return [(None, None, None, "OSError: %s" % e.strerror)]
def parseargfile(arg):
if arg.startswith('-o '):
if arg.startswith("-o "):
mode = "w"
filename = arg[3:]
else:
mode = 'a'
mode = "a"
filename = arg
if not filename:
raise TypeError('You must provide a filename.')
raise TypeError("You must provide a filename.")
return {'file': os.path.expanduser(filename), 'mode': mode}
return {"file": os.path.expanduser(filename), "mode": mode}
@special_command('tee', 'tee [-o] filename',
'Append all results to an output file (overwrite using -o).')
@special_command("tee", "tee [-o] filename", "Append all results to an output file (overwrite using -o).")
def set_tee(arg, **_):
global tee_file
@ -353,6 +359,7 @@ def set_tee(arg, **_):
return [(None, None, None, "")]
@export
def close_tee():
global tee_file
@ -361,31 +368,29 @@ def close_tee():
tee_file = None
@special_command('notee', 'notee', 'Stop writing results to an output file.')
@special_command("notee", "notee", "Stop writing results to an output file.")
def no_tee(arg, **_):
close_tee()
return [(None, None, None, "")]
@export
def write_tee(output):
global tee_file
if tee_file:
click.echo(output, file=tee_file, nl=False)
click.echo(u'\n', file=tee_file, nl=False)
click.echo("\n", file=tee_file, nl=False)
tee_file.flush()
@special_command('\\once', '\\o [-o] filename',
'Append next result to an output file (overwrite using -o).',
aliases=('\\o', ))
@special_command("\\once", "\\o [-o] filename", "Append next result to an output file (overwrite using -o).", aliases=("\\o",))
def set_once(arg, **_):
global once_file, written_to_once_file
try:
once_file = open(**parseargfile(arg))
except (IOError, OSError) as e:
raise OSError("Cannot write to file '{}': {}".format(
e.filename, e.strerror))
raise OSError("Cannot write to file '{}': {}".format(e.filename, e.strerror))
written_to_once_file = False
return [(None, None, None, "")]
@ -396,7 +401,7 @@ def write_once(output):
global once_file, written_to_once_file
if output and once_file:
click.echo(output, file=once_file, nl=False)
click.echo(u"\n", file=once_file, nl=False)
click.echo("\n", file=once_file, nl=False)
once_file.flush()
written_to_once_file = True
@ -410,22 +415,22 @@ def unset_once_if_written():
once_file = None
@special_command('\\pipe_once', '\\| command',
'Send next result to a subprocess.',
aliases=('\\|', ))
@special_command("\\pipe_once", "\\| command", "Send next result to a subprocess.", aliases=("\\|",))
def set_pipe_once(arg, **_):
global pipe_once_process, written_to_pipe_once_process
pipe_once_cmd = shlex.split(arg)
if len(pipe_once_cmd) == 0:
raise OSError("pipe_once requires a command")
written_to_pipe_once_process = False
pipe_once_process = subprocess.Popen(pipe_once_cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
bufsize=1,
encoding='UTF-8',
universal_newlines=True)
pipe_once_process = subprocess.Popen(
pipe_once_cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
bufsize=1,
encoding="UTF-8",
universal_newlines=True,
)
return [(None, None, None, "")]
@ -435,11 +440,10 @@ def write_pipe_once(output):
if output and pipe_once_process:
try:
click.echo(output, file=pipe_once_process.stdin, nl=False)
click.echo(u"\n", file=pipe_once_process.stdin, nl=False)
click.echo("\n", file=pipe_once_process.stdin, nl=False)
except (IOError, OSError) as e:
pipe_once_process.terminate()
raise OSError(
"Failed writing to pipe_once subprocess: {}".format(e.strerror))
raise OSError("Failed writing to pipe_once subprocess: {}".format(e.strerror))
written_to_pipe_once_process = True
@ -450,18 +454,14 @@ def unset_pipe_once_if_written():
if written_to_pipe_once_process:
(stdout_data, stderr_data) = pipe_once_process.communicate()
if len(stdout_data) > 0:
print(stdout_data.rstrip(u"\n"))
print(stdout_data.rstrip("\n"))
if len(stderr_data) > 0:
print(stderr_data.rstrip(u"\n"))
print(stderr_data.rstrip("\n"))
pipe_once_process = None
written_to_pipe_once_process = False
@special_command(
'watch',
'watch [seconds] [-c] query',
'Executes the query every [seconds] seconds (by default 5).'
)
@special_command("watch", "watch [seconds] [-c] query", "Executes the query every [seconds] seconds (by default 5).")
def watch_query(arg, **kwargs):
usage = """Syntax: watch [seconds] [-c] query.
* seconds: The interval at the query will be repeated, in seconds.
@ -480,27 +480,24 @@ def watch_query(arg, **kwargs):
# Oops, we parsed all the arguments without finding a statement
yield (None, None, None, usage)
return
(current_arg, _, arg) = arg.partition(' ')
(current_arg, _, arg) = arg.partition(" ")
try:
seconds = float(current_arg)
continue
except ValueError:
pass
if current_arg == '-c':
if current_arg == "-c":
clear_screen = True
continue
statement = '{0!s} {1!s}'.format(current_arg, arg)
statement = "{0!s} {1!s}".format(current_arg, arg)
destructive_prompt = confirm_destructive_query(statement)
if destructive_prompt is False:
click.secho("Wise choice!")
return
elif destructive_prompt is True:
click.secho("Your call!")
cur = kwargs['cur']
sql_list = [
(sql.rstrip(';'), "> {0!s}".format(sql))
for sql in sqlparse.split(statement)
]
cur = kwargs["cur"]
sql_list = [(sql.rstrip(";"), "> {0!s}".format(sql)) for sql in sqlparse.split(statement)]
old_pager_enabled = is_pager_enabled()
while True:
if clear_screen:
@ -509,7 +506,7 @@ def watch_query(arg, **kwargs):
# Somewhere in the code the pager its activated after every yield,
# so we disable it in every iteration
set_pager_enabled(False)
for (sql, title) in sql_list:
for sql, title in sql_list:
cur.execute(sql)
if cur.description:
headers = [x[0] for x in cur.description]
@ -527,7 +524,7 @@ def watch_query(arg, **kwargs):
@export
@special_command('delimiter', None, 'Change SQL delimiter.')
@special_command("delimiter", None, "Change SQL delimiter.")
def set_delimiter(arg, **_):
return delimiter_command.set(arg)