Skip to content

Commit

Permalink
Merge pull request #782 from dbcli/delimiter-2
Browse files Browse the repository at this point in the history
implemented DELIMITER command
  • Loading branch information
amjith authored Oct 13, 2019
2 parents 0d01061 + 1f99087 commit ff42487
Show file tree
Hide file tree
Showing 13 changed files with 247 additions and 37 deletions.
3 changes: 3 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Features:
---------
* Added DSN alias name as a format specifier to the prompt (Thanks: [Georgy Frolov]).

* Added DELIMITER command (Thanks: [Georgy Frolov])


1.20.1
======
Expand Down Expand Up @@ -723,3 +725,4 @@ Bug Fixes:
[Dick Marinus]: https://github.com/meeuw
[François Pietka]: https://github.com/fpietka
[Frederic Aoustin]: https://github.com/fraoustin
[Georgy Frolov]: https://github.com/pasenor
36 changes: 27 additions & 9 deletions mycli/clibuffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from prompt_toolkit.filters import Condition
from prompt_toolkit.application import get_app
from .packages.parseutils import is_open_quote
from .packages import special


def cli_is_multiline(mycli):
Expand All @@ -17,6 +18,7 @@ def cond():
return not _multiline_exception(doc.text)
return cond


def _multiline_exception(text):
orig = text
text = text.strip()
Expand All @@ -27,12 +29,28 @@ def _multiline_exception(text):
if text.startswith('\\fs'):
return orig.endswith('\n')

return (text.startswith('\\') or # Special Command
text.endswith(';') or # Ended with a semi-colon
text.endswith('\\g') or # Ended with \g
text.endswith('\\G') or # Ended with \G
(text == 'exit') or # Exit doesn't need semi-colon
(text == 'quit') or # Quit doesn't need semi-colon
(text == ':q') or # To all the vim fans out there
(text == '') # Just a plain enter without any text
)
return (
# Special Command
text.startswith('\\') or

# Delimiter declaration
text.lower().startswith('delimiter') or

# Ended with the current delimiter (usually a semi-column)
text.endswith(special.get_current_delimiter()) or

text.endswith('\\g') or
text.endswith('\\G') or

# Exit doesn't need semi-column`
(text == 'exit') or

# Quit doesn't need semi-column
(text == 'quit') or

# To all teh vim fans out there
(text == ':q') or

# just a plain enter without any text
(text == '')
)
8 changes: 7 additions & 1 deletion mycli/clitoolbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from prompt_toolkit.key_binding.vi_state import InputMode
from prompt_toolkit.application import get_app
from prompt_toolkit.enums import EditingMode
from .packages import special


def create_toolbar_tokens_func(mycli, show_fish_help):
Expand All @@ -12,8 +13,13 @@ def get_toolbar_tokens():
result.append(('class:bottom-toolbar', ' '))

if mycli.multi_line:
delimiter = special.get_current_delimiter()
result.append(
('class:bottom-toolbar', ' (Semi-colon [;] will end the line) '))
(
'class:bottom-toolbar',
' ({} [{}] will end the line) '.format(
'Semi-colon' if delimiter == ';' else 'Delimiter', delimiter)
))

if mycli.multi_line:
result.append(('class:bottom-toolbar.on', '[F3] Multiline: ON '))
Expand Down
83 changes: 83 additions & 0 deletions mycli/packages/special/delimitercommand.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# coding: utf-8
from __future__ import unicode_literals

import re
import sqlparse


class DelimiterCommand(object):
def __init__(self):
self._delimiter = ';'

def _split(self, sql):
"""Temporary workaround until sqlparse.split() learns about custom
delimiters."""

placeholder = "\ufffc" # unicode object replacement character

if self._delimiter == ';':
return sqlparse.split(sql)

# We must find a string that original sql does not contain.
# Most likely, our placeholder is enough, but if not, keep looking
while placeholder in sql:
placeholder += placeholder[0]
sql = sql.replace(';', placeholder)
sql = sql.replace(self._delimiter, ';')

split = sqlparse.split(sql)

return [
stmt.replace(';', self._delimiter).replace(placeholder, ';')
for stmt in split
]

def queries_iter(self, input):
"""Iterate over queries in the input string."""

queries = self._split(input)
while queries:
for sql in queries:
delimiter = self._delimiter
sql = queries.pop(0)
if sql.endswith(delimiter):
trailing_delimiter = True
sql = sql.strip(delimiter)
else:
trailing_delimiter = False

yield sql

# if the delimiter was changed by the last command,
# re-split everything, and if we previously stripped
# the delimiter, append it to the end
if self._delimiter != delimiter:
combined_statement = ' '.join([sql] + queries)
if trailing_delimiter:
combined_statement += delimiter
queries = self._split(combined_statement)[1:]

def set(self, arg, **_):
"""Change delimiter.
Since `arg` is everything that follows the DELIMITER token
after sqlparse (it may include other statements separated by
the new delimiter), we want to set the delimiter to the first
word of it.
"""
match = arg and re.search(r'[^\s]+', arg)
if not match:
message = 'Missing required argument, delimiter'
return [(None, None, None, message)]

delimiter = match.group()
if delimiter.lower() == 'delimiter':
return [(None, None, None, 'Invalid delimiter "delimiter"')]

self._delimiter = delimiter
return [(None, None, None, "Changed delimiter to {}".format(delimiter))]

@property
def current(self):
return self._delimiter
2 changes: 1 addition & 1 deletion mycli/packages/special/favoritequeries.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class FavoriteQueries(object):
╒════════╤════════╕
│ a │ b │
╞════════╪════════╡
│ 日本語 │ 日本語 │
│ 日本語 │ 日本語
╘════════╧════════╛
# Delete a favorite query.
Expand Down
20 changes: 20 additions & 0 deletions mycli/packages/special/iocommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from . import export
from .main import special_command, NO_QUERY, PARSED_QUERY
from .favoritequeries import FavoriteQueries
from .delimitercommand import DelimiterCommand
from .utils import handle_cd_command
from mycli.packages.prompt_utils import confirm_destructive_query

Expand All @@ -24,6 +25,8 @@
tee_file = None
once_file = written_to_once_file = None
favoritequeries = FavoriteQueries(ConfigObj())
delimiter_command = DelimiterCommand()


@export
def set_timing_enabled(val):
Expand Down Expand Up @@ -437,3 +440,20 @@ def watch_query(arg, **kwargs):
return
finally:
set_pager_enabled(old_pager_enabled)


@export
@special_command('delimiter', None, 'Change SQL delimiter.')
def set_delimiter(arg, **_):
return delimiter_command.set(arg)


@export
def get_current_delimiter():
return delimiter_command.current


@export
def split_queries(input):
for query in delimiter_command.queries_iter(input):
yield query
2 changes: 1 addition & 1 deletion mycli/sqlcompleter.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class SQLCompleter(Completer):
'CHARACTER SET', 'CHECK', 'COLLATE', 'COLUMN', 'COMMENT',
'COMMIT', 'CONSTRAINT', 'CREATE', 'CURRENT',
'CURRENT_TIMESTAMP', 'DATABASE', 'DATE', 'DECIMAL', 'DEFAULT',
'DELETE FROM', 'DELIMITER', 'DESC', 'DESCRIBE', 'DROP',
'DELETE FROM', 'DESC', 'DESCRIBE', 'DROP',
'ELSE', 'END', 'ENGINE', 'ESCAPE', 'EXISTS', 'FILE', 'FLOAT',
'FOR', 'FOREIGN KEY', 'FORMAT', 'FROM', 'FULL', 'FUNCTION',
'GRANT', 'GROUP BY', 'HAVING', 'HOST', 'IDENTIFIED', 'IN',
Expand Down
8 changes: 4 additions & 4 deletions mycli/sqlexecute.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import unicode_literals

import logging
import pymysql
import sqlparse
Expand Down Expand Up @@ -166,12 +168,9 @@ def run(self, statement):
if statement.startswith('\\fs'):
components = [statement]
else:
components = sqlparse.split(statement)
components = special.split_queries(statement)

for sql in components:
# Remove spaces, eol and semi-colons.
sql = sql.rstrip(';')

# \G is treated specially since we have to set the expanded output.
if sql.endswith('\\G'):
special.set_expanded_output(True)
Expand All @@ -194,6 +193,7 @@ def run(self, statement):
if not cur.nextset() or (not cur.rowcount and cur.description is None):
break


def get_result(self, cursor):
"""Get the current result's data from the cursor."""
title = headers = None
Expand Down
21 changes: 18 additions & 3 deletions test/features/iocommands.feature
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,31 @@ Feature: I/O commands

Scenario: edit sql in file with external editor
When we start external editor providing a file name
and we type sql in the editor
and we type "select * from abc" in the editor
and we exit the editor
then we see dbcli prompt
and we see the sql in prompt
and we see "select * from abc" in prompt

Scenario: tee output from query
When we tee output
and we wait for prompt
and we query "select 123456"
and we select "select 123456"
and we wait for prompt
and we notee output
and we wait for prompt
then we see 123456 in tee output

Scenario: set delimiter
When we query "delimiter $"
then delimiter is set to "$"

Scenario: set delimiter twice
When we query "delimiter $"
and we query "delimiter ]]"
then delimiter is set to "]]"

Scenario: set delimiter and query on same line
When we query "select 123; delimiter $ select 456 $ delimiter %"
then we see result "123"
and we see result "456"
and delimiter is set to "%"
2 changes: 1 addition & 1 deletion test/features/steps/crud_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def step_db_connect_test(context):
"""Send connect to database."""
db_name = context.conf['dbname']
context.currentdb = db_name
context.cli.sendline('use {0}'.format(db_name))
context.cli.sendline('use {0};'.format(db_name))


@when('we connect to tmp database')
Expand Down
56 changes: 40 additions & 16 deletions test/features/steps/iocommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ def step_edit_file(context):
wrappers.expect_exact(context, '\r\n:', timeout=2)


@when('we type sql in the editor')
def step_edit_type_sql(context):
@when('we type "{query}" in the editor')
def step_edit_type_sql(context, query):
context.cli.sendline('i')
context.cli.sendline('select * from abc')
context.cli.sendline(query)
context.cli.sendline('.')
wrappers.expect_exact(context, '\r\n:', timeout=2)

Expand All @@ -35,9 +35,9 @@ def step_edit_quit(context):
wrappers.expect_exact(context, "written", timeout=2)


@then('we see the sql in prompt')
def step_edit_done_sql(context):
for match in 'select * from abc'.split(' '):
@then('we see "{query}" in prompt')
def step_edit_done_sql(context, query):
for match in query.split(' '):
wrappers.expect_exact(context, match, timeout=5)
# Cleanup the command line.
context.cli.sendcontrol('c')
Expand All @@ -56,20 +56,35 @@ def step_tee_ouptut(context):
os.path.basename(context.tee_file_name)))


@when(u'we query "select 123456"')
def step_query_select_123456(context):
context.cli.sendline('select 123456')
wrappers.expect_pager(context, dedent("""\
+--------+\r
| 123456 |\r
+--------+\r
| 123456 |\r
+--------+\r
@when(u'we select "select {param}"')
def step_query_select_number(context, param):
context.cli.sendline(u'select {}'.format(param))
wrappers.expect_pager(context, dedent(u"""\
+{dashes}+\r
| {param} |\r
+{dashes}+\r
| {param} |\r
+{dashes}+\r
\r
"""), timeout=5)
""".format(param=param, dashes='-' * (len(param) + 2))
), timeout=5)
wrappers.expect_exact(context, '1 row in set', timeout=2)


@then(u'we see result "{result}"')
def step_see_result(context, result):
wrappers.expect_exact(
context,
u"| {} |".format(result),
timeout=2
)


@when(u'we query "{query}"')
def step_query(context, query):
context.cli.sendline(query)


@when(u'we notee output')
def step_notee_output(context):
context.cli.sendline('notee')
Expand All @@ -81,3 +96,12 @@ def step_see_123456_in_ouput(context):
assert '123456' in f.read()
if os.path.exists(context.tee_file_name):
os.remove(context.tee_file_name)


@then(u'delimiter is set to "{delimiter}"')
def delimiter_is_set(context, delimiter):
wrappers.expect_exact(
context,
u'Changed delimiter to {}'.format(delimiter),
timeout=2
)
2 changes: 1 addition & 1 deletion test/test_parseutils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest
from mycli.packages.parseutils import (
extract_tables, query_starts_with, queries_start_with, is_destructive
extract_tables, query_starts_with, queries_start_with, is_destructive,
)


Expand Down
Loading

0 comments on commit ff42487

Please sign in to comment.