Skip to content

Commit 6abda18

Browse files
author
Elliot Boschwitz
authored
Multi-line statements now execute on 'GO' (#396)
Multi-line query functionality has changed to execute whenever 'GO' is typed, followed by a new-line. Semi-colons are no longer supported.
1 parent 341fead commit 6abda18

File tree

7 files changed

+71
-9
lines changed

7 files changed

+71
-9
lines changed

build.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ def get_active_test_filepaths():
173173
'tests/test_config.py '
174174
'tests/test_naive_completion.py '
175175
'tests/test_main.py '
176+
'tests/test_multiline.py '
176177
'tests/test_fuzzy_completion.py '
177178
'tests/test_rowlimit.py '
178179
'tests/test_sqlcompletion.py '

mssqlcli/mssqlbuffer.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import unicode_literals
2-
2+
import re
3+
import sqlparse
34
from prompt_toolkit.enums import DEFAULT_BUFFER
45
from prompt_toolkit.filters import Condition
56
from prompt_toolkit.application import get_app
@@ -21,10 +22,31 @@ def cond():
2122

2223

2324
def _is_complete(sql):
24-
# A complete command is an sql statement that ends with a semicolon, unless
25+
# A complete command is an sql statement that ends with a 'GO', unless
2526
# there's an open quote surrounding it, as is common when writing a
2627
# CREATE FUNCTION command
27-
return sql.endswith(';') and not is_open_quote(sql)
28+
if sql is not None and sql != "":
29+
# remove comments
30+
sql = sqlparse.format(sql, strip_comments=True)
31+
32+
# check for open comments
33+
# remove all closed quotes to isolate instances of open comments
34+
sql_no_quotes = re.sub(r'".*?"|\'.*?\'', '', sql)
35+
is_open_comment = len(re.findall(r'\/\*', sql_no_quotes)) > 0
36+
37+
# check that 'go' is only token on newline
38+
lines = sql.split('\n')
39+
lastline = lines[len(lines) - 1].lower().strip()
40+
is_valid_go_on_lastline = lastline == 'go'
41+
42+
# check that 'go' is on last line, not in open quotes, and there's no open
43+
# comment with closed comments and quotes removed.
44+
# NOTE: this method fails when GO follows a closing '*/' block comment on the same line,
45+
# we've taken a dependency with sqlparse
46+
# (https://github.com/andialbrecht/sqlparse/issues/484)
47+
return not is_open_quote(sql) and not is_open_comment and is_valid_go_on_lastline
48+
49+
return False
2850

2951

3052
def _multiline_exception(text):

mssqlcli/mssqlcliclient.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,8 @@ def _execute_query_execute_request_for(self, query):
230230
query_has_exception = query_response.exception_message
231231
query_has_error_messages = query_messages[0].is_error if query_messages else False
232232
query_has_batch_error = query_response.batch_summaries[0].has_error \
233-
if hasattr(query_response, 'batch_summaries') else False
233+
if hasattr(query_response, 'batch_summaries') \
234+
and len(query_response.batch_summaries) > 0 else False
234235

235236
query_failed = query_has_exception or query_has_batch_error or query_has_error_messages
236237

@@ -277,7 +278,8 @@ def _exception_found_in(query_response):
277278

278279
@staticmethod
279280
def _no_results_found_in(query_response):
280-
return not query_response.batch_summaries[0].result_set_summaries
281+
return not query_response.batch_summaries \
282+
or not query_response.batch_summaries[0].result_set_summaries
281283

282284
@staticmethod
283285
def _no_rows_found_in(query_response):

mssqlcli/mssqlclirc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ smart_completion = True
1010
wider_completion_menu = False
1111

1212
# Multi-line mode allows breaking up the sql statements into multiple lines. If
13-
# this is set to True, then the end of the statements must have a semi-colon.
13+
# this is set to True, then the end of the statements must have 'GO'.
1414
# If this is set to False then sql statements can't be split into multiple
1515
# lines. End of line (return) is considered as the end of the statement.
1616
multi_line = False
1717

1818
# If multi_line_mode is set to "tsql", in multi-line mode, [Enter] will execute
19-
# the current input if the input ends in a semicolon.
19+
# the current input if the input ends in 'GO'.
2020
# If multi_line_mode is set to "safe", in multi-line mode, [Enter] will always
2121
# insert a newline, and [Esc] [Enter] or [Alt]-[Enter] must be used to execute
2222
# a command.

mssqlcli/mssqltoolbar.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def get_toolbar_tokens():
3939
if mssql_cli.multiline_mode == 'safe':
4040
result.append((token, ' ([Esc] [Enter] to execute]) '))
4141
else:
42-
result.append((token, ' (Semi-colon [;] will end the line) '))
42+
result.append((token, ' ([GO] statement will end the line) '))
4343

4444
if mssql_cli.vi_mode:
4545
result.append(

mssqlcli/packages/parseutils/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def is_open_quote(sql):
113113

114114
def _parsed_is_open_quote(parsed):
115115
# Look for unmatched single quotes, or unmatched dollar sign quotes
116-
return any(tok.match(Token.Error, ("'", "$")) for tok in parsed.flatten())
116+
return any(tok.match(Token.Error, ("'", '"', "$")) for tok in parsed.flatten())
117117

118118

119119
def parse_partial_identifier(word):

tests/test_multiline.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import pytest
2+
from mssqlcli.mssqlbuffer import _is_complete
3+
4+
5+
class TestMssqlCliMultiline:
6+
testdata = [
7+
(None, False),
8+
('', False),
9+
('select 1 /* open comment!\ngo', False),
10+
('select 1\ngo -- another comment', True),
11+
('select 1; select 2, "open quote: go', False),
12+
('select 1\n"go"', False),
13+
('select 1; GO', False),
14+
('SELECT 4;\nGO', True),
15+
('select 1\n select 2;\ngo', True),
16+
('select 1;', False),
17+
('select 1 go', False),
18+
('select 1\ngo go go', False),
19+
('GO select 1', False),
20+
('GO', True)
21+
# tests below to be enabled when sqlparse supports retaining newlines
22+
# when stripping comments (tracking here:
23+
# https://github.com/andialbrecht/sqlparse/issues/484):
24+
# ('select 3 /* another open comment\n*/ GO', True),
25+
# ('select 1\n*/go', False),
26+
# ('select 1 /*\nmultiple lines!\n*/go', True)
27+
]
28+
29+
@staticmethod
30+
@pytest.mark.parametrize("query_str, is_complete", testdata)
31+
def test_multiline_completeness(query_str, is_complete):
32+
"""
33+
Tests the _is_complete helper method, which parses a T-SQL multiline
34+
statement on each newline and determines whether the script should
35+
execute.
36+
"""
37+
assert _is_complete(query_str) == is_complete

0 commit comments

Comments
 (0)