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
5 changes: 5 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
Upcoming (TBD)
==============

Features
--------
* Add a `/source --throttle` option to pause between sourced statements.


Bug Fixes
--------
* Allow shell-style redirects with `/source` when the filename is unquoted.
Expand Down
16 changes: 14 additions & 2 deletions mycli/client_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging
import os
import re
import time
from typing import TYPE_CHECKING, Any, cast

import click
Expand Down Expand Up @@ -155,7 +156,7 @@ def register_special_commands(self) -> None:
special.register_special_command(
self.execute_from_file,
"source",
"/source [--special|--show|--page] <file>",
"/source [options] <file>",
"Execute queries from a file.",
aliases=[SpecialCommandAlias("\\.", case_sensitive=False)],
completion_snippet='execute queries from file',
Expand Down Expand Up @@ -288,7 +289,11 @@ 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, page_output = parse_source_arguments(arg)
try:
filename, allow_special, show_queries, page_output, throttle = parse_source_arguments(arg)
except ValueError as error:
yield SQLResult(status=str(error), is_error=True)
return
if page_output:
yield SQLResult(command={'name': 'source_page'})
try:
Expand All @@ -307,6 +312,7 @@ def execute_from_file(self, arg: str, **_) -> Generator[SQLResult, None, None]:
return

assert isinstance(self.sqlexecute, SQLExecute)
executed_statement = False
with file_h:
statements = statements_from_filehandle(file_h)
while True:
Expand All @@ -333,22 +339,28 @@ def execute_from_file(self, arg: str, **_) -> Generator[SQLResult, None, None]:
is_error=True,
)
return
if executed_statement and throttle > 0:
time.sleep(throttle)
if show_queries:
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)
executed_statement = True
continue

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

def change_prompt_format(self, arg: str, **_) -> list[SQLResult]:
"""
Expand Down
28 changes: 24 additions & 4 deletions mycli/packages/completion_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -816,7 +816,8 @@ def suggest_special(text: str) -> list[dict[str, Any]]:
'source',
'/source',
]:
source_options = ['--special', '--show', '--page']
source_options = ['--special', '--show', '--page', '--throttle']
source_boolean_options = source_options[:-1]
source_arguments = _arg.split()
if not source_arguments:
return [
Expand All @@ -826,9 +827,28 @@ def suggest_special(text: str) -> list[dict[str, Any]]:

used_options: set[str] = set()
argument_index = 0
while argument_index < len(source_arguments) and source_arguments[argument_index] in source_options:
used_options.add(source_arguments[argument_index])
argument_index += 1
while argument_index < len(source_arguments):
argument = source_arguments[argument_index]
if argument in source_boolean_options:
used_options.add(argument)
argument_index += 1
continue
if argument == '--throttle':
used_options.add(argument)
argument_index += 1
if argument_index >= len(source_arguments):
return []
if argument_index == len(source_arguments) - 1 and not text[-1].isspace():
return []
argument_index += 1
continue
if argument.startswith('--throttle='):
used_options.add('--throttle')
if argument == '--throttle=' or not text[-1].isspace():
return []
argument_index += 1
continue
break
remaining_options = [option for option in source_options if option not in used_options]
source_filename = _arg
for _index in range(argument_index):
Expand Down
5 changes: 3 additions & 2 deletions mycli/packages/hybrid_redirection.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
delimiter_command = DelimiterCommand()
SOURCE_COMMAND_PATTERN = re.compile(r'^([/]?source|[/\\]\.)\s+', re.IGNORECASE)
SOURCE_OPTIONS_PATTERN = re.compile(
r'^([/]?source|[/\\]\.)\s+(?P<options>(?:(?:--special|--show|--page)\s+)*)',
r'^([/]?source|[/\\]\.)\s+'
r'(?P<options>(?:(?:--special|--show|--page)\s+|--throttle(?:=[^\s]+|\s+[^\s]+)\s+)*)',
re.IGNORECASE,
)

Expand Down Expand Up @@ -67,7 +68,7 @@ def find_sql_part(
if SOURCE_COMMAND_PATTERN.match(sql_part):
source_arg_str = SOURCE_COMMAND_PATTERN.sub('', sql_part)
try:
filename, _allow_special, _show_queries, _page_output = parse_source_arguments(source_arg_str)
filename, _allow_special, _show_queries, _page_output, _throttle = parse_source_arguments(source_arg_str)
filename = parse_source_filename(filename)
except ValueError:
return ''
Expand Down
27 changes: 25 additions & 2 deletions mycli/packages/special/source.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import math
import shlex

import sqlparse
Expand Down Expand Up @@ -72,10 +73,23 @@ def _favorite_source_command_is_safe(arg: str) -> bool:
return not any(special.is_special_command(statement.rstrip(';')) for statement in sqlparse.split(query))


def parse_source_arguments(arg: str) -> tuple[str, bool, bool, bool]:
def _parse_throttle(value: str) -> float:
if not value:
raise ValueError('Missing value for --throttle.')
try:
throttle = float(value)
except ValueError:
raise ValueError(f'Invalid --throttle value: {value}. Expected a finite, non-negative number.') from None
if not math.isfinite(throttle) or throttle < 0:
raise ValueError(f'Invalid --throttle value: {value}. Expected a finite, non-negative number.')
return throttle


def parse_source_arguments(arg: str) -> tuple[str, bool, bool, bool, float]:
allow_special = False
show_queries = False
page_output = False
throttle = 0.0
filename = arg
while arguments := filename.split(maxsplit=1):
if arguments[0] == '--special':
Expand All @@ -84,10 +98,19 @@ def parse_source_arguments(arg: str) -> tuple[str, bool, bool, bool]:
show_queries = True
elif arguments[0] == '--page':
page_output = True
elif arguments[0] == '--throttle':
if len(arguments) != 2:
raise ValueError('Missing value for --throttle.')
throttle_arguments = arguments[1].split(maxsplit=1)
throttle = _parse_throttle(throttle_arguments[0])
filename = throttle_arguments[1] if len(throttle_arguments) == 2 else ''
continue
elif arguments[0].startswith('--throttle='):
throttle = _parse_throttle(arguments[0].partition('=')[2])
else:
break
filename = arguments[1] if len(arguments) == 2 else ''
return filename, allow_special, show_queries, page_output
return filename, allow_special, show_queries, page_output, throttle


def parse_source_filename(filename: str) -> str:
Expand Down
86 changes: 43 additions & 43 deletions test/features/fixture_data/help_commands.txt
Original file line number Diff line number Diff line change
@@ -1,43 +1,43 @@
+-----------------+----------+------------------------------------------+-------------------------------------------------------------+
| Command | Shortcut | Usage | Description |
+-----------------+----------+------------------------------------------+-------------------------------------------------------------+
| /bug | <null> | /bug | File a bug on GitHub. |
| /clip | <null> | /clip | <query>\clip | Copy query to the system clipboard. |
| /config | <null> | /config <help|get|search|edit> [key] | Inspect settings from config files. |
| /connect | /r | /connect [database] | Reconnect to the server, optionally switching databases. |
| /delimiter | <null> | /delimiter <string> | Change end-of-statement delimiter. |
| /dsn | <null> | /dsn <help|list|show|save|edit|delete> | Manage saved DSNs. See /dsn help. |
| /dt | <null> | /dt[+] [table] | List or describe tables. |
| /edit | /e | /edit <file> | <query>\edit | Edit query with editor (uses $VISUAL or $EDITOR). |
| /exit | /q | /exit | Exit. |
| /f | <null> | /f [name [args..] [--key=value]] | List or execute favorite queries. |
| /favorite | <null> | /favorite <command> | Alternative favorite query interface. See /favorite help. |
| /fd | <null> | /fd <name> | Delete a favorite query. |
| /fs | <null> | /fs <name> <query> | Save a favorite query. |
| \g | <null> | <query>\g | Display query results (mnemonic: go). |
| \G | <null> | <query>\G | Display query results vertically. |
| /help | /? | /help [term] | Show this table, or search for help on a term. |
| /l | <null> | /l | List databases. |
| /llm | /ai | /llm [arguments] | Interrogate an LLM. See "/llm help". |
| /nopager | /n | /nopager | Disable pager; print to stdout. |
| /notee | <null> | /notee | Stop writing results to an output file. |
| /nowarnings | /w | /nowarnings | Disable automatic warnings display. |
| /once | /o | /once [-o] <file> | Append next result to an output file (overwrite using -o). |
| /pager | /P | /pager [command] | Set pager to [command]. Print query results via pager. |
| /ping | <null> | /ping | Check the connection. |
| /pipe_once | /| | /pipe_once <command> | Send next result to a subprocess. |
| /prompt | /R | /prompt [string] | Show or change prompt format. |
| /quit | /q | /quit | Quit. |
| /redirectformat | /Tr | /redirectformat <format> | Change the table format used to output redirected results. |
| /rehash | /# | /rehash | Refresh auto-completions. |
| /source | /. | /source [--special|--show|--page] <file> | Execute queries from a file. |
| /status | /s | /status | Get status information from the server. |
| /system | <null> | /system [-r] <command> | Execute a system shell command (raw mode with -r). |
| /tableformat | /T | /tableformat <format> | Change the table format used to output interactive results. |
| /tee | <null> | /tee [-o] <file> | Append all results to an output file (overwrite using -o). |
| /timing | /t | /timing | Toggle timing of queries. |
| /use | /u | /use <database> | Change to a new database. |
| /warnings | /W | /warnings | Enable automatic warnings display. |
| /watch | <null> | /watch [seconds] [-c] <query> | Execute query every [seconds] seconds (5 by default). |
| \x | <null> | <query>\x | Display query results in an explorer rather than a pager. |
+-----------------+----------+------------------------------------------+-------------------------------------------------------------+
+-----------------+----------+----------------------------------------+-------------------------------------------------------------+
| Command | Shortcut | Usage | Description |
+-----------------+----------+----------------------------------------+-------------------------------------------------------------+
| /bug | <null> | /bug | File a bug on GitHub. |
| /clip | <null> | /clip | <query>\clip | Copy query to the system clipboard. |
| /config | <null> | /config <help|get|search|edit> [key] | Inspect settings from config files. |
| /connect | /r | /connect [database] | Reconnect to the server, optionally switching databases. |
| /delimiter | <null> | /delimiter <string> | Change end-of-statement delimiter. |
| /dsn | <null> | /dsn <help|list|show|save|edit|delete> | Manage saved DSNs. See /dsn help. |
| /dt | <null> | /dt[+] [table] | List or describe tables. |
| /edit | /e | /edit <file> | <query>\edit | Edit query with editor (uses $VISUAL or $EDITOR). |
| /exit | /q | /exit | Exit. |
| /f | <null> | /f [name [args..] [--key=value]] | List or execute favorite queries. |
| /favorite | <null> | /favorite <command> | Alternative favorite query interface. See /favorite help. |
| /fd | <null> | /fd <name> | Delete a favorite query. |
| /fs | <null> | /fs <name> <query> | Save a favorite query. |
| \g | <null> | <query>\g | Display query results (mnemonic: go). |
| \G | <null> | <query>\G | Display query results vertically. |
| /help | /? | /help [term] | Show this table, or search for help on a term. |
| /l | <null> | /l | List databases. |
| /llm | /ai | /llm [arguments] | Interrogate an LLM. See "/llm help". |
| /nopager | /n | /nopager | Disable pager; print to stdout. |
| /notee | <null> | /notee | Stop writing results to an output file. |
| /nowarnings | /w | /nowarnings | Disable automatic warnings display. |
| /once | /o | /once [-o] <file> | Append next result to an output file (overwrite using -o). |
| /pager | /P | /pager [command] | Set pager to [command]. Print query results via pager. |
| /ping | <null> | /ping | Check the connection. |
| /pipe_once | /| | /pipe_once <command> | Send next result to a subprocess. |
| /prompt | /R | /prompt [string] | Show or change prompt format. |
| /quit | /q | /quit | Quit. |
| /redirectformat | /Tr | /redirectformat <format> | Change the table format used to output redirected results. |
| /rehash | /# | /rehash | Refresh auto-completions. |
| /source | /. | /source [options] <file> | Execute queries from a file. |
| /status | /s | /status | Get status information from the server. |
| /system | <null> | /system [-r] <command> | Execute a system shell command (raw mode with -r). |
| /tableformat | /T | /tableformat <format> | Change the table format used to output interactive results. |
| /tee | <null> | /tee [-o] <file> | Append all results to an output file (overwrite using -o). |
| /timing | /t | /timing | Toggle timing of queries. |
| /use | /u | /use <database> | Change to a new database. |
| /warnings | /W | /warnings | Enable automatic warnings display. |
| /watch | <null> | /watch [seconds] [-c] <query> | Execute query every [seconds] seconds (5 by default). |
| \x | <null> | <query>\x | Display query results in an explorer rather than a pager. |
+-----------------+----------+----------------------------------------+-------------------------------------------------------------+
Loading
Loading