Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Features
* Advertise beta `--boundary-id` option in helpdoc and completions.
* Add `/source --special` to allow executing some special commands.
* Add `/source --show` to display each query before executing it.
* Add `/source --page` to display all output in one pager session.


Bug Fixes
Expand Down
23 changes: 17 additions & 6 deletions mycli/client_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,22 @@ def _render_config_value(value: Any) -> str:
return str(value)


def _parse_source_arguments(arg: str) -> tuple[str, bool, bool]:
def _parse_source_arguments(arg: str) -> tuple[str, bool, bool, bool]:
allow_special = False
show_queries = False
page_output = False
filename = arg
while arguments := filename.split(maxsplit=1):
if arguments[0] == '--special':
allow_special = True
elif arguments[0] == '--show':
show_queries = True
elif arguments[0] == '--page':
page_output = True
else:
break
filename = arguments[1] if len(arguments) == 2 else ''
return filename, allow_special, show_queries
return filename, allow_special, show_queries, page_output


def _registered_special_command(query: str) -> tuple[str, str] | None:
Expand Down Expand Up @@ -226,7 +229,7 @@ def register_special_commands(self) -> None:
special.register_special_command(
self.execute_from_file,
"source",
"/source [--special] [--show] <file>",
"/source [--special|--show|--page] <file>",
"Execute queries from a file.",
aliases=[SpecialCommandAlias("\\.", case_sensitive=False)],
)
Expand Down Expand Up @@ -349,7 +352,9 @@ def change_db(self, arg: str, **_) -> Generator[SQLResult, None, None]:
yield SQLResult(status=msg)

def execute_from_file(self, arg: str, **_) -> Generator[SQLResult, None, None]:
filename, allow_special, show_queries = _parse_source_arguments(arg)
filename, allow_special, show_queries, page_output = _parse_source_arguments(arg)
if page_output:
yield SQLResult(command={'name': 'source_page'})
if not filename:
yield SQLResult(status="Missing required argument: filename.")
return
Expand Down Expand Up @@ -388,14 +393,20 @@ def execute_from_file(self, arg: str, **_) -> Generator[SQLResult, None, None]:
)
return
if show_queries:
click.secho(f'> {special_query}')
if page_output:
yield SQLResult(command={'name': 'source_show', 'text': special_query})
else:
click.secho(f'> {special_query}')
yield from self.sqlexecute.run(special_query)
continue

if self.destructive_warning and confirm_destructive_query(self.destructive_keywords, query) is False:
continue
if show_queries:
click.secho(f'> {query}')
if page_output:
yield SQLResult(command={'name': 'source_show', 'text': query})
else:
click.secho(f'> {query}')
yield from self.sqlexecute.run(query)

def change_prompt_format(self, arg: str, **_) -> list[SQLResult]:
Expand Down
147 changes: 146 additions & 1 deletion mycli/main_modes/repl.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from __future__ import annotations

from collections.abc import Iterable
from collections.abc import Generator, Iterable
from dataclasses import dataclass
from datetime import datetime
import functools
from functools import partial
import html
from importlib import resources
import itertools
import os
import random
import re
Expand Down Expand Up @@ -441,6 +442,27 @@ def _output_results(
sqlexecute = mycli.sqlexecute
assert sqlexecute is not None

result_iterator = iter(results)
try:
first_result = next(result_iterator)
except StopIteration:
return

if first_result.command is not None and first_result.command['name'] == 'source_page':
if special.is_redirected():
_output_results(mycli, state, result_iterator, start)
return
paged_output = _single_paged_output_results(mycli, state, result_iterator, start)
try:
click.echo_via_pager(paged_output)
except KeyboardInterrupt:
pass
finally:
paged_output.close()
return

results = itertools.chain([first_result], result_iterator)

result_count = 0
watch_count = 0
for result in results:
Expand All @@ -450,6 +472,9 @@ def _output_results(
mycli.logger.debug('status: %r', result.status)
mycli.logger.debug('command: %r', result.command)
threshold = 1000
if result.command is not None and result.command['name'] == 'source_show':
click.secho(f"> {result.command['text']}")
continue
if result.command is not None and result.command['name'] == 'set_buffer':
state.buffer_text = str(result.command['text'])
continue
Expand Down Expand Up @@ -534,6 +559,126 @@ def _output_results(
mycli.output_timing(f'Time: {warnings_duration:0.03f}s', is_warnings_style=True)


def _single_paged_output_results(
mycli: 'MyCli',
state: ReplState,
results: Iterable[SQLResult],
start: float,
) -> Generator[str, None, None]:
"""Render results lazily through one pager session."""
sqlexecute = mycli.sqlexecute
assert sqlexecute is not None
result_iterator = iter(results)
result_count = 0
watch_count = 0
try:
for result in result_iterator:
mycli.logger.debug('preamble: %r', result.preamble)
mycli.logger.debug('header: %r', result.header)
mycli.logger.debug('rows: %r', result.rows)
mycli.logger.debug('status: %r', result.status)
mycli.logger.debug('command: %r', result.command)

if result.command is not None and result.command['name'] == 'source_show':
yield f"> {result.command['text']}\n"
continue
if result.command is not None and result.command['name'] == 'set_buffer':
state.buffer_text = str(result.command['text'])
continue
if result.command is not None and result.command['name'] == 'watch':
if watch_count > 0:
try:
start += float(result.command['seconds'])
except ValueError as error:
message = f'Invalid watch sleep time provided ({error}).'
mycli.log_output(message)
yield f'{message}\n'
return
else:
watch_count += 1

if mycli.auto_vertical_output:
if mycli.prompt_session is not None:
max_width = mycli.prompt_session.output.get_size().columns
else:
max_width = DEFAULT_WIDTH
else:
max_width = None

formatted = mycli.format_sqlresult(
result,
is_expanded=special.is_expanded_output(),
is_redirected=False,
null_string=mycli.null_string,
numeric_alignment=mycli.numeric_alignment,
binary_display=mycli.binary_display,
max_width=max_width,
)
duration = time.time() - start

if result_count > 0:
mycli.log_output('')
yield '\n'
for line in formatted:
mycli.log_output(line)
special.write_tee(line)
special.write_once(line)
special.write_pipe_once(line)
yield f'{line}\n'
if result.status:
mycli.log_output(result.status_plain)
yield f'{result.status_plain}\n'

if mycli.beep_after_seconds > 0 and duration >= mycli.beep_after_seconds:
assert mycli.prompt_session is not None
mycli.prompt_session.output.bell()
if special.is_timing_enabled():
timing = f'Time: {duration:0.03f}s'
mycli.log_output(timing)
yield f'{timing}\n'

start = time.time()
result_count += 1
state.mutating = state.mutating or is_mutating(result.status_plain)

if special.is_show_warnings_enabled() and isinstance(result.rows, Cursor) and result.rows.warning_count > 0:
warnings = sqlexecute.run('SHOW WARNINGS')
warnings_duration = time.time() - start
saw_warning = False
for warning in warnings:
saw_warning = True
warning_output = mycli.format_sqlresult(
warning,
is_expanded=special.is_expanded_output(),
is_redirected=False,
null_string=mycli.null_string,
numeric_alignment=mycli.numeric_alignment,
binary_display=mycli.binary_display,
max_width=max_width,
is_warnings_style=True,
)
mycli.log_output('')
yield '\n'
for line in warning_output:
mycli.log_output(line)
special.write_tee(line)
special.write_once(line)
special.write_pipe_once(line)
yield f'{line}\n'
if warning.status:
mycli.log_output(warning.status_plain)
yield f'{warning.status_plain}\n'

if saw_warning and special.is_timing_enabled():
timing = f'Time: {warnings_duration:0.03f}s'
mycli.log_output(timing)
yield f'{timing}\n'
finally:
close = getattr(result_iterator, 'close', None)
if close is not None:
close()


def _keepalive_hook(
mycli: 'MyCli',
_context: Any,
Expand Down
2 changes: 1 addition & 1 deletion mycli/packages/completion_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -812,7 +812,7 @@ def suggest_special(text: str) -> list[dict[str, Any]]:
'source',
'/source',
]:
source_options = ['--special', '--show']
source_options = ['--special', '--show', '--page']
source_arguments = _arg.split()
if not source_arguments:
return [
Expand Down
Loading
Loading