├── tests ├── features │ ├── __init__.py │ ├── steps │ │ ├── __init__.py │ │ ├── specials.py │ │ ├── basic_commands.py │ │ ├── wrappers.py │ │ ├── named_queries.py │ │ ├── expanded.py │ │ ├── auto_vertical.py │ │ ├── crud_database.py │ │ ├── iocommands.py │ │ └── crud_table.py │ ├── specials.feature │ ├── wrappager.py │ ├── named_queries.feature │ ├── basic_commands.feature │ ├── auto_vertical.feature │ ├── iocommands.feature │ ├── crud_database.feature │ ├── crud_table.feature │ ├── expanded.feature │ ├── fixture_utils.py │ ├── fixture_data │ │ ├── help_commands.txt │ │ └── help.txt │ ├── db_utils.py │ └── environment.py ├── test_query_inputs │ ├── input_small.txt │ ├── input_multiple.txt │ ├── input_col_wide.txt │ ├── input_big.txt │ ├── input_col_too_wide.txt │ └── input_multiple_merge.txt ├── jsonrpc │ ├── __init__.py │ ├── baselines │ │ └── test_malformed_query.txt │ └── generatequerybaseline.py ├── test_query_baseline │ ├── baseline_small.txt │ ├── baseline_col_too_wide.txt │ ├── baseline_multiple_merge.txt │ ├── baseline_multiple.txt │ └── baseline_col_wide.txt ├── test_localization.py ├── parseutils │ ├── test_function_metadata.py │ └── test_ctes.py ├── test_prioritization.py ├── test_config.py ├── test_multiline.py ├── test_plan.wiki ├── test_rowlimit.py ├── test_completion_refresher.py ├── test_fuzzy_completion.py ├── test_interactive_mode.py ├── test_naive_completion.py └── test_telemetry.py ├── mssqlcli ├── packages │ ├── __init__.py │ ├── mssqlliterals │ │ ├── __init__.py │ │ └── main.py │ ├── parseutils │ │ ├── __init__.py │ │ ├── utils.py │ │ ├── ctes.py │ │ └── meta.py │ ├── special │ │ ├── __init__.py │ │ ├── namedqueries.py │ │ └── main.py │ └── prioritization.py ├── __init__.py ├── locale │ └── ko_KR │ │ └── LC_MESSAGES │ │ └── mssql-cli.mo ├── jsonrpc │ ├── __init__.py │ └── contracts │ │ ├── __init__.py │ │ ├── request.py │ │ └── connectionservice.py ├── filters.py ├── localized_strings.py ├── encodingutils.py ├── util.py ├── mssqltoolsservice │ ├── __init__.py │ └── externals.py ├── mssqltoolbar.py ├── config.py ├── mssqlbuffer.py ├── key_bindings.py ├── decorators.py ├── main.py ├── sqltoolsclient.py ├── completion_refresher.py ├── mssqlstyle.py └── mssqlqueries.py ├── employee_registry.txt ├── pylintrc ├── screenshots ├── pgcli.gif ├── image01.png ├── image02.png ├── mssql-cli-less-pager.png ├── mssql-cli-autocomplete.gif └── mssql-cli-display-issue.png ├── run_in_virtual_env.sh ├── .env ├── .gitattributes ├── MANIFEST.in ├── mssql-cli.bat ├── .vscode ├── settings.json └── launch.json ├── release_scripts └── Packages.Microsoft │ ├── config.json │ ├── Dockerfile │ └── publish.sh ├── .bumpversion.cfg ├── doc ├── installation │ ├── macos.md │ ├── windows.md │ ├── README.md │ ├── linux.md │ └── pip.md ├── virtual_environment_info.md ├── roadmap.md ├── telemetry_guide.md └── troubleshooting_guide.md ├── dos2unix.py ├── pytest.ini ├── mssql-cli ├── tox.ini ├── setup.cfg ├── dev_setup.py ├── requirements-dev.txt ├── new_hire.py ├── LICENSE.txt ├── .gitignore ├── setup.py ├── utility.py └── README.md /tests/features/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mssqlcli/packages/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/features/steps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mssqlcli/packages/mssqlliterals/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mssqlcli/packages/parseutils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mssqlcli/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.1.0' 2 | -------------------------------------------------------------------------------- /tests/test_query_inputs/input_small.txt: -------------------------------------------------------------------------------- 1 | SELECT 1 -------------------------------------------------------------------------------- /tests/test_query_inputs/input_multiple.txt: -------------------------------------------------------------------------------- 1 | SELECT 1 2 | SELECT 2 -------------------------------------------------------------------------------- /employee_registry.txt: -------------------------------------------------------------------------------- 1 | elbosc was here! 2020-04-30 2 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MESSAGES CONTROL] 2 | disable=missing-docstring,invalid-name,unnecessary-pass -------------------------------------------------------------------------------- /screenshots/pgcli.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbcli/mssql-cli/HEAD/screenshots/pgcli.gif -------------------------------------------------------------------------------- /tests/test_query_inputs/input_col_wide.txt: -------------------------------------------------------------------------------- 1 | SELECT REPLICATE(CAST('X,' AS VARCHAR(MAX)), 1024) -------------------------------------------------------------------------------- /screenshots/image01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbcli/mssql-cli/HEAD/screenshots/image01.png -------------------------------------------------------------------------------- /screenshots/image02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbcli/mssql-cli/HEAD/screenshots/image02.png -------------------------------------------------------------------------------- /run_in_virtual_env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | virtualenv env 3 | source env/bin/activate 4 | $@ 5 | deactivate 6 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | MSSQL_CLI_SERVER= 2 | MSSQL_CLI_DATABASE= 3 | MSSQL_CLI_USER= 4 | MSSQL_CLI_PASSWORD= 5 | PYTHONIOENCODING=UTF-8 6 | -------------------------------------------------------------------------------- /screenshots/mssql-cli-less-pager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbcli/mssql-cli/HEAD/screenshots/mssql-cli-less-pager.png -------------------------------------------------------------------------------- /screenshots/mssql-cli-autocomplete.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbcli/mssql-cli/HEAD/screenshots/mssql-cli-autocomplete.gif -------------------------------------------------------------------------------- /screenshots/mssql-cli-display-issue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbcli/mssql-cli/HEAD/screenshots/mssql-cli-display-issue.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # baseline files need to retain crlf for cross plat tests that simulate sql tools service response 2 | *.txt text eol=crlf 3 | -------------------------------------------------------------------------------- /mssqlcli/locale/ko_KR/LC_MESSAGES/mssql-cli.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbcli/mssql-cli/HEAD/mssqlcli/locale/ko_KR/LC_MESSAGES/mssql-cli.mo -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include mssqlcli/mssqltoolsservice/bin/* 3 | include mssqlcli/mssqltoolsservice/bin/*/* 4 | include mssqlcli/packages/mssqlliterals/sqlliterals.json 5 | -------------------------------------------------------------------------------- /tests/test_query_inputs/input_big.txt: -------------------------------------------------------------------------------- 1 | ALTER DATABASE keep_AdventureWorks2014 SET COMPATIBILITY_LEVEL = 130 2 | SELECT * FROM STRING_SPLIT(REPLICATE(CAST('X,' AS VARCHAR(MAX)), 1024), ',') -------------------------------------------------------------------------------- /tests/jsonrpc/__init__.py: -------------------------------------------------------------------------------- 1 | BASELINE_REQUEST = { 2 | u'jsonrpc': u'2.0', 3 | u'params': { 4 | u'Key': u'Value' 5 | }, 6 | u'method': u'testMethod/DoThis', 7 | u'id': 1 8 | } 9 | -------------------------------------------------------------------------------- /tests/test_query_baseline/baseline_small.txt: -------------------------------------------------------------------------------- 1 | +--------------------+ 2 | | (No column name) | 3 | |--------------------| 4 | | 1 | 5 | +--------------------+ 6 | (1 row affected) 7 | -------------------------------------------------------------------------------- /tests/features/specials.feature: -------------------------------------------------------------------------------- 1 | Feature: Special commands 2 | 3 | @wip 4 | Scenario: run refresh command 5 | When we refresh completions 6 | and we wait for prompt 7 | then we see completions refresh started 8 | -------------------------------------------------------------------------------- /mssql-cli.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal 3 | REM Set the python io encoding to UTF-8 by default if not set. 4 | IF "%PYTHONIOENCODING%"=="" ( 5 | SET PYTHONIOENCODING="UTF-8" 6 | ) 7 | SET PYTHONPATH=%~dp0;%PYTHONPATH% 8 | python -m mssqlcli.main %* 9 | 10 | endlocal -------------------------------------------------------------------------------- /tests/test_query_baseline/baseline_col_too_wide.txt: -------------------------------------------------------------------------------- 1 | Msg 103, Level 15, State 4, Line 1 2 | The identifier that starts with 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' is too long. Maximum length is 128. 3 | -------------------------------------------------------------------------------- /tests/test_query_inputs/input_col_too_wide.txt: -------------------------------------------------------------------------------- 1 | SELECT xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.unittestEnabled": false, 3 | "python.testing.nosetestsEnabled": false, 4 | "python.testing.pytestEnabled": true, 5 | "python.pythonPath": "env/bin/python", 6 | "python.testing.cwd": "${workspaceDir}", 7 | "python.terminal.activateEnvironment": true, 8 | } -------------------------------------------------------------------------------- /tests/test_query_baseline/baseline_multiple_merge.txt: -------------------------------------------------------------------------------- 1 | Commands completed successfully. 2 | Commands completed successfully. 3 | (4 rows affected) 4 | Commands completed successfully. 5 | Commands completed successfully. 6 | (5 rows affected) 7 | (6 rows affected) 8 | (5 rows affected) 9 | Commands completed successfully. 10 | -------------------------------------------------------------------------------- /release_scripts/Packages.Microsoft/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": "package-repo-service.corp.microsoft.com", 3 | "port": "443", 4 | "AADClientId": "", 5 | "AADClientCertificate": "/root/private.pem", 6 | "AADResource": "", 7 | "AADTenant": "", 8 | "AADAuthorityUrl": "https://login.microsoftonline.com", 9 | "repositoryId": "" 10 | } 11 | -------------------------------------------------------------------------------- /mssqlcli/packages/special/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [] 2 | 3 | 4 | def export(defn): 5 | """Decorator to explicitly mark functions that are exposed in a lib.""" 6 | globals()[defn.__name__] = defn 7 | __all__.append(defn.__name__) 8 | return defn 9 | 10 | from mssqlcli.packages.special import main 11 | from mssqlcli.packages.special import commands 12 | -------------------------------------------------------------------------------- /tests/features/wrappager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | 4 | 5 | def wrappager(boundary): 6 | print(boundary) 7 | while 1: 8 | buf = sys.stdin.read(2048) 9 | if not buf: 10 | break 11 | sys.stdout.write(buf) 12 | print(boundary) 13 | 14 | 15 | if __name__ == "__main__": 16 | wrappager(sys.argv[1]) 17 | -------------------------------------------------------------------------------- /tests/test_query_baseline/baseline_multiple.txt: -------------------------------------------------------------------------------- 1 | +--------------------+ 2 | | (No column name) | 3 | |--------------------| 4 | | 1 | 5 | +--------------------+ 6 | (1 row affected) 7 | +--------------------+ 8 | | (No column name) | 9 | |--------------------| 10 | | 2 | 11 | +--------------------+ 12 | (1 row affected) 13 | -------------------------------------------------------------------------------- /mssqlcli/jsonrpc/__init__.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | # -------------------------------------------------------------------------------------------- 5 | -------------------------------------------------------------------------------- /tests/features/named_queries.feature: -------------------------------------------------------------------------------- 1 | Feature: named queries: 2 | save, use and delete named queries 3 | 4 | Scenario: save, use and delete named queries 5 | When we connect to test database 6 | then we see database connected 7 | when we save a named query 8 | then we see the named query saved 9 | when we delete a named query 10 | then we see the named query deleted 11 | -------------------------------------------------------------------------------- /mssqlcli/filters.py: -------------------------------------------------------------------------------- 1 | from prompt_toolkit.filters import Condition 2 | from prompt_toolkit.application import get_app 3 | 4 | 5 | @Condition 6 | def has_selected_completion(): 7 | """Enable when the current buffer has a selected completion.""" 8 | complete_state = get_app().current_buffer.complete_state 9 | return (complete_state is not None and 10 | complete_state.current_completion is not None) 11 | -------------------------------------------------------------------------------- /tests/features/basic_commands.feature: -------------------------------------------------------------------------------- 1 | Feature: run the cli, 2 | call the help command, 3 | exit the cli 4 | 5 | Scenario: run "\?" command 6 | When we send "\?" command 7 | then we see help output 8 | 9 | Scenario: run source command 10 | When we send source command 11 | then we see help output 12 | 13 | Scenario: run the cli and exit 14 | When we send "ctrl + d" 15 | then dbcli exits 16 | -------------------------------------------------------------------------------- /mssqlcli/jsonrpc/contracts/__init__.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | # -------------------------------------------------------------------------------------------- 5 | 6 | from mssqlcli.jsonrpc.contracts.request import Request 7 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 1.1.0 3 | parse = (?P\d+)\.(?P\d+)\.(?P\d+) 4 | serialize = 5 | {major}.{minor}.{patch} 6 | 7 | [bumpversion:file:mssqlcli/__init__.py] 8 | 9 | [bumpversion:file:build_scripts/debian/build.sh] 10 | 11 | [bumpversion:file:build_scripts/rpm/mssql-cli.spec] 12 | 13 | [bumpversion:file:release_scripts/Packages.Microsoft/publish.sh] 14 | 15 | [bumpversion:file:doc/installation/README.md] 16 | -------------------------------------------------------------------------------- /tests/test_localization.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | import mssqlcli.localized_strings as localized 4 | from mssqlcli.util import decode 5 | 6 | 7 | class LocalizationTests(unittest.TestCase): 8 | 9 | @staticmethod 10 | def test_product(): 11 | original = localized.goodbye() 12 | localized.translation(languages=['ko']).install() 13 | translated = decode(localized.goodbye()) 14 | assert original != translated 15 | -------------------------------------------------------------------------------- /mssqlcli/packages/mssqlliterals/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | root = os.path.dirname(__file__) 5 | literal_file = os.path.join(root, 'sqlliterals.json') 6 | 7 | with open(literal_file) as f: 8 | literals = json.load(f) 9 | 10 | 11 | def get_literals(literal_type, type_=tuple): 12 | # Where `literal_type` is one of 'keywords', 'functions', 'datatypes', 13 | # returns a tuple of literal values of that type. 14 | 15 | return type_(literals[literal_type]) 16 | -------------------------------------------------------------------------------- /tests/features/auto_vertical.feature: -------------------------------------------------------------------------------- 1 | Feature: auto_vertical mode: 2 | on, off 3 | 4 | Scenario: auto_vertical on with small query 5 | When we run dbcli with --auto-vertical-output 6 | and we execute a small query 7 | then we see small results in horizontal format 8 | 9 | Scenario: auto_vertical on with large query 10 | When we run dbcli with --auto-vertical-output 11 | and we execute a large query 12 | then we see large results in vertical format 13 | -------------------------------------------------------------------------------- /doc/installation/macos.md: -------------------------------------------------------------------------------- 1 | # macOS Installation 2 | 3 | ## Supported OS Versions 4 | mssql-cli supports macOS (x64) 10.12 and up. 5 | 6 | ## Installing mssql-cli 7 | mssql-cli is installed using `pip`: 8 | ```sh 9 | # Install pip 10 | sudo easy_install pip 11 | 12 | # Update pip 13 | python -m pip install --upgrade pip 14 | 15 | # Install mssql-cli 16 | sudo pip install mssql-cli 17 | 18 | # Run mssql-cli 19 | mssql-cli 20 | ``` 21 | 22 | ## Uninstalling mssql-cli 23 | Use `pip` to remove mssql-cli: 24 | ```sh 25 | sudo pip uninstall mssql-cli 26 | ``` 27 | -------------------------------------------------------------------------------- /tests/features/iocommands.feature: -------------------------------------------------------------------------------- 1 | Feature: I/O commands 2 | 3 | Scenario: edit sql in file with external editor 4 | When we start external editor providing a file name 5 | and we type sql in the editor 6 | and we exit the editor 7 | then we see dbcli prompt 8 | and we see the sql in prompt 9 | 10 | Scenario: tee output from query 11 | When we tee output 12 | and we wait for prompt 13 | and we query "select 123456" 14 | and we wait for prompt 15 | and we notee output 16 | and we wait for prompt 17 | then we see 123456 in tee output 18 | -------------------------------------------------------------------------------- /tests/features/crud_database.feature: -------------------------------------------------------------------------------- 1 | Feature: manipulate databases: 2 | create, drop, connect, disconnect 3 | 4 | Scenario: create and drop temporary database 5 | When we create database 6 | then we see database created 7 | when we drop database 8 | then we see database dropped 9 | when we connect to dbserver 10 | then we see database connected 11 | 12 | Scenario: connect and disconnect from test database 13 | When we connect to test database 14 | then we see database connected 15 | when we connect to dbserver 16 | then we see database connected 17 | -------------------------------------------------------------------------------- /mssqlcli/localized_strings.py: -------------------------------------------------------------------------------- 1 | import gettext 2 | import os 3 | from importlib import import_module 4 | 5 | PATH = '{0}.py'.format(os.path.splitext(__file__)[0]) 6 | DOMAIN = 'mssql-cli' 7 | LOCALE_DIR = os.path.join(os.path.dirname(__file__), 'locale') 8 | LANGUAGES = None 9 | 10 | def translation(domain=DOMAIN, localedir=LOCALE_DIR, languages=None): 11 | languages = languages if (not languages is None) else LANGUAGES 12 | return gettext.translation(domain, localedir, languages, fallback=True) 13 | 14 | translation().install() 15 | 16 | 17 | ## Localized Strings 18 | def goodbye(): 19 | return _(u'Goodbye!') 20 | -------------------------------------------------------------------------------- /dos2unix.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """\ 3 | convert dos linefeeds (crlf) to unix (lf) 4 | usage: dos2unix.py 5 | """ 6 | 7 | __version__ = "1" # version is needed for packaging 8 | 9 | import sys 10 | 11 | if len(sys.argv[1:]) != 2: 12 | sys.exit(__doc__) 13 | 14 | content = '' 15 | outsize = 0 16 | with open(sys.argv[1], 'rb') as infile: 17 | content = infile.read() 18 | with open(sys.argv[2], 'wb') as output: 19 | for line in content.splitlines(): 20 | outsize += len(line) + 1 21 | output.write(line + b'\n') 22 | 23 | print("Done. Stripped %s bytes." % (len(content) - outsize)) 24 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths= 3 | tests/test_mssqlcliclient.py 4 | tests/test_completion_refresher.py 5 | tests/test_config.py 6 | tests/test_naive_completion.py 7 | tests/test_main.py 8 | tests/test_multiline.py 9 | tests/test_fuzzy_completion.py 10 | tests/test_rowlimit.py 11 | tests/test_sqlcompletion.py 12 | tests/test_prioritization.py 13 | tests/jsonrpc/test_jsonrpc.py 14 | tests/jsonrpc/test_json_rpc_contracts.py 15 | tests/jsonrpc/test_jsonrpcclient.py 16 | tests/test_telemetry.py 17 | tests/test_localization.py 18 | tests/test_globalization.py 19 | tests/test_interactive_mode.py 20 | tests/test_noninteractive_mode.py 21 | tests/test_special.py -------------------------------------------------------------------------------- /tests/parseutils/test_function_metadata.py: -------------------------------------------------------------------------------- 1 | from mssqlcli.packages.parseutils.meta import FunctionMetadata 2 | 3 | 4 | def test_function_metadata_eq(): 5 | f1 = FunctionMetadata( 6 | 's', 'f', ['x'], ['integer'], [], 'int', False, False, False, None 7 | ) 8 | f2 = FunctionMetadata( 9 | 's', 'f', ['x'], ['integer'], [], 'int', False, False, False, None 10 | ) 11 | f3 = FunctionMetadata( 12 | 's', 'g', ['x'], ['integer'], [], 'int', False, False, False, None 13 | ) 14 | assert f1 == f2 15 | assert f1 != f3 16 | assert not (f1 != f2) 17 | assert not (f1 == f3) 18 | assert hash(f1) == hash(f2) 19 | assert hash(f1) != hash(f3) 20 | -------------------------------------------------------------------------------- /mssql-cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SOURCE="${BASH_SOURCE[0]}" 4 | while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink 5 | DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" 6 | SOURCE="$(readlink "$SOURCE")" 7 | [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located 8 | done 9 | DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" 10 | 11 | # Set the python io encoding to UTF-8 by default if not set. 12 | if [ -z ${PYTHONIOENCODING+x} ]; then export PYTHONIOENCODING=UTF-8; fi 13 | 14 | export PYTHONPATH="${DIR}:${PYTHONPATH}" 15 | 16 | python -m mssqlcli.main "$@" 17 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py35,py36,py37,py38,py39 3 | # We will build the sdist ourselves as we need to detect 4 | # what platform we are on and install the generated wheel 5 | # locally. 6 | skipsdist=True 7 | [testenv] 8 | passenv = * 9 | setenv = 10 | PYTHONPATH = {toxinidir} 11 | PYTHONIOENCODING=utf-8 12 | 13 | deps= 14 | -rrequirements-dev.txt 15 | 16 | install_commands = 17 | commands= 18 | 19 | # Build all packages 20 | python build.py build 21 | 22 | # Run dev_setup.py to set local mssqltoolsservice 23 | python dev_setup.py 24 | 25 | # Run unit tests 26 | python build.py unit_test 27 | 28 | # verify packaging via local install 29 | python build.py validate_package 30 | -------------------------------------------------------------------------------- /mssqlcli/encodingutils.py: -------------------------------------------------------------------------------- 1 | try: 2 | text_type = unicode 3 | except NameError: 4 | text_type = str 5 | 6 | 7 | def unicode2utf8(arg): 8 | """ 9 | Only in Python 2. 10 | In Python 3 the args are expected as unicode. 11 | """ 12 | 13 | try: 14 | if isinstance(arg, unicode): 15 | return arg.encode('utf-8') 16 | except NameError: 17 | pass # Python 3 18 | return arg 19 | 20 | 21 | def utf8tounicode(arg): 22 | """ 23 | Only in Python 2. 24 | In Python 3 the errors are returned as unicode. 25 | """ 26 | 27 | try: 28 | if isinstance(arg, unicode): 29 | return arg.decode('utf-8') 30 | except NameError: 31 | pass # Python 3 32 | return arg 33 | -------------------------------------------------------------------------------- /tests/features/crud_table.feature: -------------------------------------------------------------------------------- 1 | Feature: manipulate tables: 2 | create, insert, update, select, delete from, drop 3 | 4 | Scenario: create, insert, select from, update, drop table 5 | When we connect to test database 6 | then we see database connected 7 | when we create table 8 | then we see table created 9 | when we insert into table 10 | then we see record inserted 11 | when we update table 12 | then we see record updated 13 | when we select from table 14 | then we see data selected 15 | when we delete from table 16 | then we see record deleted 17 | when we drop table 18 | then we see table dropped 19 | when we connect to dbserver 20 | then we see database connected 21 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal =1 3 | 4 | [metadata] 5 | description-file = README.rst 6 | license_file = LICENSE.txt 7 | 8 | [flake8] 9 | ignore = 10 | # E501: line too long. 11 | E501, 12 | # F401, imported but unused, ignore where we import setup. 13 | F401, 14 | # E402 module level import not at top of file. 15 | # To maintain py2 - 3 compat certain orders of import is necessary. 16 | E402, 17 | # E741 ambiguous variable names catches single letter variables which are ok to use 18 | # in long python expressions 19 | E741, 20 | # F811 redefinition of unused seems to be misleading in our case as it is a third party library decorator 21 | F811, 22 | # F821 undefined name when referencing 'basestring' which is in the code base for when running on Python 2 23 | F821 -------------------------------------------------------------------------------- /dev_setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | 4 | import os 5 | import sys 6 | import utility 7 | 8 | PYTHON = os.getenv('CUSTOM_PYTHON', sys.executable) 9 | 10 | print('Running dev setup...') 11 | print('Root directory \'%s\'\n' % utility.ROOT_DIR) 12 | 13 | # install general requirements. 14 | utility.exec_command('%s -m pip install --no-cache-dir -r requirements-dev.txt' % PYTHON, 15 | utility.ROOT_DIR) 16 | 17 | import mssqlcli.mssqltoolsservice.externals as mssqltoolsservice 18 | 19 | # download the sqltoolssevice binaries for all platforms 20 | mssqltoolsservice.download_sqltoolsservice_binaries() 21 | 22 | # install mssqltoolsservice if this platform supports it. 23 | utility.copy_current_platform_mssqltoolsservice() 24 | 25 | print('Finished dev setup.') 26 | -------------------------------------------------------------------------------- /tests/features/steps/specials.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | """ 3 | Steps for behavioral style tests are defined in this module. 4 | Each step is defined by the string decorating it. 5 | This string is used to call the step in "*.feature" file. 6 | """ 7 | from __future__ import unicode_literals 8 | 9 | import wrappers 10 | from behave import when, then 11 | 12 | 13 | @when('we refresh completions') 14 | def step_refresh_completions(context): 15 | """ 16 | Send refresh command. 17 | """ 18 | context.cli.sendline('\\refresh') 19 | 20 | 21 | @then('we see completions refresh started') 22 | def step_see_refresh_started(context): 23 | """ 24 | Wait to see refresh output. 25 | """ 26 | wrappers.expect_pager( 27 | context, 'Auto-completion refresh started in the background.\r\n', timeout=2) 28 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | applicationinsights >= 0.11.1 2 | enum34 >= 1.1.6 ; python_version < '3.4' 3 | future >= 0.16.0 4 | setuptools >= 36.0.1 5 | wheel >= 0.29.0 6 | coverage >= 5.2.1 7 | twine >= 1.11.0 8 | bumpversion >= 0.5.3 9 | tox >= 2.7.0 10 | flake8 >= 3.3.0 11 | pytest >= 4.6.5 12 | pytest-cov >= 2.11.1 13 | pytest-timeout >= 1.3.3 14 | pylint >= 1.9.5 15 | docutils >= 0.13.1 16 | azure-storage == 0.36.0 17 | click >= 4.1, < 7.1 18 | Pygments >= 2.0 19 | prompt_toolkit >= 2.0.6 , < 4.0.0 ; python_version > '2.7' 20 | prompt_toolkit >= 2.0.6 , < 3.0.0 ; python_version <= '2.7' 21 | sqlparse >= 0.3.0,<0.5 22 | configobj >= 5.0.6 23 | humanize >= 0.5.1 24 | cli_helpers[styles] >= 2.0.0 ; python_version > '2.7' 25 | cli_helpers < 1.2.0 ; python_version <= '2.7' 26 | mock>=1.0.1 27 | polib>=1.1.0 28 | -------------------------------------------------------------------------------- /tests/features/expanded.feature: -------------------------------------------------------------------------------- 1 | Feature: expanded mode: 2 | on, off, auto 3 | 4 | Scenario: expanded on 5 | When we prepare the test data 6 | and we set expanded on 7 | and we select from table 8 | then we see expanded data selected 9 | when we drop table 10 | then we see table dropped 11 | 12 | Scenario: expanded off 13 | When we prepare the test data 14 | and we set expanded off 15 | and we select from table 16 | then we see nonexpanded data selected 17 | when we drop table 18 | then we see table dropped 19 | 20 | Scenario: expanded auto 21 | When we prepare the test data 22 | and we set expanded auto 23 | and we select from table 24 | then we see auto data selected 25 | when we drop table 26 | then we see table dropped 27 | -------------------------------------------------------------------------------- /new_hire.py: -------------------------------------------------------------------------------- 1 | # Designed for new hires to complete their first PR. 2 | # Writes to 'employee_registry.txt' by calling `python new_hire.py ` 3 | 4 | from datetime import date 5 | import os 6 | import sys 7 | import click 8 | from utility import ROOT_DIR 9 | 10 | def register_alias(alias): 11 | """ 12 | Appends text to 'employee_registry.txt' 13 | """ 14 | with open(os.path.join(ROOT_DIR, 'employee_registry.txt'), 'a') as f: 15 | f.write('{0} was here!\t{1}\n'.format(alias, date.today()).expandtabs(50)) 16 | 17 | if __name__ == "__main__": 18 | if len(sys.argv) != 2: 19 | click.secho("`new_hire.py` takes one string as an argument. " 20 | "Please provide your alias surrounded in strings, " 21 | "i.e. \"elbosc\".", err=True) 22 | else: 23 | register_alias(sys.argv[1]) 24 | -------------------------------------------------------------------------------- /tests/test_prioritization.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from mssqlcli.packages.prioritization import PrevalenceCounter 3 | 4 | class PrioritizationTests(unittest.TestCase): 5 | 6 | @staticmethod 7 | def test_prevalence_counter(): 8 | counter = PrevalenceCounter() 9 | sql = '''SELECT * FROM foo WHERE bar GROUP BY baz; 10 | select * from foo; 11 | SELECT * FROM foo WHERE bar GROUP 12 | BY baz''' 13 | counter.update(sql) 14 | 15 | keywords = ['SELECT', 'FROM', 'GROUP BY'] 16 | expected = [3, 3, 2] 17 | kw_counts = [counter.keyword_count(x) for x in keywords] 18 | assert kw_counts == expected 19 | assert counter.keyword_count('NOSUCHKEYWORD') == 0 20 | 21 | names = ['foo', 'bar', 'baz'] 22 | name_counts = [counter.name_count(x) for x in names] 23 | assert name_counts == [3, 2, 2] 24 | -------------------------------------------------------------------------------- /tests/jsonrpc/baselines/test_malformed_query.txt: -------------------------------------------------------------------------------- 1 | Content-Length: 40 2 | 3 | {"jsonrpc":"2.0","id":"1","result":true}Content-Length: 645 4 | 5 | {"jsonrpc":"2.0","method":"connection/complete","params":{"ownerUri":"connectionservicetest","connectionId":"6fd410b5-cd48-487a-bc9c-90569e749772","messages":null,"errorMessage":null,"errorNumber":0,"serverInfo":{"serverMajorVersion":12,"serverMinorVersion":0,"serverReleaseVersion":2269,"engineEditionId":3,"serverVersion":"12.0.2269.0","serverLevel":"RTM","serverEdition":"Enterprise Edition (64-bit)","isCloud":false,"azureVersion":0,"osVersion":"Windows NT 6.3 (Build 9600: ) (Hypervisor)\n","machineName":"BRO-HB"},"connectionSummary":{"serverName":"bro-hb","databaseName":"AdventureWorks2014","userName":"cloudsa"},"type":"Default"}}Content-Length: 205 6 | 7 | {"jsonrpc":"2.0","id":"2","error":{"code":0,"message":"SQL Execution error: A fatal error occurred.\r\n\tIncorrect syntax was encountered while select * from [HumanResources.Department was being parsed."}} -------------------------------------------------------------------------------- /tests/features/fixture_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from __future__ import print_function 4 | 5 | import os 6 | import codecs 7 | 8 | 9 | def read_fixture_lines(filename): 10 | """ 11 | Read lines of text from file. 12 | :param filename: string name 13 | :return: list of strings 14 | """ 15 | lines = [] 16 | for line in codecs.open(filename, 'rb', encoding='utf-8'): 17 | lines.append(line.strip()) 18 | return lines 19 | 20 | 21 | def read_fixture_files(): 22 | """ 23 | Read all files inside fixture_data directory. 24 | """ 25 | fixture_dict = {} 26 | 27 | current_dir = os.path.dirname(__file__) 28 | fixture_dir = os.path.join(current_dir, 'fixture_data/') 29 | for filename in os.listdir(fixture_dir): 30 | if filename not in ['.', '..']: 31 | fullname = os.path.join(fixture_dir, filename) 32 | fixture_dict[filename] = read_fixture_lines(fullname) 33 | 34 | return fixture_dict 35 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import shutil 3 | from mssqlcli.config import ( 4 | ensure_dir_exists, 5 | get_config, 6 | ) 7 | from mssqltestutils import getTempPath 8 | 9 | 10 | class ConfigTests(unittest.TestCase): 11 | 12 | @staticmethod 13 | def test_ensure_existing_dir(): 14 | rcfilePath = getTempPath('subdir', 'rcfile') 15 | get_config(rcfilePath) 16 | # should just not raise 17 | ensure_dir_exists(rcfilePath) 18 | shutil.rmtree(getTempPath()) 19 | 20 | 21 | # Below test does not seem to work on windows. 22 | # Commenting this out so that it doesn't fail our test runs. 23 | # Tracked by Github Issue 24 | # def test_ensure_other_create_error(self): 25 | # rcfilePath = getTempPath('subdir', 'rcfile') 26 | # get_config(rcfilePath) 27 | 28 | # # trigger an oserror that isn't "directory already exists" 29 | # os.chmod(rcfilePath, stat.S_IREAD) 30 | 31 | # with pytest.raises(OSError): 32 | # ensure_dir_exists(rcfilePath) 33 | -------------------------------------------------------------------------------- /mssqlcli/util.py: -------------------------------------------------------------------------------- 1 | from os import devnull 2 | import subprocess 3 | 4 | def encode(s): 5 | try: 6 | return s.encode('utf-8') 7 | except (AttributeError, SyntaxError): 8 | pass 9 | return s 10 | 11 | # In Python 3, all strings are sequences of Unicode characters. 12 | # There is a bytes type that holds raw bytes. 13 | # In Python 2, a string may be of type str or of type unicode. 14 | def decode(s): 15 | try: 16 | return s.decode('utf-8') 17 | except (AttributeError, SyntaxError, UnicodeEncodeError): 18 | pass 19 | return s 20 | 21 | def is_command_valid(command): 22 | """ 23 | Checks if command is recognized on machine. Used to determine installations 24 | of 'less' pager. 25 | """ 26 | if not command: 27 | return False 28 | 29 | try: 30 | # call command silentyly 31 | with open(devnull, 'wb') as no_out: 32 | subprocess.call(command, stdout=no_out, stderr=no_out) 33 | except OSError: 34 | return False 35 | else: 36 | return True 37 | -------------------------------------------------------------------------------- /release_scripts/Packages.Microsoft/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM amd64/ubuntu:18.04 AS builder 2 | 3 | RUN apt-get update 4 | RUN apt-get -y install wget curl nano sudo gnupg gnupg2 gnupg1 jq 5 | RUN apt-get -y install software-properties-common 6 | RUN apt-get -y install apt-transport-https 7 | 8 | # Requirements for installing the Repo CLI for Packages.Microsoft 9 | ADD ./release_scripts/Packages.Microsoft/config.json /root/.repoclient/config.json 10 | ADD ./release_scripts/Packages.Microsoft/private.pem /root/private.pem 11 | 12 | # Install Repo CLI requirements 13 | RUN curl http://tux-devrepo.corp.microsoft.com/keys/tux-devrepo.asc > tux-devrepo.asc 14 | RUN apt-key add tux-devrepo.asc 15 | RUN echo "deb [arch=amd64] http://tux-devrepo.corp.microsoft.com/repos/tux-dev/ xenial main" | tee /etc/apt/sources.list.d/tuxdev.list 16 | RUN apt-get update 17 | 18 | # Add mssql-cli repo 19 | WORKDIR /root 20 | RUN mkdir Repos 21 | RUN mkdir Repos/mssql-cli 22 | ADD . Repos/mssql-cli 23 | 24 | # add privileges to publish script 25 | WORKDIR /root/Repos/mssql-cli 26 | RUN chmod +x release_scripts/Packages.Microsoft/publish.sh 27 | -------------------------------------------------------------------------------- /tests/features/steps/basic_commands.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | """ 3 | Steps for behavioral style tests are defined in this module. 4 | Each step is defined by the string decorating it. 5 | This string is used to call the step in "*.feature" file. 6 | """ 7 | from __future__ import unicode_literals 8 | 9 | import tempfile 10 | 11 | from behave import when 12 | import wrappers 13 | 14 | 15 | @when('we run dbcli') 16 | def step_run_cli(context): 17 | wrappers.run(context) 18 | 19 | 20 | @when('we wait for prompt') 21 | def step_wait_prompt(context): 22 | wrappers.wait_prompt(context) 23 | 24 | 25 | @when('we send "ctrl + d"') 26 | def step_ctrl_d(context): 27 | """ 28 | Send Ctrl + D to hopefully exit. 29 | """ 30 | context.cli.sendcontrol('d') 31 | context.exit_sent = True 32 | 33 | 34 | @when('we send "\?" command') 35 | def step_send_help(context): 36 | """ 37 | Send \? to see help. 38 | """ 39 | context.cli.sendline('\?') 40 | 41 | 42 | @when(u'we send source command') 43 | def step_send_source_command(context): 44 | with tempfile.NamedTemporaryFile() as f: 45 | f.write(b'\?') 46 | f.flush() 47 | context.cli.sendline('\i {0}'.format(f.name)) 48 | wrappers.expect_exact( 49 | context, context.conf['pager_boundary'] + '\r\n', timeout=5) 50 | -------------------------------------------------------------------------------- /tests/features/steps/wrappers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | import re 5 | import pexpect 6 | 7 | 8 | def expect_exact(context, expected, timeout): 9 | try: 10 | context.cli.expect_exact(expected, timeout=timeout) 11 | except: 12 | # Strip color codes out of the output. 13 | actual = re.sub(r'\x1b\[([0-9A-Za-z;?])+[m|K]?', '', context.cli.before) 14 | raise Exception('Expected:\n---\n{0!r}\n---\n\nActual:\n---\n{1!r}\n---'.format( 15 | expected, 16 | actual)) 17 | 18 | 19 | def expect_pager(context, expected, timeout): 20 | expect_exact(context, "{0}\r\n{1}{0}\r\n".format( 21 | context.conf['pager_boundary'], expected), timeout=timeout) 22 | 23 | 24 | def run_cli(context, run_args=None): 25 | """Run the process using pexpect.""" 26 | run_args = run_args or [] 27 | cli_cmd = context.conf.get('cli_command') 28 | cmd_parts = [cli_cmd] + run_args 29 | cmd = ' '.join(cmd_parts) 30 | context.cli = pexpect.spawnu(cmd, cwd=context.package_root) 31 | context.exit_sent = False 32 | context.currentdb = context.conf['dbname'] 33 | 34 | 35 | def wait_prompt(context): 36 | """Make sure prompt is displayed.""" 37 | expect_exact(context, '{0}> '.format(context.conf['dbname']), timeout=5) 38 | -------------------------------------------------------------------------------- /mssqlcli/mssqltoolsservice/__init__.py: -------------------------------------------------------------------------------- 1 | # Template package that stores native sql tools service binaries during wheel compilation. 2 | # Files will be dynamically created here and cleaned up after each run. 3 | 4 | import os 5 | import platform 6 | 7 | 8 | def get_executable_path(): 9 | """ 10 | Find mssqltoolsservice executable relative to this package. 11 | """ 12 | # Debug mode. 13 | if 'MSSQLTOOLSSERVICE_PATH' in os.environ: 14 | mssqltoolsservice_base_path = os.environ['MSSQLTOOLSSERVICE_PATH'] 15 | else: 16 | # Retrieve path to program relative to this package. 17 | mssqltoolsservice_base_path = os.path.abspath( 18 | os.path.join( 19 | os.path.abspath(__file__), 20 | '..', 21 | 'bin')) 22 | 23 | # Format name based on platform. 24 | mssqltoolsservice_name = u'MicrosoftSqlToolsServiceLayer{}'.format( 25 | u'.exe' if (platform.system() == u'Windows') else u'') 26 | 27 | mssqltoolsservice_full_path = os.path.abspath(os.path.join(mssqltoolsservice_base_path, \ 28 | mssqltoolsservice_name)) 29 | 30 | if not os.path.exists(mssqltoolsservice_full_path): 31 | error_message = '{} does not exist. Please re-install the mssql-cli package'\ 32 | .format(mssqltoolsservice_full_path) 33 | raise EnvironmentError(error_message) 34 | 35 | return mssqltoolsservice_full_path 36 | -------------------------------------------------------------------------------- /doc/virtual_environment_info.md: -------------------------------------------------------------------------------- 1 | Virtual Environment 2 | ======================================== 3 | 4 | ## What is a virtual environment? 5 | - A isolated Python environment that is installed into a directory. 6 | - Maintains it's own copy of Python and pip (Python's package manager). 7 | - When activated all Python operations will route to the Python interpreter within the virtual environment. 8 | 9 | ## What are the benefits? 10 | - Keeps your global site-packages directory clean and manageable. 11 | - Keeps system or user installed Python and it's libraries untouched. 12 | - Solves the problem of “Project X depends on version 1.x but, Project Y needs 4.x”. 13 | - Development will not interfere with the System or the user's python. 14 | - All libraries installed in the virtual environment will only be used within that environment. 15 | 16 | ## How to install? 17 | 18 | $ pip install virtualenv 19 | 20 | ## How to create a virtual environment? 21 | In current directory: 22 | 23 | python -m venv . 24 | 25 | In a subdirectory that does not exist: 26 | 27 | python -m venv ./new_dir 28 | 29 | ## How to activate a virtual environment? 30 | ##### Windows 31 | ``` 32 | new_dir\scripts\activate.bat 33 | ``` 34 | ##### MacOS/Linux (bash) 35 | ``` 36 | . new_dir/bin/activate 37 | ``` 38 | ## How to deactivate a virtual environment? 39 | ##### Windows 40 | ``` 41 | \env\scripts\deactivate.bat 42 | ``` 43 | ##### MacOS/Linux (bash) 44 | ``` 45 | deactivate 46 | ``` 47 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Python: Launch mssql-cli (integrated)", 6 | "type": "python", 7 | "request": "launch", 8 | "cwd": "${workspaceFolder}", 9 | "program": "${workspaceFolder}/mssqlcli/main.py", 10 | "envFile": "${workspaceFolder}/.env", 11 | "console": "integratedTerminal", 12 | }, 13 | { 14 | "name": "Python: Launch mssql-cli (external)", 15 | "type": "python", 16 | "request": "launch", 17 | "cwd": "${workspaceFolder}", 18 | "program": "${workspaceFolder}/mssqlcli/main.py", 19 | "envFile": "${workspaceFolder}/.env", 20 | "console": "externalTerminal", 21 | }, 22 | { 23 | "name": "Python: Current File (external)", 24 | "type": "python", 25 | "request": "launch", 26 | "program": "${file}", 27 | "envFile": "${workspaceFolder}/.env", 28 | "console": "externalTerminal", 29 | }, 30 | { 31 | "name": "Test Query (external)", 32 | "type": "python", 33 | "request": "launch", 34 | "program": "${workspaceFolder}/mssqlcli/main.py", 35 | "envFile": "${workspaceFolder}/.env", 36 | "console": "externalTerminal", 37 | "args": [ 38 | "--query", "select * from t0" 39 | ] 40 | }, 41 | ] 42 | } -------------------------------------------------------------------------------- /tests/features/fixture_data/help_commands.txt: -------------------------------------------------------------------------------- 1 | Command 2 | Description 3 | \# 4 | Refresh auto-completions. 5 | \? 6 | Show Commands. 7 | \c[onnect] database_name 8 | Change to a new database. 9 | \copy [tablename] to/from [filename] 10 | Copy data between a file and a table. 11 | \d[+] [pattern] 12 | List or describe tables, views and sequences. | 13 | \dT[S+] [pattern] 14 | List data types 15 | \db[+] [pattern] 16 | List tablespaces. 17 | \df[+] [pattern] 18 | List functions. 19 | \di[+] [pattern] 20 | List indexes. 21 | \dm[+] [pattern] 22 | List materialized views. 23 | \dn[+] [pattern] 24 | List schemas. 25 | \ds[+] [pattern] 26 | List sequences. 27 | \dt[+] [pattern] 28 | List tables. 29 | \du[+] [pattern] 30 | List roles. 31 | \dv[+] [pattern] 32 | List views. 33 | \dx[+] [pattern] 34 | List extensions. 35 | \e [file] 36 | Edit the query with external editor. 37 | \h 38 | Show SQL syntax and help. 39 | \i filename 40 | Execute commands from file. 41 | \l 42 | List databases. 43 | \n[+] [name] [param1 param2 ...] 44 | List or execute named queries. 45 | \nd [name] 46 | Delete a named query. 47 | \ns name query 48 | Save a named query. 49 | \o [filename] 50 | Send all query results to file. 51 | \pager [command] 52 | Set PAGER. Pring the query results via PAGER. | 53 | \pset [key] [value] 54 | A limited version of traditional \pset 55 | \refresh 56 | Refresh auto-completions. 57 | \sf[+] FUNCNAME 58 | Show a function's definition. 59 | \timing 60 | Toggle timing of commands. 61 | \x 62 | Toggle expanded output. 63 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | * Redistributions in binary form must reproduce the above copyright notice, this 10 | list of conditions and the following disclaimer in the documentation and/or 11 | other materials provided with the distribution. 12 | 13 | * Neither the name of the {organization} nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 24 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /doc/roadmap.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | The following milestones outline the areas of focus for future mssql-cli work. This document is not intended as a commitment to any 3 | specific feature or timeline. Instead, the intent is to group related work into milestones using broader themes to convey the direction 4 | and priorities of mssql-cli. Actual work and focus may change, but the hope is this roadmap is our guide. 5 | 6 | ## Feedback 7 | The best way to give feedback is to create a github issue in the [dbcli/mssql-cli](https://github.com/dbcli/mssql-cli/issues) repo 8 | using the [roadmap label](https://github.com/dbcli/mssql-cli/labels/roadmap). 9 | 10 | ## Milestone 1 11 | * Parity with pgcli and mycli 12 | * Support special commands 13 | * Smart completion 14 | * Engineering Fundamentals 15 | * Travis 16 | * AppVeyor 17 | * Code coverage > 80% 18 | 19 | ## Milestone 2 20 | * Packaging 21 | * self-contained python runtimes 22 | * docker 23 | * Package manager support 24 | * brew 25 | * apt-get 26 | * yum 27 | 28 | ## Milestone 3 29 | * Non-interactive support 30 | * Pipe queries via stdin 31 | * Pipe query results to stdout 32 | * sqlcmd syntax 33 | * Support file operations 34 | * Run query from file 35 | * Save query results to file 36 | * Support other data formats as output 37 | * CSV 38 | * XML 39 | * json 40 | 41 | ## Future Milestones 42 | * Innovation 43 | * New special commands 44 | * Script object 45 | * Script table data 46 | * Script database 47 | * Peek definition 48 | * Schema compare 49 | * Per database query history 50 | -------------------------------------------------------------------------------- /tests/features/steps/named_queries.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | """ 3 | Steps for behavioral style tests are defined in this module. 4 | Each step is defined by the string decorating it. 5 | This string is used to call the step in "*.feature" file. 6 | """ 7 | from __future__ import unicode_literals 8 | 9 | import wrappers 10 | from behave import when, then 11 | 12 | 13 | @when('we save a named query') 14 | def step_save_named_query(context): 15 | """ 16 | Send \ns command 17 | """ 18 | context.cli.sendline('\\ns foo SELECT 12345') 19 | 20 | 21 | @when('we use a named query') 22 | def step_use_named_query(context): 23 | """ 24 | Send \n command 25 | """ 26 | context.cli.sendline('\\n foo') 27 | 28 | 29 | @when('we delete a named query') 30 | def step_delete_named_query(context): 31 | """ 32 | Send \nd command 33 | """ 34 | context.cli.sendline('\\nd foo') 35 | 36 | 37 | @then('we see the named query saved') 38 | def step_see_named_query_saved(context): 39 | """ 40 | Wait to see query saved. 41 | """ 42 | wrappers.expect_pager(context, 'Saved.\r\n', timeout=1) 43 | 44 | 45 | @then('we see the named query executed') 46 | def step_see_named_query_executed(context): 47 | """ 48 | Wait to see select output. 49 | """ 50 | wrappers.expect_exact(context, '12345', timeout=1) 51 | wrappers.expect_exact(context, 'SELECT 1', timeout=1) 52 | 53 | 54 | @then('we see the named query deleted') 55 | def step_see_named_query_deleted(context): 56 | """ 57 | Wait to see query deleted. 58 | """ 59 | wrappers.expect_pager(context, 'foo: Deleted\r\n', timeout=1) 60 | -------------------------------------------------------------------------------- /tests/test_multiline.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from mssqlcli.mssqlbuffer import _is_query_executable 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 | ('select 1;select 1\nGO\nselect 1;select 1\nGO', True), 22 | ("select '\nGO\n';", False) 23 | # tests below to be enabled when sqlparse supports retaining newlines 24 | # when stripping comments (tracking here: 25 | # https://github.com/andialbrecht/sqlparse/issues/484): 26 | # ('select 3 /* another open comment\n*/ GO', True), 27 | # ('select 1\n*/go', False), 28 | # ('select 1 /*\nmultiple lines!\n*/go', True) 29 | ] 30 | 31 | @staticmethod 32 | @pytest.mark.parametrize("query_str, is_query_executable", testdata) 33 | def test_multiline_completeness(query_str, is_query_executable): 34 | """ 35 | Tests the _is_query_executable helper method, which parses a T-SQL multiline 36 | statement on each newline and determines whether the script should 37 | execute. 38 | """ 39 | assert _is_query_executable(query_str) == is_query_executable 40 | -------------------------------------------------------------------------------- /tests/test_plan.wiki: -------------------------------------------------------------------------------- 1 | = Gross Checks = 2 | * [ ] Check connecting to a local database. 3 | * [ ] Check connecting to a remote database. 4 | * [ ] Check connecting to a database with a user/password. 5 | * [ ] Check connecting to a non-existent database. 6 | * [ ] Test changing the database. 7 | 8 | == PGExecute == 9 | * [ ] Test successful execution given a cursor. 10 | * [ ] Test unsuccessful execution with a syntax error. 11 | * [ ] Test a series of executions with the same cursor without failure. 12 | * [ ] Test a series of executions with the same cursor with failure. 13 | * [ ] Test passing in a special command. 14 | 15 | == Naive Autocompletion == 16 | * [ ] Input empty string, ask for completions - Everything. 17 | * [ ] Input partial prefix, ask for completions - Stars with prefix. 18 | * [ ] Input fully autocompleted string, ask for completions - Only full match 19 | * [ ] Input non-existent prefix, ask for completions - nothing 20 | * [ ] Input lowercase prefix - case insensitive completions 21 | 22 | == Smart Autocompletion == 23 | * [ ] Input empty string and check if only keywords are returned. 24 | * [ ] Input SELECT prefix and check if only columns are returned. 25 | * [ ] Input SELECT blah - only keywords are returned. 26 | * [ ] Input SELECT * FROM - Table names only 27 | 28 | == PGSpecial == 29 | * [ ] Test \d 30 | * [ ] Test \d tablename 31 | * [ ] Test \d tablena* 32 | * [ ] Test \d non-existent-tablename 33 | * [ ] Test \d index 34 | * [ ] Test \d sequence 35 | * [ ] Test \d view 36 | 37 | == Exceptionals == 38 | * [ ] Test the 'use' command to change db. 39 | -------------------------------------------------------------------------------- /mssqlcli/mssqltoolbar.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from prompt_toolkit.application import get_app 4 | from prompt_toolkit.key_binding.vi_state import InputMode 5 | 6 | 7 | def _get_vi_mode(): 8 | return { 9 | InputMode.INSERT: 'I', 10 | InputMode.NAVIGATION: 'N', 11 | InputMode.REPLACE: 'R', 12 | InputMode.INSERT_MULTIPLE: 'M', 13 | }[get_app().vi_state.input_mode] 14 | 15 | 16 | def create_toolbar_tokens_func(mssql_cli): 17 | """ 18 | Return a function that generates the toolbar tokens. 19 | """ 20 | token = 'class:toolbar' 21 | token_on = 'class:toolbar.on' 22 | token_off = 'class:toolbar.off' 23 | 24 | def get_toolbar_tokens(): 25 | result = [] 26 | result.append((token, ' ')) 27 | 28 | if mssql_cli.completer.smart_completion: 29 | result.append((token_on, '[F2] Smart Completion: ON ')) 30 | else: 31 | result.append((token_off, '[F2] Smart Completion: OFF ')) 32 | 33 | if mssql_cli.multiline: 34 | result.append((token_on, '[F3] Multiline: ON ')) 35 | else: 36 | result.append((token_off, '[F3] Multiline: OFF ')) 37 | 38 | if mssql_cli.multiline: 39 | if mssql_cli.multiline_mode == 'safe': 40 | result.append((token, ' ([Esc] [Enter] to execute]) ')) 41 | else: 42 | result.append((token, ' ([GO] statement will end the line) ')) 43 | 44 | if mssql_cli.vi_mode: 45 | result.append( 46 | (token_on, '[F4] Vi-mode (' + _get_vi_mode() + ')')) 47 | else: 48 | result.append((token_on, '[F4] Emacs-mode')) 49 | 50 | return result 51 | return get_toolbar_tokens 52 | -------------------------------------------------------------------------------- /mssqlcli/packages/prioritization.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import re 4 | from collections import defaultdict 5 | import sqlparse 6 | from sqlparse.tokens import Name 7 | from .mssqlliterals.main import get_literals 8 | 9 | 10 | white_space_regex = re.compile('\\s+', re.MULTILINE) 11 | 12 | 13 | def _compile_regex(keyword): 14 | # Surround the keyword with word boundaries and replace interior whitespace 15 | # with whitespace wildcards 16 | pattern = '\\b' + white_space_regex.sub(r'\\s+', keyword) + '\\b' 17 | return re.compile(pattern, re.MULTILINE | re.IGNORECASE) 18 | 19 | 20 | keywords = get_literals('keywords') 21 | keyword_regexs = dict((kw, _compile_regex(kw)) for kw in keywords) 22 | 23 | 24 | class PrevalenceCounter: 25 | def __init__(self): 26 | self.keyword_counts = defaultdict(int) 27 | self.name_counts = defaultdict(int) 28 | 29 | def update(self, text): 30 | self.update_keywords(text) 31 | self.update_names(text) 32 | 33 | def update_names(self, text): 34 | for parsed in sqlparse.parse(text): 35 | for token in parsed.flatten(): 36 | if token.ttype in Name: 37 | self.name_counts[token.value] += 1 38 | 39 | def clear_names(self): 40 | self.name_counts = defaultdict(int) 41 | 42 | def update_keywords(self, text): 43 | # Count keywords. Can't rely for sqlparse for this, because it's 44 | # database agnostic 45 | for keyword, regex in keyword_regexs.items(): 46 | for _ in regex.finditer(text): 47 | self.keyword_counts[keyword] += 1 48 | 49 | def keyword_count(self, keyword): 50 | return self.keyword_counts[keyword] 51 | 52 | def name_count(self, name): 53 | return self.name_counts[name] 54 | -------------------------------------------------------------------------------- /mssqlcli/packages/special/namedqueries.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ...config import get_config 3 | 4 | class NamedQueries: 5 | 6 | section_name = 'named queries' 7 | 8 | usage = u'''Named Queries are a way to save frequently used queries 9 | with a short name. Think of them as favorites. 10 | Examples: 11 | 12 | # Save a new named query. 13 | > \\sn simple select * from abc where a is not Null; 14 | 15 | # List all named queries. 16 | > \\n 17 | +--------+----------------------------------------+ 18 | | Name | Query | 19 | |--------+----------------------------------------| 20 | | simple | SELECT * FROM xyzb where a is not null | 21 | +--------+----------------------------------------+ 22 | 23 | # Run a named query. 24 | > \\n simple 25 | +-----+ 26 | | a | 27 | |-----| 28 | | 50 | 29 | +-----+ 30 | 31 | # Delete a named query. 32 | > \\dn simple 33 | simple: Deleted 34 | ''' 35 | 36 | # Class-level variable, for convenience to use as a singleton. 37 | instance = None 38 | 39 | def __init__(self, config): 40 | self.config = config 41 | 42 | def list(self): 43 | return self.config.get(self.section_name, []) 44 | 45 | def get(self, name): 46 | return self.config.get(self.section_name, {}).get(name, None) 47 | 48 | def save(self, name, query): 49 | if self.section_name not in self.config: 50 | self.config[self.section_name] = {} 51 | self.config[self.section_name][name] = query 52 | self.config.write() 53 | 54 | def delete(self, name): 55 | try: 56 | del self.config[self.section_name][name] 57 | except KeyError: 58 | return '%s: Not Found.' % name 59 | self.config.write() 60 | return '%s: Deleted' % name 61 | 62 | 63 | named_queries = NamedQueries(get_config()) 64 | -------------------------------------------------------------------------------- /tests/features/fixture_data/help.txt: -------------------------------------------------------------------------------- 1 | +--------------------------+-----------------------------------------------+ 2 | | Command | Description | 3 | |--------------------------+-----------------------------------------------| 4 | | \# | Refresh auto-completions. | 5 | | \? | Show Help. | 6 | | \c[onnect] database_name | Change to a new database. | 7 | | \d [pattern] | List or describe tables, views and sequences. | 8 | | \dT[S+] [pattern] | List data types | 9 | | \df[+] [pattern] | List functions. | 10 | | \di[+] [pattern] | List indexes. | 11 | | \dn[+] [pattern] | List schemas. | 12 | | \ds[+] [pattern] | List sequences. | 13 | | \dt[+] [pattern] | List tables. | 14 | | \du[+] [pattern] | List roles. | 15 | | \dv[+] [pattern] | List views. | 16 | | \e [file] | Edit the query with external editor. | 17 | | \l | List databases. | 18 | | \n[+] [name] | List or execute named queries. | 19 | | \nd [name [query]] | Delete a named query. | 20 | | \ns name query | Save a named query. | 21 | | \refresh | Refresh auto-completions. | 22 | | \timing | Toggle timing of commands. | 23 | | \x | Toggle expanded output. | 24 | +--------------------------+-----------------------------------------------+ 25 | -------------------------------------------------------------------------------- /tests/test_query_inputs/input_multiple_merge.txt: -------------------------------------------------------------------------------- 1 | drop table if exists dbo.category; 2 | CREATE TABLE dbo.category ( 3 | category_id INT PRIMARY KEY, 4 | category_name VARCHAR(255) NOT NULL, 5 | amount DECIMAL(10 , 2 ) 6 | ); 7 | 8 | INSERT INTO dbo.category(category_id, category_name, amount) 9 | VALUES(1,'Children Bicycles',15000), 10 | (2,'Comfort Bicycles',25000), 11 | (3,'Cruisers Bicycles',13000), 12 | (4,'Cyclocross Bicycles',10000); 13 | 14 | drop table if exists dbo.category_staging; 15 | CREATE TABLE dbo.category_staging ( 16 | category_id INT PRIMARY KEY, 17 | category_name VARCHAR(255) NOT NULL, 18 | amount DECIMAL(10 , 2 ) 19 | ); 20 | 21 | INSERT INTO dbo.category_staging(category_id, category_name, amount) 22 | VALUES(1,'Children Bicycles',15000), 23 | (3,'Cruisers Bicycles',13000), 24 | (4,'Cyclocross Bicycles',20000), 25 | (5,'Electric Bikes',10000), 26 | (6,'Mountain Bikes',10000); 27 | 28 | 29 | go 30 | 31 | --Do the merge 32 | MERGE dbo.category t 33 | USING dbo.category_staging s 34 | ON (s.category_id = t.category_id) 35 | WHEN MATCHED 36 | THEN UPDATE SET 37 | t.category_name = s.category_name, 38 | t.amount = s.amount 39 | WHEN NOT MATCHED BY TARGET 40 | THEN INSERT (category_id, category_name, amount) 41 | VALUES (s.category_id, s.category_name, s.amount) 42 | WHEN NOT MATCHED BY SOURCE 43 | THEN DELETE; 44 | 45 | go 46 | 47 | --Do the exact same statement (cut/pasted) 48 | MERGE dbo.category t 49 | USING dbo.category_staging s 50 | ON (s.category_id = t.category_id) 51 | WHEN MATCHED 52 | THEN UPDATE SET 53 | t.category_name = s.category_name, 54 | t.amount = s.amount 55 | WHEN NOT MATCHED BY TARGET 56 | THEN INSERT (category_id, category_name, amount) 57 | VALUES (s.category_id, s.category_name, s.amount) 58 | WHEN NOT MATCHED BY SOURCE 59 | THEN DELETE; 60 | go 61 | -------------------------------------------------------------------------------- /mssqlcli/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os.path import expanduser, exists, dirname 3 | 4 | import shutil 5 | import platform 6 | from configobj import ConfigObj 7 | from mssqlcli import __file__ as package_root 8 | 9 | 10 | def config_location(): 11 | if platform.system() == 'Windows': 12 | return os.getenv('LOCALAPPDATA') + '\\dbcli\\mssqlcli\\' 13 | 14 | if 'XDG_CONFIG_HOME' in os.environ: 15 | return '%s/mssqlcli/' % expanduser(os.environ['XDG_CONFIG_HOME']) 16 | 17 | return expanduser('~/.config/mssqlcli/') 18 | 19 | 20 | def load_config(usr_cfg, def_cfg=None): 21 | cfg = ConfigObj() 22 | cfg.merge(ConfigObj(def_cfg, interpolation=False)) 23 | cfg.merge(ConfigObj(expanduser(usr_cfg), interpolation=False, encoding='utf-8')) 24 | cfg.filename = expanduser(usr_cfg) 25 | 26 | return cfg 27 | 28 | 29 | def ensure_dir_exists(path): 30 | parent_dir = expanduser(dirname(path)) 31 | if not os.path.exists(parent_dir): 32 | os.makedirs(parent_dir) 33 | 34 | 35 | def write_default_config(source, destination, overwrite=False): 36 | destination = expanduser(destination) 37 | if not overwrite and exists(destination): 38 | return 39 | 40 | ensure_dir_exists(destination) 41 | 42 | shutil.copyfile(source, destination) 43 | 44 | 45 | def upgrade_config(config, def_config): 46 | cfg = load_config(config, def_config) 47 | cfg.write() 48 | 49 | 50 | def get_config(config_file=None): 51 | pkg_root = os.path.dirname(package_root) 52 | 53 | config_file = config_file or '%sconfig' % config_location() 54 | 55 | default_config = os.path.join(pkg_root, 'mssqlclirc') 56 | write_default_config(default_config, config_file) 57 | 58 | return load_config(config_file, default_config) 59 | 60 | 61 | def get_casing_file(config): 62 | casing_file = config['main']['casing_file'] 63 | if casing_file == 'default': 64 | casing_file = config_location() + 'casing' 65 | return casing_file 66 | -------------------------------------------------------------------------------- /doc/installation/windows.md: -------------------------------------------------------------------------------- 1 | # Windows Installation 2 | 3 | ## Requirements 4 | 5 | ### Supported OS Versions 6 | mssql-cli is supported on Windows with: 7 | * Windows (x86/x64) 8.1 8 | * Windows (x86/x64) 10 9 | * Windows Server 2012+ 10 | 11 | ### Python Installation 12 | 13 | Python is required for installation of mssql-cli and is not installed by default on Windows. To install Python, follow these instructions: 14 | 1. Open the latest Python installer package from [here](https://www.python.org/downloads/). 15 | 2. Select **Add Python [Version] to PATH** option before installation. Python must be in the PATH environment variable. 16 | 3. Install Python. 17 | 18 | > If Python was installed into the "Program Files" directory, you may need to open the command prompt as an administrator for the above command to succeed. 19 | 20 | Proceed to the next section once Python is installed and in the PATH environment variable. 21 | 22 | ## Installing mssql-cli 23 | > Note: your path to Python may be different from the listed command. For example, instead of `python` you may need to call `python3`. 24 | 25 | mssql-cli is installed using `pip` on Windows: 26 | ```sh 27 | python -m pip install mssql-cli 28 | ``` 29 | 30 | ## Uninstalling mssql-cli 31 | Uninstall mssql-cli using `pip`: 32 | ```sh 33 | pip uninstall mssql-cli 34 | ``` 35 | 36 | ## Installation Behind a Proxy 37 | Set two environment variables: 38 | ```sh 39 | set http_proxy=domain\username:password@proxy_server:port 40 | set https_proxy=domain\username:password@proxy_server:port 41 | ``` 42 | If the Password contains special characters like `@,$,!` (e.g. `password:PLACEHOLDER`) then replace the special characters by their hex code equivalents with `%` prefix, as exemplified below: 43 | * `@`: `%40` 44 | * `$`: `%24` 45 | * `!`: `%21` 46 | 47 | Example: `username:p%40ssword@proxy_server:port` 48 | 49 | You may attempt installation after completing these steps: 50 | 51 | ```sh 52 | pip install mssql-cli 53 | ``` 54 | -------------------------------------------------------------------------------- /tests/features/steps/expanded.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | """Steps for behavioral style tests are defined in this module. 3 | 4 | Each step is defined by the string decorating it. This string is used 5 | to call the step in "*.feature" file. 6 | 7 | """ 8 | from __future__ import unicode_literals 9 | 10 | import wrappers 11 | from behave import when, then 12 | from textwrap import dedent 13 | 14 | 15 | @when('we prepare the test data') 16 | def step_prepare_data(context): 17 | """Create table, insert a record.""" 18 | context.cli.sendline('drop table if exists a;') 19 | wrappers.wait_prompt(context) 20 | context.cli.sendline( 21 | 'create table a(x integer, y real, z numeric(10, 4));') 22 | wrappers.expect_pager(context, 'CREATE TABLE\r\n', timeout=2) 23 | context.cli.sendline('''insert into a(x, y, z) values(1, 1.0, 1.0);''') 24 | wrappers.expect_pager(context, 'INSERT 0 1\r\n', timeout=2) 25 | 26 | 27 | @when('we set expanded {mode}') 28 | def step_set_expanded(context, mode): 29 | """Set expanded to mode.""" 30 | context.cli.sendline('\\' + 'x {}'.format(mode)) 31 | wrappers.expect_exact(context, 'Expanded display is', timeout=2) 32 | wrappers.wait_prompt(context) 33 | 34 | 35 | @then('we see {which} data selected') 36 | def step_see_data(context, which): 37 | """Select data from expanded test table.""" 38 | if which == 'expanded': 39 | wrappers.expect_pager( 40 | context, 41 | dedent('''\ 42 | -[ RECORD 1 ]-------------------------\r 43 | x | 1\r 44 | y | 1.0\r 45 | z | 1.0000\r 46 | SELECT 1\r 47 | '''), 48 | timeout=1) 49 | else: 50 | wrappers.expect_pager( 51 | context, 52 | dedent('''\ 53 | +-----+-----+--------+\r 54 | | x | y | z |\r 55 | |-----+-----+--------|\r 56 | | 1 | 1.0 | 1.0000 |\r 57 | +-----+-----+--------+\r 58 | SELECT 1\r 59 | '''), 60 | timeout=1) 61 | -------------------------------------------------------------------------------- /tests/jsonrpc/generatequerybaseline.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import io 4 | import subprocess 5 | import time 6 | import mssqlcli.mssqltoolsservice as mssqltoolsservice 7 | import mssqlcli.jsonrpc.jsonrpcclient as json_rpc_client 8 | 9 | 10 | # Helper to generate a query baseline 11 | # 12 | def generate_query_baseline(file_name): 13 | sqltoolsservice_args = [mssqltoolsservice.get_executable_path()] 14 | with io.open(file_name, 'wb') as baseline: 15 | tools_service_process = subprocess.Popen( 16 | sqltoolsservice_args, 17 | bufsize=0, 18 | stdin=subprocess.PIPE, 19 | stdout=baseline) 20 | 21 | # Before running this script, enter a real server name. 22 | parameters = {u'OwnerUri': u'connectionservicetest', 23 | u'Connection': 24 | {u'ServerName': u'*', 25 | u'DatabaseName': u'AdventureWorks2014', 26 | u'UserName': u'*', 27 | u'Password': u'*', 28 | u'AuthenticationType': u'Integrated'} 29 | } 30 | 31 | writer = json_rpc_client.JsonRpcWriter(tools_service_process.stdin) 32 | writer.send_request(u'connection/connect', parameters, request_id=1) 33 | 34 | time.sleep(2) 35 | 36 | parameters = {u'OwnerUri': u'connectionservicetest', 37 | u'Query': "select * from HumanResources.Department"} 38 | writer.send_request(u'query/executeString', parameters, request_id=2) 39 | 40 | time.sleep(5) 41 | parameters = {u'OwnerUri': u'connectionservicetest', 42 | u'BatchIndex': 0, 43 | u'ResultSetIndex': 0, 44 | u'RowsStartIndex': 0, 45 | u'RowsCount': 16} 46 | 47 | writer.send_request(u'query/subset', parameters, request_id=3) 48 | # submit raw request. 49 | time.sleep(5) 50 | tools_service_process.kill() 51 | 52 | 53 | if __name__ == '__main__': 54 | generate_query_baseline( 55 | "select_from_humanresources_department_adventureworks2014.txt") 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env*/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | .noseids 50 | junit/ 51 | 52 | # macOS 53 | .swp 54 | .DS_Store 55 | 56 | # Translations 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # pyenv 80 | .python-version 81 | 82 | # celery beat schedule file 83 | celerybeat-schedule 84 | 85 | # SageMath parsed files 86 | *.sage.py 87 | 88 | # dotenv 89 | .env 90 | 91 | # sqltoolsservice binaries 92 | /mssqlcli/mssqltoolsservice/bin/* 93 | 94 | # virtualenv 95 | .venv 96 | venv/ 97 | env*/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | 112 | # Pycharm 113 | .idea/ 114 | 115 | # vscode 116 | .vscode/ 117 | 118 | # Allow 119 | !build_scripts/rpm/mssql-cli.spec 120 | 121 | # .env file 122 | .env 123 | 124 | # cert and config files for Packages.Microsoft publishing 125 | release_scripts/Packages.Microsoft/private.pem 126 | release_scripts/Packages.Microsoft/config.json 127 | -------------------------------------------------------------------------------- /tests/features/db_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from __future__ import print_function 4 | 5 | from psycopg2 import connect 6 | from psycopg2.extensions import AsIs 7 | 8 | 9 | def create_db(hostname='localhost', username=None, password=None, dbname=None, port=None): 10 | """Create test database. 11 | 12 | :param hostname: string 13 | :param username: string 14 | :param password: string 15 | :param dbname: string 16 | :param port: int 17 | :return: 18 | 19 | """ 20 | cn = create_cn(hostname, password, username, 'postgres', port) 21 | 22 | # ISOLATION_LEVEL_AUTOCOMMIT = 0 23 | # Needed for DB creation. 24 | cn.set_isolation_level(0) 25 | 26 | with cn.cursor() as cr: 27 | cr.execute('drop database if exists %s', (AsIs(dbname),)) 28 | cr.execute('create database %s', (AsIs(dbname),)) 29 | 30 | cn.close() 31 | 32 | cn = create_cn(hostname, password, username, dbname, port) 33 | return cn 34 | 35 | 36 | def create_cn(hostname, password, username, dbname, port): 37 | """ 38 | Open connection to database. 39 | :param hostname: 40 | :param password: 41 | :param username: 42 | :param dbname: string 43 | :return: psycopg2.connection 44 | """ 45 | cn = connect(host=hostname, user=username, database=dbname, 46 | password=password, port=port) 47 | 48 | print('Created connection: {0}.'.format(cn.dsn)) 49 | return cn 50 | 51 | 52 | def drop_db(hostname='localhost', username=None, password=None, 53 | dbname=None, port=None): 54 | """ 55 | Drop database. 56 | :param hostname: string 57 | :param username: string 58 | :param password: string 59 | :param dbname: string 60 | """ 61 | cn = create_cn(hostname, password, username, 'postgres', port) 62 | 63 | # ISOLATION_LEVEL_AUTOCOMMIT = 0 64 | # Needed for DB drop. 65 | cn.set_isolation_level(0) 66 | 67 | with cn.cursor() as cr: 68 | cr.execute('drop database if exists %s', (AsIs(dbname),)) 69 | 70 | close_cn(cn) 71 | 72 | 73 | def close_cn(cn=None): 74 | """ 75 | Close connection. 76 | :param connection: psycopg2.connection 77 | """ 78 | if cn: 79 | cn.close() 80 | print('Closed connection: {0}.'.format(cn.dsn)) 81 | -------------------------------------------------------------------------------- /tests/test_query_baseline/baseline_col_wide.txt: -------------------------------------------------------------------------------- 1 | +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 2 | | (No column name) | 3 | |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 4 | | X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X... | 5 | +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 6 | (1 row affected) 7 | -------------------------------------------------------------------------------- /mssqlcli/mssqlbuffer.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import re 3 | import sqlparse 4 | from prompt_toolkit.enums import DEFAULT_BUFFER 5 | from prompt_toolkit.filters import Condition 6 | from prompt_toolkit.application import get_app 7 | from .packages.parseutils.utils import is_open_quote 8 | 9 | 10 | def mssql_is_multiline(mssql_cli): 11 | @Condition 12 | def cond(): 13 | doc = get_app().layout.get_buffer_by_name(DEFAULT_BUFFER).document 14 | 15 | if not mssql_cli.multiline: 16 | return False 17 | if mssql_cli.multiline_mode == 'safe': 18 | return True 19 | return not _multiline_exception(doc.text) 20 | 21 | return cond 22 | 23 | 24 | def _is_query_executable(sql): 25 | # A complete command is an sql statement that ends with a 'GO', unless 26 | # there's an open quote surrounding it, as is common when writing a 27 | # CREATE FUNCTION command 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 50 | 51 | 52 | def _multiline_exception(text): 53 | text = text.strip() 54 | return ( 55 | text.startswith('\\') or # Special Command 56 | text.endswith(r'\e') or # Ended with \e which should launch the editor 57 | _is_query_executable(text) or # A complete SQL command 58 | (text == 'exit') or # Exit doesn't need semi-colon 59 | (text == 'quit') or # Quit doesn't need semi-colon 60 | (text == ':q') or # To all the vim fans out there 61 | (text == '') # Just a plain enter without any text 62 | ) 63 | -------------------------------------------------------------------------------- /mssqlcli/jsonrpc/contracts/request.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=too-many-arguments 2 | import logging 3 | import abc 4 | 5 | ABC = abc.ABCMeta('ABC', (object,), {}) # compatibile with Python 2 *and* 3. 6 | logger = logging.getLogger(u'mssqlcli.connectionservice') 7 | 8 | class Request(ABC): 9 | """ 10 | Abstract request class. 11 | """ 12 | 13 | def __init__(self, request_id, owner_uri, json_rpc_client, parameters, 14 | method_name, connectionCompleteEvent): 15 | self.request_id = request_id 16 | self.owner_uri = owner_uri 17 | self.json_rpc_client = json_rpc_client 18 | self.params = parameters 19 | self.method_name = method_name 20 | self.connectionCompleteEvent = connectionCompleteEvent 21 | self.finished = False 22 | 23 | @staticmethod 24 | @abc.abstractmethod 25 | def decode_response(response): 26 | """ 27 | Returns decoded response. 28 | """ 29 | pass 30 | 31 | @classmethod 32 | @abc.abstractmethod 33 | def response_error(cls, error): 34 | """ 35 | Returns object when response fails. 36 | """ 37 | pass 38 | 39 | def execute(self): 40 | """ 41 | Executes the request. 42 | """ 43 | self.json_rpc_client.submit_request( 44 | self.method_name, self.params.format(), self.request_id) 45 | 46 | def get_response(self): 47 | """ 48 | Get latest response, event or exception if it occured. 49 | """ 50 | try: 51 | response = self.json_rpc_client.get_response(self.request_id, self.owner_uri) 52 | decoded_response = None 53 | if response: 54 | logger.debug(response) 55 | decoded_response = self.decode_response(response) 56 | 57 | if isinstance(decoded_response, self.connectionCompleteEvent): 58 | self.finished = True 59 | self.json_rpc_client.request_finished(self.request_id) 60 | self.json_rpc_client.request_finished(self.owner_uri) 61 | return decoded_response 62 | except Exception as error: # pylint: disable=broad-except 63 | logger.info(str(error)) 64 | self.finished = True 65 | self.json_rpc_client.request_finished(self.request_id) 66 | self.json_rpc_client.request_finished(self.owner_uri) 67 | return self.response_error(error) 68 | 69 | def completed(self): 70 | """ 71 | Return state of request. 72 | """ 73 | return self.finished 74 | -------------------------------------------------------------------------------- /tests/features/steps/auto_vertical.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | from __future__ import unicode_literals 3 | from textwrap import dedent 4 | 5 | from behave import then, when 6 | 7 | import wrappers 8 | 9 | 10 | @when('we run dbcli with {arg}') 11 | def step_run_cli_with_arg(context, arg): 12 | wrappers.run(context, run_args=arg.split('=')) 13 | 14 | 15 | @when('we execute a small query') 16 | def step_execute_small_query(context): 17 | context.cli.sendline('select 1') 18 | 19 | 20 | @when('we execute a large query') 21 | def step_execute_large_query(context): 22 | context.cli.sendline( 23 | 'select {}'.format(','.join([str(n) for n in range(1, 50)]))) 24 | 25 | 26 | @then('we see small results in horizontal format') 27 | def step_see_small_results(context): 28 | wrappers.expect_pager(context, dedent("""\ 29 | +------------+\r 30 | | ?column? |\r 31 | |------------|\r 32 | | 1 |\r 33 | +------------+\r 34 | SELECT 1\r 35 | """), timeout=5) 36 | 37 | 38 | @then('we see large results in vertical format') 39 | def step_see_large_results(context): 40 | wrappers.expect_pager(context, dedent("""\ 41 | -[ RECORD 1 ]-------------------------\r 42 | ?column? | 1\r 43 | ?column? | 2\r 44 | ?column? | 3\r 45 | ?column? | 4\r 46 | ?column? | 5\r 47 | ?column? | 6\r 48 | ?column? | 7\r 49 | ?column? | 8\r 50 | ?column? | 9\r 51 | ?column? | 10\r 52 | ?column? | 11\r 53 | ?column? | 12\r 54 | ?column? | 13\r 55 | ?column? | 14\r 56 | ?column? | 15\r 57 | ?column? | 16\r 58 | ?column? | 17\r 59 | ?column? | 18\r 60 | ?column? | 19\r 61 | ?column? | 20\r 62 | ?column? | 21\r 63 | ?column? | 22\r 64 | ?column? | 23\r 65 | ?column? | 24\r 66 | ?column? | 25\r 67 | ?column? | 26\r 68 | ?column? | 27\r 69 | ?column? | 28\r 70 | ?column? | 29\r 71 | ?column? | 30\r 72 | ?column? | 31\r 73 | ?column? | 32\r 74 | ?column? | 33\r 75 | ?column? | 34\r 76 | ?column? | 35\r 77 | ?column? | 36\r 78 | ?column? | 37\r 79 | ?column? | 38\r 80 | ?column? | 39\r 81 | ?column? | 40\r 82 | ?column? | 41\r 83 | ?column? | 42\r 84 | ?column? | 43\r 85 | ?column? | 44\r 86 | ?column? | 45\r 87 | ?column? | 46\r 88 | ?column? | 47\r 89 | ?column? | 48\r 90 | ?column? | 49\r 91 | SELECT 1\r 92 | """), timeout=5) 93 | -------------------------------------------------------------------------------- /doc/telemetry_guide.md: -------------------------------------------------------------------------------- 1 | # Telemetry Guide 2 | 3 | By default, Microsoft collects anonymous usage data in order to improve the user experience. The usage data collected allows the team to prioritize features and bug fixes. 4 | 5 | Microsoft Privacy statement: https://go.microsoft.com/fwlink/?LinkId=521839 6 | 7 | ## Table of Contents 8 | 1. [How do we anonymize the data?](#anonymize) 9 | 1. [What do we collect](#collect) 10 | 2. [How do I opt out?](#opt_out) 11 | 3. [Additional Information](#information) 12 | 13 | 14 | ### How do we anonymize data? 15 | Hashed random UUID: a cryptographically (SHA256) anonymous and unique ID per user. 16 | 17 | ### What do we collect? 18 | ##### Environment 19 | Environment data points give the team insight into commonly used platforms and allow the team to prioritize features, bug fixes, and packaging. 20 | - Platform name 21 | - Platform version 22 | - Target Server edition and version 23 | - Connection Type (Azure vs. Single Instance) 24 | - Python version 25 | - Product version 26 | - Locale 27 | - Shell type 28 | 29 | ##### Measurements 30 | Measurement data points allow the team to be proactive in improving performance. 31 | - Start time 32 | - End time 33 | - Session duration 34 | 35 | ##### Errors 36 | Abnormal process termination. 37 | - Error flag to indicate type of shutdown. 38 | 39 | ### How do I opt out? 40 | The mssql-cli Tools telemetry feature is enabled by default. Opt-out of the telemetry feature by setting the ```MSSQL_CLI_TELEMETRY_OPTOUT``` environment variable to ```true``` or ```1```. 41 | 42 | ##### Windows 43 | ``` 44 | set MSSQL_CLI_TELEMETRY_OPTOUT=True 45 | ``` 46 | ##### MacOS/Linux (bash) 47 | ``` 48 | export MSSQL_CLI_TELEMETRY_OPTOUT=True 49 | ``` 50 | 51 | ### Additional Information 52 | 53 | ##### What does the anonymous usage data actually look like? 54 | For those interested, anonymous usage data of the previous session will always be outputted to a telemetry file in the user's configuration directory. It can be found right next to the location of the configuration file. 55 | ##### MacOSX & Linux 56 | ``` 57 | ~/.config/mssqlcli/mssqlcli_telemetry.log 58 | ``` 59 | ##### Windows 60 | ``` 61 | C:\Users\\AppData\Local\dbcli\mssqlcli\mssqlcli_telemetry.log 62 | ``` 63 | 64 | ##### Partial sample of log content 65 | ``` 66 | { 67 | "name": "mssqlcli", 68 | "properties": { 69 | "Reserved.SequenceNumber": 1, 70 | "Reserved.EventId": "e6f0bdab-65b0-4e79-87a4-b05fa514c92d", 71 | "Reserved.SessionId": "3fa7361c-50ff-4bff-8a55-f22bdc26452d", 72 | } 73 | } 74 | ``` 75 | -------------------------------------------------------------------------------- /doc/installation/README.md: -------------------------------------------------------------------------------- 1 | # Install mssql-cli 2 | 3 | ## Official Installation of mssql-cli 4 | 5 | Select your platform for mssql-cli installation instructions: 6 | 7 | | [Windows](https://github.com/dbcli/mssql-cli/blob/master/doc/installation/windows.md#windows-installation) (preview)| [macOS](https://github.com/dbcli/mssql-cli/blob/master/doc/installation/macos.md#macos-installation) | [Linux](https://github.com/dbcli/mssql-cli/blob/master/doc/installation/linux.md) | 8 | | - | - | - | 9 | 10 | 11 | ## Direct Downloads 12 | 13 | The instructions above are the preferred installation method. Direct downloads are provided as an alternative in the 14 | scenario that your machine may not have access to the Microsoft package repository. Instructions for direct downloads can 15 | also be found in the links above. 16 | 17 | | Supported Platform |Latest Stable | 18 | |--------------------------------------------|------------------------------| 19 | | Windows (x64) |[.whl][whl-win-x64] | 20 | | Windows (x86) |[.whl][whl-win-x86] | 21 | | macOS 10.12+ |[.whl][whl-macos] | 22 | | Linux (Python Wheel) |[.whl][whl-linux] | 23 | | Ubuntu 14.04+ |[.deb][deb] | 24 | | Debian 8.7+ |[.deb][deb] | 25 | | CentOS 7+ |[.rpm][rpm] | 26 | | Red Hat Enterprise Linux 7+ |[.rpm][rpm] | 27 | | openSUSE |[.rpm][rpm] | 28 | | Fedora 25+ |[.rpm][rpm] | 29 | 30 | 31 | [deb]: https://packages.microsoft.com/ubuntu/16.04/prod/pool/main/m/mssql-cli/mssql-cli_1.0.0-1_all.deb 32 | 33 | [rpm]: https://packages.microsoft.com/centos/7/prod/mssql-cli-1.0.0-1.el7.x86_64.rpm 34 | 35 | [whl-win-x64]: https://files.pythonhosted.org/packages/f6/cd/cf9be6175ccc241fd70e11e8d8d6455a630e06ffe1c937034b37e1301b2c/mssql_cli-1.0.0-py2.py3-none-win_amd64.whl 36 | 37 | [whl-win-x86]: https://files.pythonhosted.org/packages/08/e0/38d4721bcc0f5f013e8a05b722f55675185dc9e62f460a8615f25e4f0098/mssql_cli-1.0.0-py2.py3-none-win32.whl 38 | 39 | [whl-macos]: https://files.pythonhosted.org/packages/14/2f/8ba644a5f8a51048a749441113acd51df282b20ad1497b2aaf599adb10db/mssql_cli-1.0.0-py2.py3-none-macosx_10_11_intel.whl 40 | 41 | [whl-linux]: https://files.pythonhosted.org/packages/46/98/257260e7a520291de8168c91bb10778dcae324e2a236e856dd3cce7fe0b1/mssql_cli-1.0.0-py2.py3-none-manylinux1_x86_64.whl 42 | -------------------------------------------------------------------------------- /mssqlcli/key_bindings.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from prompt_toolkit.enums import EditingMode 3 | from prompt_toolkit.key_binding import KeyBindings 4 | from .filters import has_selected_completion 5 | 6 | _logger = logging.getLogger(__name__) 7 | 8 | 9 | def mssqlcli_bindings(mssql_cli): 10 | """ 11 | Custom key bindings for mssqlcli. 12 | """ 13 | key_bindings = KeyBindings() 14 | 15 | @key_bindings.add(u'f2') 16 | def _(event): 17 | """ 18 | Enable/Disable SmartCompletion Mode. 19 | """ 20 | del event # event is unused 21 | _logger.debug('Detected F2 key.') 22 | mssql_cli.completer.smart_completion = not mssql_cli.completer.smart_completion 23 | 24 | @key_bindings.add(u'f3') 25 | def _(event): 26 | """ 27 | Enable/Disable Multiline Mode. 28 | """ 29 | del event # event is unused 30 | _logger.debug('Detected F3 key.') 31 | mssql_cli.multiline = not mssql_cli.multiline 32 | 33 | @key_bindings.add(u'f4') 34 | def _(event): 35 | """ 36 | Toggle between Vi and Emacs mode. 37 | """ 38 | _logger.debug('Detected F4 key.') 39 | mssql_cli.vi_mode = not mssql_cli.vi_mode 40 | event.app.editing_mode = EditingMode.VI if mssql_cli.vi_mode else EditingMode.EMACS 41 | 42 | @key_bindings.add(u'tab') 43 | def _(event): 44 | """ 45 | Force autocompletion at cursor. 46 | """ 47 | _logger.debug('Detected key.') 48 | b = event.app.current_buffer 49 | if b.complete_state: 50 | b.complete_next() 51 | else: 52 | b.start_completion(select_first=True) 53 | 54 | @key_bindings.add(u'c-space') 55 | def _(event): 56 | """ 57 | Initialize autocompletion at cursor. 58 | 59 | If the autocompletion menu is not showing, display it with the 60 | appropriate completions for the context. 61 | 62 | If the menu is showing, select the next completion. 63 | """ 64 | _logger.debug('Detected key.') 65 | 66 | b = event.app.current_buffer 67 | if b.complete_state: 68 | b.complete_next() 69 | else: 70 | b.start_completion(select_first=False) 71 | 72 | @key_bindings.add(u'enter', filter=has_selected_completion) 73 | def _(event): 74 | """ 75 | Makes the enter key work as the tab key only when showing the menu. 76 | """ 77 | _logger.debug('Detected enter key.') 78 | 79 | event.current_buffer.complete_state = None 80 | b = event.app.current_buffer 81 | b.complete_state = None 82 | 83 | return key_bindings 84 | -------------------------------------------------------------------------------- /tests/test_rowlimit.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=protected-access 2 | import unittest 3 | import pytest 4 | from mssqlcli.mssql_cli import MssqlCli 5 | from mssqlcli.mssqlclioptionsparser import check_row_limit 6 | from mssqltestutils import create_mssql_cli_options 7 | 8 | class RowLimitTests(unittest.TestCase): 9 | 10 | DEFAULT_OPTIONS = create_mssql_cli_options() 11 | DEFAULT = MssqlCli(DEFAULT_OPTIONS).row_limit 12 | LIMIT = DEFAULT + 1000 13 | 14 | low_count = 1 15 | over_default = DEFAULT + 1 16 | over_limit = LIMIT + 1 17 | 18 | def test_default_row_limit(self): 19 | cli = MssqlCli(self.DEFAULT_OPTIONS) 20 | stmt = "SELECT * FROM students" 21 | result = cli._should_show_limit_prompt(stmt, ['row']*self.low_count) 22 | assert not result 23 | 24 | result = cli._should_show_limit_prompt(stmt, ['row']*self.over_default) 25 | assert result 26 | 27 | def test_no_limit(self): 28 | cli_options = create_mssql_cli_options(row_limit=0) 29 | cli = MssqlCli(cli_options) 30 | assert cli.row_limit == 0 31 | stmt = "SELECT * FROM students" 32 | 33 | result = cli._should_show_limit_prompt(stmt, ['row']*self.over_limit) 34 | assert not result 35 | 36 | def test_row_limit_on_non_select(self): 37 | cli = MssqlCli(self.DEFAULT_OPTIONS) 38 | stmt = "UPDATE students set name='Boby'" 39 | result = cli._should_show_limit_prompt(stmt, None) 40 | assert not result 41 | 42 | cli_options = create_mssql_cli_options(row_limit=0) 43 | assert cli_options.row_limit == 0 44 | cli = MssqlCli(cli_options) 45 | result = cli._should_show_limit_prompt(stmt, ['row']*self.over_default) 46 | assert cli.row_limit == 0 47 | assert not result 48 | 49 | 50 | class TestRowLimitArgs: 51 | """ 52 | Tests for valid row-limit arguments. 53 | """ 54 | # pylint: disable=protected-access 55 | 56 | test_data_valid = [5, 0] 57 | test_data_invalid = ['string!', -3] 58 | 59 | @staticmethod 60 | @pytest.mark.parametrize("row_limit", test_data_valid) 61 | def test_valid_row_limit(row_limit): 62 | """ 63 | Test valid value types for row limit argument 64 | """ 65 | assert check_row_limit(row_limit) == row_limit 66 | 67 | @staticmethod 68 | @pytest.mark.parametrize("row_limit", test_data_invalid) 69 | def test_invalid_row_limit(row_limit): 70 | """ 71 | Test invalid value types for row limit argument 72 | """ 73 | try: 74 | check_row_limit(row_limit) 75 | except SystemExit: 76 | # mssqlcli class calls sys.exit(1) on invalid value 77 | assert True 78 | else: 79 | assert False 80 | -------------------------------------------------------------------------------- /tests/features/steps/crud_database.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Steps for behavioral style tests are defined in this module. 4 | Each step is defined by the string decorating it. 5 | This string is used to call the step in "*.feature" file. 6 | """ 7 | from __future__ import unicode_literals 8 | 9 | import pexpect 10 | 11 | import wrappers 12 | from behave import when, then 13 | 14 | 15 | @when('we create database') 16 | def step_db_create(context): 17 | """ 18 | Send create database. 19 | """ 20 | context.cli.sendline('create database {0};'.format( 21 | context.conf['dbname_tmp'])) 22 | 23 | context.response = { 24 | 'database_name': context.conf['dbname_tmp'] 25 | } 26 | 27 | 28 | @when('we drop database') 29 | def step_db_drop(context): 30 | """ 31 | Send drop database. 32 | """ 33 | context.cli.sendline('drop database {0};'.format( 34 | context.conf['dbname_tmp'])) 35 | 36 | 37 | @when('we connect to test database') 38 | def step_db_connect_test(context): 39 | """ 40 | Send connect to database. 41 | """ 42 | db_name = context.conf['dbname'] 43 | context.cli.sendline('\\connect {0}'.format(db_name)) 44 | 45 | 46 | @when('we connect to dbserver') 47 | def step_db_connect_dbserver(context): 48 | """ 49 | Send connect to database. 50 | """ 51 | context.cli.sendline('\\connect postgres') 52 | context.currentdb = 'postgres' 53 | 54 | 55 | @then('dbcli exits') 56 | def step_wait_exit(context): 57 | """ 58 | Make sure the cli exits. 59 | """ 60 | wrappers.expect_exact(context, pexpect.EOF, timeout=5) 61 | 62 | 63 | @then('we see dbcli prompt') 64 | def step_see_prompt(context): 65 | """ 66 | Wait to see the prompt. 67 | """ 68 | wrappers.expect_exact(context, '{0}> '.format(context.conf['dbname']), timeout=5) 69 | context.atprompt = True 70 | 71 | 72 | @then('we see help output') 73 | def step_see_help(context): 74 | for expected_line in context.fixture_data['help_commands.txt']: 75 | wrappers.expect_exact(context, expected_line, timeout=1) 76 | 77 | 78 | @then('we see database created') 79 | def step_see_db_created(context): 80 | """ 81 | Wait to see create database output. 82 | """ 83 | wrappers.expect_pager(context, 'CREATE DATABASE\r\n', timeout=5) 84 | 85 | 86 | @then('we see database dropped') 87 | def step_see_db_dropped(context): 88 | """ 89 | Wait to see drop database output. 90 | """ 91 | wrappers.expect_pager(context, 'DROP DATABASE\r\n', timeout=2) 92 | 93 | 94 | @then('we see database connected') 95 | def step_see_db_connected(context): 96 | """ 97 | Wait to see drop database output. 98 | """ 99 | wrappers.expect_exact(context, 'You are now connected to database', timeout=2) 100 | -------------------------------------------------------------------------------- /tests/features/steps/iocommands.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | from __future__ import unicode_literals 3 | import os 4 | import os.path 5 | import wrappers 6 | 7 | from behave import when, then 8 | 9 | 10 | @when('we start external editor providing a file name') 11 | def step_edit_file(context): 12 | """Edit file with external editor.""" 13 | context.editor_file_name = os.path.join( 14 | context.package_root, 'test_file_{0}.sql'.format(context.conf['vi'])) 15 | if os.path.exists(context.editor_file_name): 16 | os.remove(context.editor_file_name) 17 | context.cli.sendline('\e {0}'.format( 18 | os.path.basename(context.editor_file_name))) 19 | wrappers.expect_exact( 20 | context, 'Entering Ex mode. Type "visual" to go to Normal mode.', timeout=2) 21 | wrappers.expect_exact(context, '\r\n:', timeout=2) 22 | 23 | 24 | @when('we type sql in the editor') 25 | def step_edit_type_sql(context): 26 | context.cli.sendline('i') 27 | context.cli.sendline('select * from abc') 28 | context.cli.sendline('.') 29 | wrappers.expect_exact(context, ':', timeout=2) 30 | 31 | 32 | @when('we exit the editor') 33 | def step_edit_quit(context): 34 | context.cli.sendline('x') 35 | wrappers.expect_exact(context, "written", timeout=2) 36 | 37 | 38 | @then('we see the sql in prompt') 39 | def step_edit_done_sql(context): 40 | for match in 'select * from abc'.split(' '): 41 | wrappers.expect_exact(context, match, timeout=1) 42 | # Cleanup the command line. 43 | context.cli.sendcontrol('c') 44 | # Cleanup the edited file. 45 | if context.editor_file_name and os.path.exists(context.editor_file_name): 46 | os.remove(context.editor_file_name) 47 | 48 | 49 | @when(u'we tee output') 50 | def step_tee_ouptut(context): 51 | context.tee_file_name = os.path.join( 52 | context.package_root, 'tee_file_{0}.sql'.format(context.conf['vi'])) 53 | if os.path.exists(context.tee_file_name): 54 | os.remove(context.tee_file_name) 55 | context.cli.sendline('\o {0}'.format( 56 | os.path.basename(context.tee_file_name))) 57 | wrappers.expect_exact( 58 | context, context.conf['pager_boundary'] + '\r\n', timeout=5) 59 | wrappers.expect_exact(context, "Writing to file", timeout=5) 60 | wrappers.expect_exact( 61 | context, context.conf['pager_boundary'] + '\r\n', timeout=5) 62 | wrappers.expect_exact(context, "Time", timeout=5) 63 | 64 | 65 | @when(u'we query "select 123456"') 66 | def step_query_select_123456(context): 67 | context.cli.sendline('select 123456') 68 | 69 | 70 | @when(u'we notee output') 71 | def step_notee_output(context): 72 | context.cli.sendline('notee') 73 | wrappers.expect_exact(context, "Time", timeout=5) 74 | 75 | 76 | @then(u'we see 123456 in tee output') 77 | def step_see_123456_in_ouput(context): 78 | with open(context.tee_file_name) as f: 79 | assert '123456' in f.read() 80 | if os.path.exists(context.tee_file_name): 81 | os.remove(context.tee_file_name) 82 | context.atprompt = True 83 | -------------------------------------------------------------------------------- /tests/features/steps/crud_table.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | """ 3 | Steps for behavioral style tests are defined in this module. 4 | Each step is defined by the string decorating it. 5 | This string is used to call the step in "*.feature" file. 6 | """ 7 | from __future__ import unicode_literals 8 | 9 | import wrappers 10 | from behave import when, then 11 | from textwrap import dedent 12 | 13 | 14 | @when('we create table') 15 | def step_create_table(context): 16 | """ 17 | Send create table. 18 | """ 19 | context.cli.sendline('create table a(x text);') 20 | 21 | 22 | @when('we insert into table') 23 | def step_insert_into_table(context): 24 | """ 25 | Send insert into table. 26 | """ 27 | context.cli.sendline('''insert into a(x) values('xxx');''') 28 | 29 | 30 | @when('we update table') 31 | def step_update_table(context): 32 | """ 33 | Send insert into table. 34 | """ 35 | context.cli.sendline('''update a set x = 'yyy' where x = 'xxx';''') 36 | 37 | 38 | @when('we select from table') 39 | def step_select_from_table(context): 40 | """ 41 | Send select from table. 42 | """ 43 | context.cli.sendline('select * from a;') 44 | 45 | 46 | @when('we delete from table') 47 | def step_delete_from_table(context): 48 | """ 49 | Send deete from table. 50 | """ 51 | context.cli.sendline('''delete from a where x = 'yyy';''') 52 | 53 | 54 | @when('we drop table') 55 | def step_drop_table(context): 56 | """ 57 | Send drop table. 58 | """ 59 | context.cli.sendline('drop table a;') 60 | 61 | 62 | @then('we see table created') 63 | def step_see_table_created(context): 64 | """ 65 | Wait to see create table output. 66 | """ 67 | wrappers.expect_pager(context, 'CREATE TABLE\r\n', timeout=2) 68 | 69 | 70 | @then('we see record inserted') 71 | def step_see_record_inserted(context): 72 | """ 73 | Wait to see insert output. 74 | """ 75 | wrappers.expect_pager(context, 'INSERT 0 1\r\n', timeout=2) 76 | 77 | 78 | @then('we see record updated') 79 | def step_see_record_updated(context): 80 | """ 81 | Wait to see update output. 82 | """ 83 | wrappers.expect_pager(context, 'UPDATE 1\r\n', timeout=2) 84 | 85 | 86 | @then('we see data selected') 87 | def step_see_data_selected(context): 88 | """ 89 | Wait to see select output. 90 | """ 91 | wrappers.expect_pager( 92 | context, 93 | dedent('''\ 94 | +-----+\r 95 | | x |\r 96 | |-----|\r 97 | | yyy |\r 98 | +-----+\r 99 | SELECT 1\r 100 | '''), 101 | timeout=1) 102 | 103 | 104 | @then('we see record deleted') 105 | def step_see_data_deleted(context): 106 | """ 107 | Wait to see delete output. 108 | """ 109 | wrappers.expect_pager(context, 'DELETE 1\r\n', timeout=2) 110 | 111 | 112 | @then('we see table dropped') 113 | def step_see_table_dropped(context): 114 | """ 115 | Wait to see drop output. 116 | """ 117 | wrappers.expect_pager(context, 'DROP TABLE\r\n', timeout=2) 118 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import os 3 | import re 4 | import datetime 5 | # pylint: disable=redefined-builtin 6 | from codecs import open 7 | from setuptools import setup, find_packages 8 | 9 | _version_re = re.compile(r'__version__\s+=\s+(.*)') 10 | 11 | with open('mssqlcli/__init__.py', 'rb') as f: 12 | version = str(ast.literal_eval(_version_re.search( 13 | f.read().decode('utf-8')).group(1))) 14 | 15 | description = 'CLI for SQL Server Database. With auto-completion and syntax highlighting.' 16 | 17 | MSSQLTOOLSSERVICE_VERSION = '1.0.0a21' 18 | 19 | 20 | def get_timestamped_version(ver): 21 | """ 22 | Appends .dev to version. 23 | :param version: The version number 24 | :return: .dev. Example 0.0.1.dev1711030310 25 | """ 26 | return ver + '.dev' + datetime.datetime.now().strftime("%y%m%d%H%M") 27 | 28 | 29 | install_requirements = [ 30 | 'click >= 4.1,<7.1', 31 | 'Pygments >= 2.0', # Pygments has to be Capitalcased. 32 | 'prompt_toolkit>=2.0.6,<4.0.0;python_version>"2.7"', 33 | 'prompt_toolkit>=2.0.6,<3.0.0;python_version<="2.7"', 34 | 'sqlparse >=0.3.0,<0.5', 35 | 'configobj >= 5.0.6', 36 | 'humanize >= 0.5.1', 37 | 'cli_helpers[styles] >= 2.0.0;python_version>"2.7"', 38 | 'cli_helpers < 1.2.0;python_version<="2.7"', 39 | 'applicationinsights>=0.11.1', 40 | 'future>=0.16.0', 41 | 'wheel>=0.29.0', 42 | 'enum34>=1.1.6;python_version<"3.4"' 43 | ] 44 | 45 | with open("README.md", "r") as fh: 46 | long_description = fh.read() 47 | 48 | setup( 49 | name='mssql-cli', 50 | author='Microsoft Corporation', 51 | author_email='sqlcli@microsoft.com', 52 | version=version if os.environ.get('MSSQL_CLI_OFFICIAL_BUILD', '').lower() == 'true' 53 | else get_timestamped_version(version), 54 | license='BSD-3', 55 | url='https://github.com/dbcli/mssql-cli', 56 | packages=find_packages(), 57 | package_data={'mssqlcli': ['mssqlclirc', 58 | 'packages/mssqlliterals/sqlliterals.json']}, 59 | description=description, 60 | long_description=long_description, 61 | long_description_content_type='text/markdown', 62 | install_requires=install_requirements, 63 | include_package_data=True, 64 | scripts=[ 65 | 'mssql-cli.bat', 66 | 'mssql-cli' 67 | ], 68 | classifiers=[ 69 | 'Intended Audience :: Developers', 70 | 'License :: OSI Approved :: BSD License', 71 | 'Operating System :: Unix', 72 | 'Programming Language :: Python', 73 | 'Programming Language :: Python :: 2.7', 74 | 'Programming Language :: Python :: 3', 75 | 'Programming Language :: Python :: 3.5', 76 | 'Programming Language :: Python :: 3.6', 77 | 'Programming Language :: Python :: 3.7', 78 | 'Programming Language :: Python :: 3.8', 79 | 'Programming Language :: Python :: 3.9', 80 | 'Programming Language :: SQL', 81 | 'Topic :: Database', 82 | 'Topic :: Database :: Front-Ends', 83 | 'Topic :: Software Development', 84 | 'Topic :: Software Development :: Libraries :: Python Modules', 85 | ], 86 | ) 87 | -------------------------------------------------------------------------------- /mssqlcli/decorators.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility decorators 3 | 4 | The module variable is_diagnostics_mode can be set to a boolean or callable. It is used to determine 5 | if an exception should be re-raised when the suppression decorator is requested to raise in 6 | diagnostics mode. 7 | 8 | """ 9 | 10 | import hashlib 11 | from functools import wraps 12 | 13 | # module global variable 14 | is_diagnostics_mode = False 15 | 16 | 17 | # internal functions 18 | 19 | def _should_raise(raise_in_diagnostics): 20 | """ 21 | This utitilty method is used in exception suppression decorator to determine if an exception 22 | should be re-raised. 23 | 24 | :param raise_in_diagnostics: The boolean value given to the suppression decorator to indicate if 25 | exception should be re-raised in diagnostics mode. 26 | """ 27 | if not raise_in_diagnostics: 28 | return False 29 | if not is_diagnostics_mode: 30 | return False 31 | if isinstance(is_diagnostics_mode, bool) and is_diagnostics_mode: 32 | return True 33 | if hasattr(is_diagnostics_mode, '__call__') and \ 34 | is_diagnostics_mode(): # pylint: disable=not-callable 35 | return True 36 | return False 37 | 38 | 39 | def call_once(factory_func): 40 | """" 41 | When a function is annotated by this decorator, it will be only executed once. The result will 42 | be cached and return for following invocations. 43 | """ 44 | factory_func.executed = False 45 | factory_func.cached_result = None 46 | 47 | def _wrapped(*args, **kwargs): 48 | if not factory_func.executed: 49 | factory_func.cached_result = factory_func(*args, **kwargs) 50 | 51 | return factory_func.cached_result 52 | return _wrapped 53 | 54 | 55 | def hash256_result(func): 56 | """Secure the return string of the annotated function with SHA256 algorithm. If the annotated 57 | function doesn't return string or return None, raise ValueError.""" 58 | @wraps(func) 59 | def _decorator(*args, **kwargs): 60 | val = func(*args, **kwargs) 61 | if not val: 62 | raise ValueError('Return value is None') 63 | if not isinstance(val, str): 64 | raise ValueError('Return value is not string') 65 | hash_object = hashlib.sha256(val.encode('utf-8')) 66 | return str(hash_object.hexdigest()) 67 | return _decorator 68 | 69 | 70 | def suppress_all_exceptions(raise_in_diagnostics=False, fallback_return=None): 71 | def _decorator(func): 72 | @wraps(func) 73 | def _wrapped_func(*args, **kwargs): 74 | try: 75 | return func(*args, **kwargs) 76 | except Exception as ex: # nopa pylint: disable=broad-except 77 | if _should_raise(raise_in_diagnostics): 78 | raise ex 79 | if fallback_return: 80 | return fallback_return 81 | 82 | return _wrapped_func 83 | 84 | return _decorator 85 | 86 | 87 | def transfer_doc(source_func): 88 | def _decorator(func): 89 | func.__doc__ = source_func.__doc__ 90 | return func 91 | return _decorator 92 | -------------------------------------------------------------------------------- /mssqlcli/mssqltoolsservice/externals.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import os 4 | import sys 5 | import tarfile 6 | import zipfile 7 | from future.standard_library import install_aliases 8 | import requests 9 | import utility 10 | 11 | install_aliases() 12 | 13 | SQLTOOLSSERVICE_RELEASE = "v3.0.0-release.72" 14 | 15 | SQLTOOLSSERVICE_BASE = os.path.join(utility.ROOT_DIR, 'sqltoolsservice/') 16 | 17 | # Supported platform key's must match those in mssqlscript's setup.py. 18 | SUPPORTED_PLATFORMS = { 19 | 'manylinux1_x86_64': SQLTOOLSSERVICE_BASE + 'manylinux1/' + 20 | 'Microsoft.SqlTools.ServiceLayer-rhel-x64-netcoreapp3.1.tar.gz', 21 | 'macosx_10_11_intel': SQLTOOLSSERVICE_BASE + 'macosx_10_11_intel/' + 22 | 'Microsoft.SqlTools.ServiceLayer-osx-x64-netcoreapp3.1.tar.gz', 23 | 'win_amd64': SQLTOOLSSERVICE_BASE + 'win_amd64/' + 24 | 'Microsoft.SqlTools.ServiceLayer-win-x64-netcoreapp3.1.zip', 25 | 'win32': SQLTOOLSSERVICE_BASE + 'win32/' + 26 | 'Microsoft.SqlTools.ServiceLayer-win-x86-netcoreapp3.1.zip' 27 | } 28 | 29 | TARGET_DIRECTORY = os.path.abspath(os.path.join(os.path.abspath(__file__), '..', 'bin')) 30 | 31 | def download_sqltoolsservice_binaries(): 32 | """ 33 | Download each for the plaform specific sqltoolsservice packages 34 | """ 35 | for packageFilePath in SUPPORTED_PLATFORMS.values(): 36 | if not os.path.exists(os.path.dirname(packageFilePath)): 37 | os.makedirs(os.path.dirname(packageFilePath)) 38 | 39 | packageFileName = os.path.basename(packageFilePath) 40 | githubUrl = 'https://github.com/microsoft/sqltoolsservice/releases/download/{}/{}'.format(SQLTOOLSSERVICE_RELEASE, packageFileName) 41 | print('Downloading {}'.format(githubUrl)) 42 | r = requests.get(githubUrl) 43 | with open(packageFilePath, 'wb') as f: 44 | f.write(r.content) 45 | 46 | def copy_sqltoolsservice(platform): 47 | """ 48 | For each supported platform, build a universal wheel. 49 | """ 50 | # Clean up dangling directories if previous run was interrupted. 51 | utility.clean_up(directory=TARGET_DIRECTORY) 52 | 53 | if not platform or platform not in SUPPORTED_PLATFORMS: 54 | print('{} is not supported.'.format(platform)) 55 | print('Please provide a valid platform flag.' + 56 | '[win32, win_amd64, manylinux1_x86_64, macosx_10_11_intel]') 57 | sys.exit(1) 58 | 59 | copy_file_path = SUPPORTED_PLATFORMS[platform] 60 | 61 | print('Sqltoolsservice archive found at {}'.format(copy_file_path)) 62 | if copy_file_path.endswith('tar.gz'): 63 | compressed_file = tarfile.open(name=copy_file_path, mode='r:gz') 64 | elif copy_file_path.endswith('.zip'): 65 | compressed_file = zipfile.ZipFile(copy_file_path) 66 | 67 | if not os.path.exists(TARGET_DIRECTORY): 68 | os.makedirs(TARGET_DIRECTORY) 69 | 70 | print(u'Bin placing sqltoolsservice for this platform: {}.'.format(platform)) 71 | print(u'Extracting files from {}'.format(copy_file_path)) 72 | compressed_file.extractall(TARGET_DIRECTORY) 73 | 74 | 75 | def clean_up_sqltoolsservice(): 76 | utility.clean_up(directory=TARGET_DIRECTORY) 77 | -------------------------------------------------------------------------------- /tests/test_completion_refresher.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | from mock import Mock, patch 4 | from mssqlcli.completion_refresher import CompletionRefresher 5 | 6 | class CompletionRefresherTests(unittest.TestCase): 7 | 8 | @staticmethod 9 | def test_ctor(): 10 | """ 11 | Refresher object should contain a few handlers 12 | :param refresher: 13 | :return: 14 | """ 15 | refresher = CompletionRefresher() 16 | assert len(refresher.refreshers) > 0 17 | actual_handlers = set(refresher.refreshers.keys()) 18 | expected_handlers = set(['databases', 'schemas', 'tables', 'types', 'views']) 19 | assert expected_handlers == actual_handlers 20 | 21 | @staticmethod 22 | def test_refresh_called_once(): 23 | """ 24 | 25 | :param refresher: 26 | :return: 27 | """ 28 | callbacks = Mock() 29 | mssqlcliclient = Mock() 30 | refresher = CompletionRefresher() 31 | 32 | with patch.object(refresher, '_bg_refresh') as bg_refresh: 33 | actual = refresher.refresh(mssqlcliclient, callbacks) 34 | time.sleep(1) # Wait for the thread to work. 35 | assert len(actual) == 1 36 | assert len(actual[0]) == 4 37 | assert actual[0][3] == 'Auto-completion refresh started in the background.' 38 | bg_refresh.assert_called_with(mssqlcliclient, callbacks, None, None) 39 | 40 | @staticmethod 41 | def test_refresh_called_twice(): 42 | """ 43 | If refresh is called a second time, it should be restarted 44 | :param refresher: 45 | :return: 46 | """ 47 | callbacks = Mock() 48 | mssqlcliclient = Mock() 49 | refresher = CompletionRefresher() 50 | 51 | def bg_refresh_mock(*_): 52 | time.sleep(3) # seconds 53 | 54 | refresher._bg_refresh = bg_refresh_mock #pylint: disable=protected-access 55 | 56 | actual1 = refresher.refresh(mssqlcliclient, callbacks) 57 | time.sleep(1) # Wait for the thread to work. 58 | assert len(actual1) == 1 59 | assert len(actual1[0]) == 4 60 | assert actual1[0][3] == 'Auto-completion refresh started in the background.' 61 | 62 | actual2 = refresher.refresh(mssqlcliclient, callbacks) 63 | time.sleep(1) # Wait for the thread to work. 64 | assert len(actual2) == 1 65 | assert len(actual2[0]) == 4 66 | assert actual2[0][3] == 'Auto-completion refresh restarted.' 67 | 68 | @staticmethod 69 | def test_refresh_with_callbacks(): 70 | """ 71 | Callbacks must be called 72 | :param refresher: 73 | """ 74 | class MssqlCliClientMock: #pylint: disable=too-few-public-methods 75 | @staticmethod 76 | def connect_to_database(): 77 | return 'connectionservicetest', [] 78 | 79 | mssqlcliclient = MssqlCliClientMock() 80 | callbacks = [Mock()] 81 | refresher = CompletionRefresher() 82 | 83 | # Set refreshers to 0: we're not testing refresh logic here 84 | refresher.refreshers = {} 85 | refresher.refresh(mssqlcliclient, callbacks) 86 | time.sleep(1) # Wait for the thread to work. 87 | assert callbacks[0].call_count == 1 88 | -------------------------------------------------------------------------------- /utility.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from subprocess import check_call, CalledProcessError 3 | import os 4 | import platform 5 | import shlex 6 | import shutil 7 | import sys 8 | import string 9 | import random 10 | 11 | ROOT_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..')) 12 | 13 | MSSQLCLI_DIST_DIRECTORY = os.path.abspath( 14 | os.path.join(os.path.abspath(__file__), '..', 'dist')) 15 | 16 | MSSQLCLI_BUILD_DIRECTORY = os.path.abspath( 17 | os.path.join(os.path.abspath(__file__), '..', 'build')) 18 | 19 | MSSQLCLI_RPM_DIRECTORY = os.path.abspath( 20 | os.path.join(os.path.abspath(__file__), '..', '..', 'rpm_output') 21 | ) 22 | 23 | MSSQLCLI_DEB_DIRECTORY = os.path.abspath( 24 | os.path.join(os.path.abspath(__file__), '..', '..', 'debian_output') 25 | ) 26 | 27 | 28 | def exec_command(command, directory, continue_on_error=True): 29 | """ 30 | Execute command. 31 | """ 32 | try: 33 | command_split = [token.strip('"') for token in shlex.split(command, posix=False)] 34 | # The logic above is used to preserve multiple token arguments with pytest. It is 35 | # specifically needed when calling "not unstable" for running all tests not marked 36 | # as unstable. 37 | 38 | check_call(command_split, cwd=directory) 39 | except CalledProcessError as err: 40 | # Continue execution in scenarios where we may be bulk command execution. 41 | print(err, file=sys.stderr) 42 | if not continue_on_error: 43 | sys.exit(1) 44 | else: 45 | pass 46 | 47 | 48 | def clean_up_egg_info_sub_directories(directory): 49 | for f in os.listdir(directory): 50 | if f.endswith(".egg-info"): 51 | clean_up(os.path.join(directory, f)) 52 | 53 | 54 | def clean_up(directory): 55 | """ 56 | Delete directory. 57 | """ 58 | try: 59 | shutil.rmtree(directory) 60 | except OSError: 61 | # Ignored, directory may not exist which is fine. 62 | pass 63 | 64 | 65 | def get_current_platform(): 66 | """ 67 | Get current platform name. 68 | """ 69 | system = platform.system() 70 | arch = platform.architecture()[0] 71 | 72 | run_time_id = None 73 | if system == 'Windows': 74 | if arch == '32bit': 75 | run_time_id = 'win32' 76 | elif arch == '64bit': 77 | run_time_id = 'win_amd64' 78 | elif system == 'Darwin': 79 | run_time_id = 'macosx_10_11_intel' 80 | elif system == 'Linux': 81 | run_time_id = 'manylinux1_x86_64' 82 | 83 | return run_time_id 84 | 85 | 86 | def copy_current_platform_mssqltoolsservice(): 87 | """ 88 | Copy the necessary mssqltoolsservice binaries for the current platform if supported. 89 | """ 90 | # pylint: disable=import-outside-toplevel 91 | import mssqlcli.mssqltoolsservice.externals as mssqltoolsservice 92 | 93 | current_platform = get_current_platform() 94 | if current_platform: 95 | mssqltoolsservice.copy_sqltoolsservice(current_platform) 96 | else: 97 | print("This platform: {} does not support mssqltoolsservice.".format(platform.system())) 98 | 99 | 100 | def random_str(size=12, chars=string.ascii_uppercase + string.digits): 101 | return ''.join(random.choice(chars) for x in range(size)) 102 | -------------------------------------------------------------------------------- /mssqlcli/packages/special/main.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=too-many-arguments 2 | 3 | import logging 4 | from collections import namedtuple 5 | 6 | from mssqlcli.packages.special import export 7 | 8 | logger = logging.getLogger('mssqlcli.special') 9 | 10 | NO_QUERY = 0 11 | PARSED_QUERY = 1 12 | RAW_QUERY = 2 13 | 14 | SpecialCommand = namedtuple('SpecialCommand', 15 | ['handler', 'command', 'shortcut', 'description', 'arg_type', 'hidden', 16 | 'case_sensitive']) 17 | 18 | COMMANDS = {} 19 | 20 | 21 | @export 22 | class CommandNotFound(Exception): 23 | pass 24 | 25 | 26 | @export 27 | def parse_special_command(sql): 28 | command, _, arg = sql.partition(' ') 29 | verbose = '+' in command 30 | command = command.strip().replace('+', '') 31 | return (command, verbose, arg.strip()) 32 | 33 | 34 | @export 35 | def special_command(command, shortcut, description, arg_type=PARSED_QUERY, 36 | hidden=False, case_sensitive=False, aliases=()): 37 | def wrapper(wrapped): 38 | register_special_command(wrapped, command, shortcut, description, 39 | arg_type, hidden, case_sensitive, aliases) 40 | return wrapped 41 | return wrapper 42 | 43 | 44 | @export 45 | def register_special_command(handler, command, shortcut, description, 46 | arg_type=PARSED_QUERY, hidden=False, case_sensitive=False, aliases=()): 47 | cmd = command.lower() if not case_sensitive else command 48 | COMMANDS[cmd] = SpecialCommand(handler, command, shortcut, description, 49 | arg_type, hidden, case_sensitive) 50 | for alias in aliases: 51 | cmd = alias.lower() if not case_sensitive else alias 52 | COMMANDS[cmd] = SpecialCommand(handler, command, shortcut, description, 53 | arg_type, case_sensitive=case_sensitive, 54 | hidden=True) 55 | 56 | 57 | @export 58 | def execute(mssqlcliclient, sql): 59 | """Execute a special command and return the results. If the special command 60 | is not supported a KeyError will be raised. 61 | """ 62 | command, verbose, pattern = parse_special_command(sql) 63 | 64 | if (command not in COMMANDS) and (command.lower() not in COMMANDS): 65 | raise CommandNotFound('Command not found: %s' % command) 66 | 67 | try: 68 | special_cmd = COMMANDS[command] 69 | except KeyError: 70 | special_cmd = COMMANDS[command.lower()] 71 | if special_cmd.case_sensitive: 72 | raise CommandNotFound('Command not found: %s' % command) 73 | 74 | logger.debug(u'Executing special command %s with argument %s.', command, pattern) 75 | 76 | if special_cmd.arg_type == NO_QUERY: 77 | return special_cmd.handler() 78 | if special_cmd.arg_type == PARSED_QUERY: 79 | return special_cmd.handler(mssqlcliclient=mssqlcliclient, pattern=pattern, verbose=verbose) 80 | if special_cmd.arg_type == RAW_QUERY: 81 | return special_cmd.handler(mssqlcliclient=mssqlcliclient, query=sql) 82 | 83 | return None 84 | 85 | 86 | @special_command('help', '\\?', 'Show this help.', arg_type=NO_QUERY, aliases=('\\?', '?')) 87 | def show_help(): # All the parameters are ignored. 88 | headers = ['Command', 'Shortcut', 'Description'] 89 | result = [] 90 | 91 | for _, value in sorted(COMMANDS.items()): 92 | if not value.hidden: 93 | result.append((value.command, value.shortcut, value.description)) 94 | return [(result, headers, None, None, False)] 95 | -------------------------------------------------------------------------------- /tests/test_fuzzy_completion.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import unittest 3 | import mssqlcli.mssqlcompleter as mssqlcompleter 4 | 5 | class FuzzyCompletionTests(unittest.TestCase): 6 | 7 | @staticmethod 8 | def getCompleter(): 9 | return mssqlcompleter.MssqlCompleter() 10 | 11 | def test_ranking_ignores_identifier_quotes(self): 12 | """When calculating result rank, identifier quotes should be ignored. 13 | 14 | The result ranking algorithm ignores identifier quotes. Without this 15 | correction, the match "user", which Postgres requires to be quoted 16 | since it is also a reserved word, would incorrectly fall below the 17 | match user_action because the literal quotation marks in "user" 18 | alter the position of the match. 19 | 20 | This test checks that the fuzzy ranking algorithm correctly ignores 21 | quotation marks when computing match ranks. 22 | 23 | """ 24 | completer = self.getCompleter() 25 | text = 'user' 26 | collection = ['user_action', '"user"'] 27 | matches = completer.find_matches(text, collection) 28 | assert len(matches) == 2 29 | 30 | def test_ranking_based_on_shortest_match(self): 31 | """Fuzzy result rank should be based on shortest match. 32 | 33 | Result ranking in fuzzy searching is partially based on the length 34 | of matches: shorter matches are considered more relevant than 35 | longer ones. When searching for the text 'user', the length 36 | component of the match 'user_group' could be either 4 ('user') or 37 | 7 ('user_gr'). 38 | 39 | This test checks that the fuzzy ranking algorithm uses the shorter 40 | match when calculating result rank. 41 | 42 | """ 43 | completer = self.getCompleter() 44 | text = 'user' 45 | collection = ['api_user', 'user_group'] 46 | matches = completer.find_matches(text, collection) 47 | assert matches[1].priority > matches[0].priority 48 | 49 | def test_should_break_ties_using_lexical_order(self): 50 | """Fuzzy result rank should use lexical order to break ties. 51 | 52 | When fuzzy matching, if multiple matches have the same match length and 53 | start position, present them in lexical (rather than arbitrary) order. For 54 | example, if we have tables 'user', 'user_action', and 'user_group', a 55 | search for the text 'user' should present these tables in this order. 56 | 57 | The input collections to this test are out of order; each run checks that 58 | the search text 'user' results in the input tables being reordered 59 | lexically. 60 | 61 | """ 62 | collections = [ 63 | ['user_action', 'user'], 64 | ['user_group', 'user'], 65 | ['user_group', 'user_action'], 66 | ] 67 | completer = self.getCompleter() 68 | text = 'user' 69 | for collection in collections: 70 | matches = completer.find_matches(text, collection) 71 | assert matches[1].priority > matches[0].priority 72 | 73 | def test_matching_should_be_case_insensitive(self): 74 | """Fuzzy matching should keep matches even if letter casing doesn't match. 75 | 76 | This test checks that variations of the text which have different casing 77 | are still matched. 78 | """ 79 | completer = self.getCompleter() 80 | text = 'foo' 81 | collection = ['Foo', 'FOO', 'fOO'] 82 | matches = completer.find_matches(text, collection) 83 | assert len(matches) == 3 84 | -------------------------------------------------------------------------------- /tests/parseutils/test_ctes.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlparse import parse 3 | from mssqlcli.packages.parseutils.ctes import ( 4 | token_start_pos, extract_ctes, 5 | extract_column_names as _extract_column_names) 6 | 7 | 8 | def extract_column_names(sql): 9 | p = parse(sql)[0] 10 | return _extract_column_names(p) 11 | 12 | 13 | def test_token_str_pos(): 14 | sql = 'SELECT * FROM xxx' 15 | p = parse(sql)[0] 16 | idx = p.token_index(p.tokens[-1]) 17 | assert token_start_pos(p.tokens, idx) == len('SELECT * FROM ') 18 | 19 | sql = 'SELECT * FROM \nxxx' 20 | p = parse(sql)[0] 21 | idx = p.token_index(p.tokens[-1]) 22 | assert token_start_pos(p.tokens, idx) == len('SELECT * FROM \n') 23 | 24 | 25 | def test_single_column_name_extraction(): 26 | sql = 'SELECT abc FROM xxx' 27 | assert extract_column_names(sql) == ('abc',) 28 | 29 | 30 | def test_aliased_single_column_name_extraction(): 31 | sql = 'SELECT abc def FROM xxx' 32 | assert extract_column_names(sql) == ('def',) 33 | 34 | 35 | def test_aliased_expression_name_extraction(): 36 | sql = 'SELECT 99 abc FROM xxx' 37 | assert extract_column_names(sql) == ('abc',) 38 | 39 | 40 | def test_multiple_column_name_extraction(): 41 | sql = 'SELECT abc, def FROM xxx' 42 | assert extract_column_names(sql) == ('abc', 'def') 43 | 44 | 45 | def test_missing_column_name_handled_gracefully(): 46 | sql = 'SELECT abc, 99 FROM xxx' 47 | assert extract_column_names(sql) == ('abc',) 48 | 49 | sql = 'SELECT abc, 99, def FROM xxx' 50 | assert extract_column_names(sql) == ('abc', 'def') 51 | 52 | 53 | def test_aliased_multiple_column_name_extraction(): 54 | sql = 'SELECT abc def, ghi jkl FROM xxx' 55 | assert extract_column_names(sql) == ('def', 'jkl') 56 | 57 | 58 | def test_table_qualified_column_name_extraction(): 59 | sql = 'SELECT abc.def, ghi.jkl FROM xxx' 60 | assert extract_column_names(sql) == ('def', 'jkl') 61 | 62 | 63 | @pytest.mark.parametrize('sql', [ 64 | 'INSERT INTO foo (x, y, z) VALUES (5, 6, 7) RETURNING x, y', 65 | 'DELETE FROM foo WHERE x > y RETURNING x, y', 66 | 'UPDATE foo SET x = 9 RETURNING x, y', 67 | ]) 68 | def test_extract_column_names_from_returning_clause(sql): 69 | assert extract_column_names(sql) == ('x', 'y') 70 | 71 | 72 | def test_simple_cte_extraction(): 73 | sql = 'WITH a AS (SELECT abc FROM xxx) SELECT * FROM a' 74 | start_pos = len('WITH a AS ') 75 | stop_pos = len('WITH a AS (SELECT abc FROM xxx)') 76 | ctes, remainder = extract_ctes(sql) 77 | 78 | assert tuple(ctes) == (('a', ('abc',), start_pos, stop_pos),) 79 | assert remainder.strip() == 'SELECT * FROM a' 80 | 81 | 82 | def test_cte_extraction_around_comments(): 83 | sql = '''--blah blah blah 84 | WITH a AS (SELECT abc def FROM x) 85 | SELECT * FROM a''' 86 | start_pos = len('''--blah blah blah 87 | WITH a AS ''') 88 | stop_pos = len('''--blah blah blah 89 | WITH a AS (SELECT abc def FROM x)''') 90 | 91 | ctes, remainder = extract_ctes(sql) 92 | assert tuple(ctes) == (('a', ('def',), start_pos, stop_pos),) 93 | assert remainder.strip() == 'SELECT * FROM a' 94 | 95 | 96 | def test_multiple_cte_extraction(): 97 | sql = '''WITH 98 | x AS (SELECT abc, def FROM x), 99 | y AS (SELECT ghi, jkl FROM y) 100 | SELECT * FROM a, b''' 101 | 102 | start1 = len('''WITH 103 | x AS ''') 104 | 105 | stop1 = len('''WITH 106 | x AS (SELECT abc, def FROM x)''') 107 | 108 | start2 = len('''WITH 109 | x AS (SELECT abc, def FROM x), 110 | y AS ''') 111 | 112 | stop2 = len('''WITH 113 | x AS (SELECT abc, def FROM x), 114 | y AS (SELECT ghi, jkl FROM y)''') 115 | 116 | ctes, remainder = extract_ctes(sql) 117 | assert tuple(ctes) == ( 118 | ('x', ('abc', 'def'), start1, stop1), 119 | ('y', ('ghi', 'jkl'), start2, stop2)) 120 | -------------------------------------------------------------------------------- /mssqlcli/main.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from __future__ import print_function 3 | 4 | import getpass 5 | import io 6 | import os 7 | import sys 8 | from builtins import input 9 | import click 10 | import six 11 | from mssqlcli.config import config_location 12 | from mssqlcli.__init__ import __version__ 13 | from mssqlcli.mssqlclioptionsparser import create_parser 14 | import mssqlcli.telemetry as telemetry_session 15 | 16 | click.disable_unicode_literals_warning = True 17 | 18 | 19 | MSSQLCLI_TELEMETRY_PROMPT = """ 20 | Telemetry 21 | --------- 22 | By default, mssql-cli collects usage data in order to improve your experience. 23 | The data is anonymous and does not include commandline argument values. 24 | The data is collected by Microsoft. 25 | 26 | Disable telemetry collection by setting environment variable MSSQL_CLI_TELEMETRY_OPTOUT to 'True' or '1'. 27 | 28 | Microsoft Privacy statement: https://go.microsoft.com/fwlink/?LinkId=521839 29 | """ 30 | 31 | 32 | def run_cli_with(options): 33 | 34 | if create_config_dir_for_first_use(): 35 | display_telemetry_message() 36 | 37 | display_version_message(options) 38 | 39 | configure_and_update_options(options) 40 | 41 | # Importing MssqlCli creates a config dir by default. 42 | # Moved import here so we can create the config dir for first use prior. 43 | # pylint: disable=import-outside-toplevel 44 | from mssqlcli.mssql_cli import MssqlCli 45 | 46 | # set interactive mode to false if -Q or -i is specified 47 | if options.query or options.input_file: 48 | options.interactive_mode = False 49 | 50 | mssqlcli = MssqlCli(options) 51 | try: 52 | mssqlcli.connect_to_database() 53 | telemetry_session.set_server_information(mssqlcli.mssqlcliclient_main) 54 | 55 | if mssqlcli.interactive_mode: 56 | mssqlcli.run() 57 | else: 58 | text = options.query 59 | if options.input_file: 60 | # get query text from input file 61 | try: 62 | if six.PY2: 63 | with io.open(options.input_file, 'r', encoding='utf-8') as f: 64 | text = f.read() 65 | else: 66 | with open(options.input_file, 'r', encoding='utf-8') as f: 67 | text = f.read() 68 | except OSError as e: 69 | click.secho(str(e), err=True, fg='red') 70 | sys.exit(1) 71 | mssqlcli.execute_query(text) 72 | finally: 73 | mssqlcli.shutdown() 74 | 75 | 76 | def configure_and_update_options(options): 77 | if options.dac_connection and options.server and not \ 78 | options.server.lower().startswith("admin:"): 79 | options.server = "admin:" + options.server 80 | 81 | if not options.integrated_auth: 82 | if not options.username: 83 | options.username = input(u'Username (press enter for sa):') or u'sa' 84 | if not options.password: 85 | pw = getpass.getpass() 86 | if pw is not None: 87 | pw = pw.replace('\r', '').replace('\n', '') 88 | options.password = pw 89 | 90 | 91 | def create_config_dir_for_first_use(): 92 | config_dir = os.path.dirname(config_location()) 93 | if not os.path.exists(config_dir): 94 | os.makedirs(config_dir) 95 | return True 96 | 97 | return False 98 | 99 | 100 | def display_version_message(options): 101 | if options.version: 102 | print('Version:', __version__) 103 | sys.exit(0) 104 | 105 | 106 | def display_telemetry_message(): 107 | print(MSSQLCLI_TELEMETRY_PROMPT) 108 | 109 | 110 | def main(): 111 | try: 112 | telemetry_session.start() 113 | mssqlcli_options_parser = create_parser() 114 | mssqlcli_options = mssqlcli_options_parser.parse_args(sys.argv[1:]) 115 | run_cli_with(mssqlcli_options) 116 | finally: 117 | # Upload telemetry async in a separate process. 118 | telemetry_session.conclude() 119 | 120 | 121 | if __name__ == "__main__": 122 | main() 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI](https://badge.fury.io/py/mssql-cli.svg)](https://pypi.python.org/pypi/mssql-cli) 2 | [![Python 2.7|3.4|3.5|3.6|3.7|3.8](https://img.shields.io/badge/python-2.7 | 3.4 | 3.5 | 3.6 | 3.7 | 3.8-blue.svg)](https://github.com/dbcli/mssql-cli) 3 | 4 | # mssql-cli 5 | 6 | 7 | 8 | > **DEPRECATION NOTICE** mssql-cli is on the path to deprecation, and will be fully replaced by the new [go-sqlcmd](https://learn.microsoft.com/sql/tools/sqlcmd/go-sqlcmd-utility) utility once it becomes generally available. We are actively in development for the new sqlcmd, and would love to hear feedback on it [here](https://github.com/microsoft/go-sqlcmd/issues)! 9 | 10 | [**mssql-cli**](https://github.com/dbcli/mssql-cli) is an interactive command line query tool for SQL Server. This open source tool works cross-platform and proud to be a part of the [dbcli](https://github.com/dbcli) community. 11 | 12 | ![mssql-cli Autocomplete](https://github.com/dbcli/mssql-cli/raw/main/screenshots/mssql-cli-autocomplete.gif) 13 | 14 | mssql-cli supports a rich interactive command line experience, with features such as: 15 | - **Auto-completion**: fewer keystrokes needed to complete complicated queries. 16 | - **Syntax highlighting**: highlights T-SQL keywords. 17 | - **Query history**: easily complete an auto-suggested query that was previously executed. 18 | - **Configuration file support**: customize the mssql-cli experience for your needs. 19 | - **Multi-line queries**: execute multiple queries at once using the multi-line edit mode. 20 | - **Non-interactive support**: execute a query without jumping into the interactive experience. 21 | 22 | ## Quick Start 23 | Read the section below to quickly get started with mssql-cli. Consult the [usage guide](https://github.com/dbcli/mssql-cli/tree/main/doc/usage_guide.md) for a deeper dive into mssql-cli features. 24 | 25 | ### Install mssql-cli 26 | Platform-specific installation instructions are below: 27 | | [Windows](https://github.com/dbcli/mssql-cli/blob/main/doc/installation/windows.md#windows-installation) (preview) | [macOS](https://github.com/dbcli/mssql-cli/blob/main/doc/installation/macos.md#macos-installation) | [Linux](https://github.com/dbcli/mssql-cli/blob/main/doc/installation/linux.md) | 28 | | - | - | - | 29 | 30 | Visit the [installation reference guide](https://github.com/dbcli/mssql-cli/tree/main/doc/installation) to view all supported releases and downloads. 31 | 32 | #### Install with Linux Package Manager 33 | Follow the [Linux installation instructions]('https://github.com/dbcli/mssql-cli/blob/main/doc/installation/linux.md') to install mssql-cli using `apt-get`, `yum`, and other Linux package managers. 34 | 35 | #### Install with pip 36 | ```sh 37 | python -m pip install mssql-cli 38 | ``` 39 | Please refer to the [pip installation docs](https://github.com/dbcli/mssql-cli/blob/main/doc/installation/pip.md) for more platform-specific information. 40 | 41 | ### Connect to Server 42 | Complete the command below to connect to your server: 43 | ```sh 44 | mssql-cli -S -d -U -P 45 | ``` 46 | 47 | ### Exit mssql-cli 48 | Press **Ctrl+D** or type `quit`. 49 | 50 | ### Show Options 51 | For general help content, pass in the `-h` parameter: 52 | ```sh 53 | mssql-cli --help 54 | ``` 55 | 56 | ### Usage Docs 57 | Please refer to the [usage guide](https://github.com/dbcli/mssql-cli/tree/main/doc/usage_guide.md) for details on options and example usage. If you are having any issues using mssql-cli, please see the [troubleshooting guide](https://github.com/dbcli/mssql-cli/blob/main/doc/troubleshooting_guide.md). 58 | 59 | ## Telemetry 60 | The mssql-cli tool includes a telemetry feature. Please refer to the [telemetry guide](https://github.com/dbcli/mssql-cli/tree/main/doc/telemetry_guide.md) for more information. 61 | 62 | ## Contributing 63 | If you would like to contribute to the project, please refer to the [development guide](https://github.com/dbcli/mssql-cli/tree/main/doc/development_guide.md). 64 | 65 | ## Contact Us 66 | If you encounter any bugs or would like to leave a feature request, please file an issue in the 67 | [**Issues**](https://github.com/dbcli/mssql-cli/issues) section of our GitHub repo. 68 | 69 | ## Code of Conduct 70 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact 71 | opencode@microsoft.com with any additional questions or comments. 72 | 73 | ## License 74 | mssql-cli is licensed under the [BSD-3 license](https://github.com/dbcli/mssql-cli/blob/main/LICENSE.txt). 75 | -------------------------------------------------------------------------------- /mssqlcli/jsonrpc/contracts/connectionservice.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=too-many-instance-attributes 2 | # pylint: disable=too-few-public-methods 3 | 4 | from mssqlcli.jsonrpc.contracts import Request 5 | 6 | 7 | class ConnectionRequest(Request): 8 | """ 9 | SqlToolsService Connection request. 10 | """ 11 | 12 | def __init__(self, request_id, owner_uri, json_rpc_client, parameters): 13 | super(ConnectionRequest, self).__init__(request_id, owner_uri, json_rpc_client, 14 | ConnectionParams(parameters), 15 | u'connection/connect', 16 | ConnectionCompleteEvent) 17 | 18 | @classmethod 19 | def response_error(cls, error): 20 | return ConnectionCompleteEvent({ 21 | u'params': { 22 | u'ownerUri': cls.owner_uri, 23 | u'connectionId': None, 24 | u'messages': str(error), 25 | u'errorMessage': u'Connection request encountered an exception', 26 | u'errorNumber': None 27 | } 28 | }) 29 | 30 | @staticmethod 31 | def decode_response(response): 32 | """ 33 | Decode response dictionary into a Connection parameter type. 34 | """ 35 | 36 | if u'result' in response: 37 | return ConnectionResponse(response) 38 | 39 | if 'method' in response and response['method'] == 'connection/complete': 40 | return ConnectionCompleteEvent(response) 41 | 42 | # Could not decode return st 43 | return response 44 | 45 | 46 | class ConnectionDetails: 47 | """ 48 | Connection details params. 49 | """ 50 | 51 | def __init__(self, parameters): 52 | # Required params 53 | self.ServerName = parameters[u'ServerName'] 54 | self.DatabaseName = parameters[u'DatabaseName'] 55 | self.UserName = parameters[u'UserName'] 56 | self.Password = parameters[u'Password'] 57 | self.AuthenticationType = parameters[u'AuthenticationType'] 58 | # Optional Params 59 | if u'Encrypt' in parameters: 60 | self.Encrypt = parameters[u'Encrypt'] 61 | if u'TrustServerCertificate' in parameters: 62 | self.TrustServerCertificate = parameters[u'TrustServerCertificate'] 63 | if u'ConnectTimeout' in parameters: 64 | self.ConnectTimeout = parameters[u'ConnectTimeout'] 65 | if u'ApplicationIntent' in parameters: 66 | self.ApplicationIntent = parameters[u'ApplicationIntent'] 67 | if u'MultiSubnetFailover' in parameters: 68 | self.MultiSubnetFailover = parameters[u'MultiSubnetFailover'] 69 | if u'PacketSize' in parameters: 70 | self.PacketSize = parameters[u'PacketSize'] 71 | 72 | def format(self): 73 | return vars(self) 74 | 75 | 76 | class ConnectionParams: 77 | def __init__(self, parameters): 78 | self.owner_uri = parameters[u'OwnerUri'] 79 | self.connection_details = ConnectionDetails(parameters) 80 | 81 | def format(self): 82 | return {u'OwnerUri': self.owner_uri, 83 | u'Connection': self.connection_details.format()} 84 | 85 | 86 | # 87 | # The Connection Events. 88 | # 89 | 90 | class ConnectionCompleteEvent: 91 | def __init__(self, params): 92 | inner_params = params[u'params'] 93 | self.owner_uri = inner_params[u'ownerUri'] 94 | self.connection_id = inner_params[u'connectionId'] 95 | self.messages = inner_params[u'messages'] 96 | self.error_message = inner_params[u'errorMessage'] 97 | self.error_number = inner_params[u'errorNumber'] 98 | # server information. 99 | if u'serverInfo' in inner_params and inner_params[u'serverInfo']: 100 | self.is_cloud = inner_params[u'serverInfo'][u'isCloud'] if \ 101 | u'isCloud' in inner_params[u'serverInfo'] else False 102 | self.server_version = inner_params[u'serverInfo'][u'serverVersion'] if \ 103 | u'serverVersion' in inner_params[u'serverInfo'] else None 104 | self.server_edition = inner_params[u'serverInfo'][u'serverEdition'] if \ 105 | u'serverEdition' in inner_params[u'serverInfo'] else None 106 | 107 | if u'connectionSummary' in inner_params and inner_params[u'connectionSummary']: 108 | self.connected_database = inner_params[u'connectionSummary'][u'databaseName'] 109 | 110 | 111 | class ConnectionResponse: 112 | def __init__(self, params): 113 | self.result = params[u'result'] 114 | self.request_id = params[u'id'] 115 | -------------------------------------------------------------------------------- /doc/troubleshooting_guide.md: -------------------------------------------------------------------------------- 1 | Troubleshooting Guide 2 | ======================================== 3 | ## Table of Contents 4 | 1. [Installation Issues](#Installation_Issues) 5 | 2. [Usage Issues](#Usage_Issues) 6 | 3. [Reporting Issues](#Reporting_Issues) 7 | 8 | ## Installation Issues: 9 | 10 | If you're having installation issues, please check the below known issues and workarounds. If you're having a different issue, please check the [issues](https://github.com/dbcli/mssql-cli/issues) page to see if the issue has already been reported. If you don't see your issue there, filing a new issue would be appreciated. 11 | 12 | ### Error: No module named mssqlcli 13 | If the installation was successful and this error message is encountered, this may be caused by different versions of python in the environment. 14 | i.e Used python 3.6 to install mssql-cli, but PATH has python 2.7 so it uses the python 2.7 interpreter which has no visibility to packages installed into python 3.6. 15 | 16 | The workaround to prevent this is to use a virtual environment, which will provide a isolated environment that is tied to a specific python version. 17 | More information can be found at: 18 | 19 | - [Virtual Environment Info](virtual_environment_info.md) 20 | 21 | - [Development guide](development_guide.md#Environment_Setup) 22 | 23 | ### Error: Could not find version that satisfies the requirement mssql-cli 24 | If you see the above error running `pip install mssql-cli`, this means the pip version used is out-of-date. Please upgrade your pip installation for your python platform and OS distribution. 25 | 26 | ### Error: System.DllNotFoundException: Unable to load DLL 'System.Security.Cryptography.Native': The specified module could not be found. 27 | If you encounter this error on MacOS, this means you need the latest version of OpenSSL. Later version of macOS (Sierra, and High Sierra) should not have this issue. To install OpenSSL use the following commands: 28 | ```shell 29 | $ brew update 30 | $ brew install openssl 31 | $ mkdir -p /usr/local/lib 32 | $ ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/ 33 | $ ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/ 34 | ``` 35 | 36 | ### Error: libunwind.so: cannot open shared object file 37 | If you encounter the below error running mssql-cli, this means the libunwind package is not installed. Please install the libunwind package for your Linux distribution. 38 | ```shell 39 | Failed to load /usr/local/lib/python2.7/dist-packages/mssqltoolsservice/bin/libcoreclr.so, error 40 | libunwind.so.8: cannot open shared object file: No such file or directory 41 | ``` 42 | 43 | ### Error: Failed to initialize CoreCLR, HRESULT: 0x80131500 44 | If you encounter the below error running mssql-cli, this means the libicu package is not installed. Please install the libicu package for your Linux distribution. 45 | ```shell 46 | Failed to initialize CoreCLR, HRESULT: 0x80131500 47 | ``` 48 | 49 | ## Usage Issues: 50 | 51 | ### Unknown glyph fills up prompt: 52 | If you encounter the display below, it is a Windows 10 issue that can pop up on the command prompt or powershell prompt. 53 | The current workaround for this issue is to change the font of the prompt to Consolas. 54 | ![alt text](https://github.com/dbcli/mssql-cli/blob/master/screenshots/mssql-cli-display-issue.png "mssql-cli display issue") 55 | 56 | ### Default pager outputs results separate from main console: 57 | If the output of your query looks like the display below, it is due to the default setting on LESS. 58 | ![alt text](https://github.com/dbcli/mssql-cli/blob/master/screenshots/mssql-cli-less-pager.png "mssql-cli pager issue") 59 | To fix this issue, uncomment 60 | the pager option in your config file located here 61 | * Unix/Linux 62 | ``` 63 | ~/.config/mssqlcli/config 64 | 65 | # Default pager. 66 | # By default 'PAGER' environment variable is used 67 | pager = less -SRXF 68 | ``` 69 | * In Windows: 70 | ``` 71 | %USERPROFILE%\AppData\Local\dbcli\mssqlcli\config 72 | 73 | # Default pager. 74 | # By default 'PAGER' environment variable is used 75 | pager = less -SRXF 76 | ``` 77 | 78 | ## Reporting Issues: 79 | If the issue you are encountering is not listed above nor filed on our github, please file a issue here [issues](#https://github.com/dbcli/mssql-cli/issues) with the following information listed below and any other additional symptoms of your issue. 80 | 81 | * Command used to install mssql-cli 82 | * Python version and location 83 | ``` 84 | $ python -V 85 | $ where python 86 | ``` 87 | * Pip Version and location 88 | ``` 89 | $ pip -V 90 | $ where pip 91 | ``` 92 | * OS Distribution and version 93 | * Target Server and Database version and edition 94 | * Python environment variables set 95 | -------------------------------------------------------------------------------- /tests/test_interactive_mode.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import pytest 4 | import utility 5 | from mssqlcli.util import is_command_valid 6 | from mssqltestutils import ( 7 | create_mssql_cli, 8 | create_mssql_cli_config, 9 | shutdown, 10 | test_queries, 11 | get_file_contents, 12 | get_io_paths 13 | ) 14 | 15 | class TestInteractiveMode: 16 | """ 17 | Fixture used at class-level. 18 | """ 19 | @staticmethod 20 | @pytest.fixture(scope='class') 21 | def mssqlcli(): 22 | """ 23 | Pytest fixture which returns interactive mssql-cli instance 24 | and cleans up on teardown. 25 | """ 26 | mssqlcli = create_mssql_cli(interactive_mode=True) 27 | yield mssqlcli 28 | shutdown(mssqlcli) 29 | 30 | 31 | class TestInteractiveModeQueries(TestInteractiveMode): 32 | @staticmethod 33 | @pytest.mark.parametrize("query_str, test_file", test_queries) 34 | @pytest.mark.timeout(60) 35 | def test_query(query_str, test_file, mssqlcli): 36 | _, file_baseline = get_io_paths(test_file) 37 | output_baseline = get_file_contents(file_baseline) 38 | output_query = '\n'.join(mssqlcli.execute_query(query_str)).replace('\r', '') 39 | assert output_query == output_baseline 40 | 41 | 42 | class TestInteractiveModeInvalidRuns: 43 | @pytest.mark.timeout(60) 44 | def test_noninteractive_run(self): 45 | ''' 46 | Test that calling run throws an exception only when interactive_mode is false 47 | ''' 48 | self.invalid_run(interactive_mode=False) 49 | 50 | def test_interactive_output(self): 51 | ''' 52 | Test run with interactive mode enabled with output file, which should return ValueError 53 | ''' 54 | self.invalid_run(output_file='will-fail.txt') 55 | 56 | @staticmethod 57 | def test_output_with_interactive_change(): 58 | ''' 59 | Fails on run after interactive mode has been toggled 60 | ''' 61 | mssqlcli = create_mssql_cli(interactive_mode=False, output_file='will-fail-eventually.txt') 62 | mssqlcli.interactive_mode = True 63 | try: 64 | mssqlcli.run() 65 | assert False 66 | except ValueError: 67 | assert True 68 | finally: 69 | shutdown(mssqlcli) 70 | 71 | @staticmethod 72 | @pytest.mark.timeout(60) 73 | def invalid_run(**options): 74 | ''' 75 | Tests mssql-cli runs with invalid combination of properities set 76 | ''' 77 | mssqlcli = None 78 | try: 79 | mssqlcli = create_mssql_cli(**options) 80 | mssqlcli.run() 81 | assert False 82 | except ValueError: 83 | assert True 84 | finally: 85 | if mssqlcli is not None: 86 | shutdown(mssqlcli) 87 | 88 | 89 | class TestInteractiveModePager(TestInteractiveMode): 90 | """ 91 | Test default pager setting. 92 | """ 93 | 94 | @staticmethod 95 | @pytest.mark.timeout(60) 96 | def test_pager_environ(mssqlcli): 97 | """ 98 | Defaults to environment variable with no config value for pager 99 | """ 100 | os.environ['PAGER'] = 'testing environ value' 101 | 102 | config = create_mssql_cli_config() 103 | 104 | # remove config pager value if exists 105 | if 'pager' in config['main'].keys(): 106 | config['main'].pop('pager') 107 | 108 | assert mssqlcli.set_default_pager(config) == 'testing environ value' 109 | 110 | os.environ['PAGER'] = 'less -SRXF' 111 | assert mssqlcli.set_default_pager(config) == 'less -SRXF' 112 | 113 | @staticmethod 114 | @pytest.mark.timeout(60) 115 | def test_pager_config(mssqlcli): 116 | """ 117 | Defaults to config value over environment variable 118 | """ 119 | os.environ['PAGER'] = 'less -SRXF' 120 | config_value = 'testing config value' 121 | 122 | config = create_mssql_cli_config() 123 | config['main']['pager'] = config_value 124 | assert mssqlcli.set_default_pager(config) == config_value 125 | 126 | 127 | class TestInteractiveModeRun: 128 | """ 129 | Tests the executable. 130 | """ 131 | 132 | @staticmethod 133 | @pytest.mark.timeout(60) 134 | def test_valid_command(): 135 | """ 136 | Checks valid command by running mssql-cli executable in repo 137 | """ 138 | if sys.platform == 'win32': 139 | exe_name = 'mssql-cli.bat' 140 | else: 141 | exe_name = 'mssql-cli' 142 | 143 | assert is_command_valid([os.path.join(utility.ROOT_DIR, exe_name), '--version']) 144 | 145 | @staticmethod 146 | @pytest.mark.timeout(60) 147 | def test_invalid_command(): 148 | assert not is_command_valid(None) 149 | assert not is_command_valid('') 150 | -------------------------------------------------------------------------------- /release_scripts/Packages.Microsoft/publish.sh: -------------------------------------------------------------------------------- 1 | # Publishes deb and rpm packages to Packages.Microsoft repo 2 | 3 | # Validate initial argument is used 4 | if [[ -z "$1" ]] 5 | then 6 | echo "First argument should be path to local repo." 7 | exit 1 8 | fi 9 | 10 | # Validate second argument specifices either prod or testing for publishing channel. 11 | # Each dictionary specifies key-value paris for repo URLs with distribution names. 12 | # Both URL and distribution are needed to query for a repo ID, which is later used 13 | # for publishing. 14 | if [[ ${2,,} = 'prod' ]]; then 15 | declare -A repos=( \ 16 | ["microsoft-ubuntu-xenial-prod"]="xenial" \ 17 | ["microsoft-ubuntu-bionic-prod"]="bionic" \ 18 | ["microsoft-debian-jessie-prod"]="jessie" \ 19 | ["microsoft-debian-stretch-prod"]="stretch" \ 20 | ["microsoft-rhel7.0-prod"]="trusty" \ 21 | ["microsoft-rhel7.1-prod"]="trusty" \ 22 | ["microsoft-rhel7.2-prod"]="trusty" \ 23 | ["microsoft-rhel7.3-prod"]="trusty" \ 24 | ["microsoft-rhel7.4-prod"]="trusty" \ 25 | ["microsoft-centos7-prod"]="trusty" \ 26 | ["microsoft-centos8-prod"]="centos" \ 27 | ) 28 | elif [[ ${2,,} = 'testing' ]]; then 29 | declare -A repos=( \ 30 | ["microsoft-ubuntu-trusty-prod"]="testing" \ 31 | ["microsoft-ubuntu-xenial-prod"]="testing" \ 32 | ["microsoft-debian-jessie-prod"]="testing" \ 33 | ["microsoft-debian-stretch-prod"]="testing" \ 34 | ["microsoft-opensuse42.3-testing-prod"]="testing" \ 35 | ["microsoft-rhel7.0-testing-prod"]="testing" \ 36 | ["microsoft-rhel7.1-testing-prod"]="testing" \ 37 | ["microsoft-rhel7.3-testing-prod"]="testing" \ 38 | ["microsoft-rhel7.2-testing-prod"]="testing" \ 39 | ["microsoft-rhel7.4-testing-prod"]="testing" \ 40 | ["microsoft-rhel8.0-testing-prod"]="testing" \ 41 | ["microsoft-centos7-testing-prod"]="testing" \ 42 | ["microsoft-centos8-testing-prod"]="testing" \ 43 | ["microsoft-opensuse42.2-testing-prod"]="testing" \ 44 | ["microsoft-sles12-testing-prod"]="testing" \ 45 | ["microsoft-ubuntu-bionic-prod"]="testing" \ 46 | ["microsoft-ubuntu-cosmic-prod"]="testing" \ 47 | ["microsoft-ubuntu-disco-prod"]="testing" \ 48 | ["microsoft-debian-buster-prod"]="testing" \ 49 | ["microsoft-debian-jessie-prod"]="testing" \ 50 | ["microsoft-debian-stretch-prod"]="testing" \ 51 | ) 52 | else 53 | echo "Second argument should specify 'prod' or 'testing' for repository distribution type." 54 | exit 1 55 | fi 56 | 57 | # Confirm if third optional '--upload' argument is used 58 | if [[ ${3,,} = '--upload' ]]; then 59 | is_upload='True' 60 | 61 | # download latest stable deb and rpm packages 62 | wget https://mssqlcli.blob.core.windows.net/daily/deb/mssql-cli_1.1.0-1_all.deb --directory-prefix=/root/ 63 | wget https://mssqlcli.blob.core.windows.net/daily/rpm/mssql-cli-1.1.0-1.el7.x86_64.rpm --directory-prefix=/root/ 64 | else 65 | is_upload='False' 66 | fi 67 | 68 | local_repo=$1 69 | deb_pkg=/root/mssql-cli_1.1.0-1_all.deb 70 | rpm_pkg=/root/mssql-cli-1.1.0-1.el7.x86_64.rpm 71 | 72 | # build url_match_string to get repo ID's from above URL names 73 | url_match_str="" 74 | for repo_url in ${!repos[@]}; do 75 | # get key from url string 76 | distribution="${repos[$repo_url]}" 77 | 78 | if [[ $url_match_str == "" ]]; then 79 | # only append 'or' to string if not first index 80 | url_match_str="(.url==\"${repo_url}\" and .distribution==\"${distribution}\")" 81 | else 82 | url_match_str="${url_match_str} or (.url==\"${repo_url}\" and .distribution==\"${distribution}\")" 83 | fi 84 | done 85 | 86 | # construct string for select statement in jq command, 87 | # filters by repo URL and distribution type 88 | select_stmnt="select(${url_match_str})" 89 | 90 | # query for list of IDs from repo urls 91 | list_repo_id=$(repoclient repo list | jq -r ".[] | ${select_stmnt} | @base64") 92 | for repo_data in $(echo "${list_repo_id}"); do 93 | _jq() { 94 | # decode JSON 95 | echo ${repo_data} | base64 --decode | jq -r ${1} 96 | } 97 | repo_id=$(_jq '.id') 98 | repo_type=$(_jq '.type') 99 | 100 | # publish deb or rpm package 101 | # '-r' specifies the destination repository (by ID) 102 | # 'break' exits loop if something failed with command 103 | if [[ $repo_type == "apt" ]]; then 104 | command="repoclient package add $deb_pkg -r $repo_id" 105 | elif [[ $repo_type == "yum" ]]; then 106 | command="repoclient package add $rpm_pkg -r $repo_id" 107 | else 108 | echo "No package published for $(_jq '.url')" 109 | break 110 | fi 111 | 112 | echo $command 113 | if [[ $is_upload == "True" ]]; then 114 | # publish package 115 | echo "Publishing $repo_type for $repo_url..." 116 | eval "$command || break" 117 | printf "\n" 118 | fi 119 | done 120 | -------------------------------------------------------------------------------- /tests/features/environment.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from __future__ import print_function 4 | 5 | import os 6 | import sys 7 | import db_utils as dbutils 8 | import fixture_utils as fixutils 9 | import pexpect 10 | 11 | from steps.wrappers import run_cli, wait_prompt 12 | 13 | 14 | def before_all(context): 15 | """ 16 | Set env parameters. 17 | """ 18 | os.environ['LINES'] = "100" 19 | os.environ['COLUMNS'] = "100" 20 | os.environ['PAGER'] = 'cat' 21 | os.environ['EDITOR'] = 'ex' 22 | 23 | context.package_root = os.path.abspath( 24 | os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) 25 | 26 | os.environ["COVERAGE_PROCESS_START"] = os.path.join(context.package_root, 27 | '.coveragerc') 28 | 29 | context.exit_sent = False 30 | 31 | vi = '_'.join([str(x) for x in sys.version_info[:3]]) 32 | db_name = context.config.userdata.get('pg_test_db', 'pgcli_behave_tests') 33 | db_name_full = '{0}_{1}'.format(db_name, vi) 34 | 35 | # Store get params from config. 36 | context.conf = { 37 | 'host': context.config.userdata.get( 38 | 'pg_test_host', 39 | os.getenv('PGHOST', 'localhost') 40 | ), 41 | 'user': context.config.userdata.get( 42 | 'pg_test_user', 43 | os.getenv('PGUSER', 'postgres') 44 | ), 45 | 'pass': context.config.userdata.get( 46 | 'pg_test_pass', 47 | os.getenv('PGPASSWORD', None) 48 | ), 49 | 'port': context.config.userdata.get( 50 | 'pg_test_port', 51 | os.getenv('PGPORT', '5432') 52 | ), 53 | 'cli_command': ( 54 | context.config.userdata.get('pg_cli_command', None) or 55 | '{python} -c "{startup}"'.format( 56 | python=sys.executable, 57 | startup='; '.join([ 58 | "import coverage", 59 | "coverage.process_startup()", 60 | "import pgcli.main", 61 | "pgcli.main.cli()"]))), 62 | 'dbname': db_name_full, 63 | 'dbname_tmp': db_name_full + '_tmp', 64 | 'vi': vi, 65 | 'pager_boundary': '---boundary---', 66 | } 67 | os.environ['PAGER'] = "{0} {1} {2}".format( 68 | sys.executable, 69 | os.path.join(context.package_root, "tests/features/wrappager.py"), 70 | context.conf['pager_boundary']) 71 | 72 | # Store old env vars. 73 | context.pgenv = { 74 | 'PGDATABASE': os.environ.get('PGDATABASE', None), 75 | 'PGUSER': os.environ.get('PGUSER', None), 76 | 'PGHOST': os.environ.get('PGHOST', None), 77 | 'PGPASSWORD': os.environ.get('PGPASSWORD', None), 78 | 'PGPORT': os.environ.get('PGPORT', None), 79 | } 80 | 81 | # Set new env vars. 82 | os.environ['PGDATABASE'] = context.conf['dbname'] 83 | os.environ['PGUSER'] = context.conf['user'] 84 | os.environ['PGHOST'] = context.conf['host'] 85 | os.environ['PGPORT'] = context.conf['port'] 86 | 87 | if context.conf['pass']: 88 | os.environ['PGPASSWORD'] = context.conf['pass'] 89 | else: 90 | if 'PGPASSWORD' in os.environ: 91 | del os.environ['PGPASSWORD'] 92 | 93 | context.cn = dbutils.create_db(context.conf['host'], context.conf['user'], 94 | context.conf['pass'], context.conf['dbname'], 95 | context.conf['port']) 96 | 97 | context.fixture_data = fixutils.read_fixture_files() 98 | 99 | 100 | def after_all(context): 101 | """ 102 | Unset env parameters. 103 | """ 104 | dbutils.close_cn(context.cn) 105 | dbutils.drop_db(context.conf['host'], context.conf['user'], 106 | context.conf['pass'], context.conf['dbname'], 107 | context.conf['port']) 108 | 109 | # Restore env vars. 110 | for k, v in context.pgenv.items(): 111 | if k in os.environ and v is None: 112 | del os.environ[k] 113 | elif v: 114 | os.environ[k] = v 115 | 116 | 117 | def before_step(context, _): 118 | context.atprompt = False 119 | 120 | 121 | def before_scenario(context, _): 122 | run_cli(context) 123 | wait_prompt(context) 124 | 125 | 126 | def after_scenario(context, _): 127 | """Cleans up after each test complete.""" 128 | 129 | if hasattr(context, 'cli') and not context.exit_sent: 130 | # Quit nicely. 131 | if not context.atprompt: 132 | dbname = context.currentdb 133 | context.cli.expect_exact( 134 | '{0}> '.format(dbname), 135 | timeout=5 136 | ) 137 | context.cli.sendcontrol('d') 138 | context.cli.expect_exact(pexpect.EOF, timeout=5) 139 | 140 | # TODO: uncomment to debug a failure 141 | # def after_step(context, step): 142 | # if step.status == "failed": 143 | # import ipdb; ipdb.set_trace() 144 | -------------------------------------------------------------------------------- /mssqlcli/sqltoolsclient.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import subprocess 3 | import io 4 | import time 5 | import uuid 6 | 7 | import mssqlcli.mssqltoolsservice as mssqltoolsservice 8 | import mssqlcli.jsonrpc.jsonrpcclient as json_rpc_client 9 | import mssqlcli.jsonrpc.contracts.connectionservice as connection 10 | import mssqlcli.jsonrpc.contracts.queryexecutestringservice as query 11 | from .config import config_location 12 | 13 | logger = logging.getLogger(u'mssqlcli.sqltoolsclient') 14 | 15 | 16 | class SqlToolsClient: 17 | """ 18 | Create sql tools service requests. 19 | """ 20 | CONNECTION_REQUEST = u'connection_request' 21 | QUERY_EXECUTE_STRING_REQUEST = u'query_execute_string_request' 22 | QUERY_SUBSET_REQUEST = u'query_subset_request' 23 | 24 | def __init__(self, input_stream=None, output_stream=None, enable_logging=False): 25 | """ 26 | Initializes the sql tools client. 27 | Input and output streams for JsonRpcClient are taken as optional params, 28 | Else a SqlToolsService process is started and its stdin and stdout is used. 29 | """ 30 | self.current_id = uuid.uuid4().int 31 | self.tools_service_process = None 32 | 33 | sqltoolsservice_args = [mssqltoolsservice.get_executable_path()] 34 | 35 | if enable_logging: 36 | sqltoolsservice_args.append('--enable-logging') 37 | sqltoolsservice_args.append('--log-dir') 38 | sqltoolsservice_args.append(config_location()) 39 | 40 | if input_stream and output_stream: 41 | self.json_rpc_client = json_rpc_client.JsonRpcClient( 42 | input_stream, output_stream) 43 | else: 44 | self.tools_service_process = subprocess.Popen( 45 | sqltoolsservice_args, 46 | bufsize=0, 47 | stdin=subprocess.PIPE, 48 | stdout=subprocess.PIPE) 49 | 50 | self.json_rpc_client = json_rpc_client.JsonRpcClient( 51 | self.tools_service_process.stdin, 52 | io.open( 53 | self.tools_service_process.stdout.fileno(), 54 | u'rb', 55 | buffering=0, 56 | closefd=False)) 57 | 58 | logger.info(u'SqlToolsService process id: %s', self.tools_service_process.pid) 59 | 60 | self.json_rpc_client.start() 61 | logger.info(u'Sql Tools Client Initialized') 62 | 63 | def create_request(self, request_type, parameters, owner_uri): 64 | """ 65 | Create request of request type passed in. 66 | """ 67 | request = None 68 | self.current_id = str(uuid.uuid4().int) 69 | 70 | if request_type == u'connection_request': 71 | logger.info(u'SqlToolsClient connection request Id %s and owner Uri %s', 72 | self.current_id, owner_uri) 73 | request = connection.ConnectionRequest(self.current_id, owner_uri, self.json_rpc_client, 74 | parameters) 75 | 76 | if request_type == u'query_execute_string_request': 77 | logger.info(u'SqlToolsClient execute string request Id %s and owner Uri %s', 78 | self.current_id, owner_uri) 79 | request = query.QueryExecuteStringRequest(self.current_id, owner_uri, 80 | self.json_rpc_client, parameters) 81 | 82 | if request_type == u'query_subset_request': 83 | logger.info(u'SqlToolsClient subset request Id %s and owner Uri %s', 84 | self.current_id, owner_uri) 85 | request = query.QuerySubsetRequest(self.current_id, owner_uri, 86 | self.json_rpc_client, parameters) 87 | 88 | return request 89 | 90 | def shutdown(self): 91 | self.json_rpc_client.shutdown() 92 | 93 | if self.tools_service_process: 94 | logger.info(u'Shutting down Sql Tools Client Process Id: %s', 95 | self.tools_service_process.pid) 96 | 97 | try: 98 | # kill process and give it time to die 99 | self.tools_service_process.kill() 100 | time.sleep(0.1) 101 | except OSError: 102 | # catches 'No such process' error from Python 2 103 | pass 104 | finally: 105 | # Close the stdout file handle or else we would get a resource warning (found via 106 | # pytest). This must be closed after the process is killed, otherwise we would block 107 | # because the process is using it's stdout. 108 | self.tools_service_process.stdout.close() 109 | # None value indicates process has not terminated. 110 | if not self.tools_service_process.poll(): 111 | logger.warning( 112 | u'\nSql Tools Service process was not shut down properly.') 113 | -------------------------------------------------------------------------------- /mssqlcli/packages/parseutils/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import re 3 | import sqlparse 4 | from sqlparse.sql import Identifier 5 | from sqlparse.tokens import Token, Error 6 | 7 | cleanup_regex = { 8 | # This matches only alphanumerics and underscores. 9 | 'alphanum_underscore': re.compile(r'(\w+)$'), 10 | # This matches everything except spaces, parens, colon, and comma 11 | 'many_punctuations': re.compile(r'([^():,\s]+)$'), 12 | # This matches everything except spaces, parens, colon, comma, and period 13 | 'most_punctuations': re.compile(r'([^\.():,\s]+)$'), 14 | # This matches everything except a space. 15 | 'all_punctuations': re.compile(r'([^\s]+)$'), 16 | } 17 | 18 | 19 | def last_word(text, include='alphanum_underscore'): 20 | r""" 21 | Find the last word in a sentence. 22 | 23 | >>> last_word('abc') 24 | 'abc' 25 | >>> last_word(' abc') 26 | 'abc' 27 | >>> last_word('') 28 | '' 29 | >>> last_word(' ') 30 | '' 31 | >>> last_word('abc ') 32 | '' 33 | >>> last_word('abc def') 34 | 'def' 35 | >>> last_word('abc def ') 36 | '' 37 | >>> last_word('abc def;') 38 | '' 39 | >>> last_word('bac $def') 40 | 'def' 41 | >>> last_word('bac $def', include='most_punctuations') 42 | '$def' 43 | >>> last_word('bac \def', include='most_punctuations') 44 | '\\\\def' 45 | >>> last_word('bac \def;', include='most_punctuations') 46 | '\\\\def;' 47 | >>> last_word('bac::def', include='most_punctuations') 48 | 'def' 49 | >>> last_word('"foo*bar', include='most_punctuations') 50 | '"foo*bar' 51 | """ 52 | 53 | if not text: # Empty string 54 | return '' 55 | 56 | if text[-1].isspace(): 57 | return '' 58 | regex = cleanup_regex[include] 59 | matches = regex.search(text) 60 | if matches: 61 | return matches.group(0) 62 | return '' 63 | 64 | 65 | def find_prev_keyword(sql, n_skip=0): 66 | """ Find the last sql keyword in an SQL statement 67 | 68 | Returns the value of the last keyword, and the text of the query with 69 | everything after the last keyword stripped 70 | """ 71 | if not sql.strip(): 72 | return None, '' 73 | 74 | parsed = sqlparse.parse(sql)[0] 75 | flattened = list(parsed.flatten()) 76 | flattened = flattened[:len(flattened) - n_skip] 77 | 78 | logical_operators = ('AND', 'OR', 'NOT', 'BETWEEN') 79 | 80 | for t in reversed(flattened): 81 | if t.value == '(' or (t.is_keyword and 82 | (t.value.upper() not in logical_operators) 83 | ): 84 | # Find the location of token t in the original parsed statement 85 | # We can't use parsed.token_index(t) because t may be a child token 86 | # inside a TokenList, in which case token_index thows an error 87 | # Minimal example: 88 | # p = sqlparse.parse('select * from foo where bar') 89 | # t = list(p.flatten())[-3] # The "Where" token 90 | # p.token_index(t) # Throws ValueError: not in list 91 | idx = flattened.index(t) 92 | 93 | # Combine the string values of all tokens in the original list 94 | # up to and including the target keyword token t, to produce a 95 | # query string with everything after the keyword token removed 96 | text = ''.join(tok.value for tok in flattened[:idx + 1]) 97 | return t, text 98 | 99 | return None, '' 100 | 101 | 102 | # Postgresql dollar quote signs look like `$$` or `$tag$` 103 | dollar_quote_regex = re.compile(r'^\$[^$]*\$$') 104 | 105 | 106 | def is_open_quote(sql): 107 | """Returns true if the query contains an unclosed quote""" 108 | 109 | # parsed can contain one or more semi-colon separated commands 110 | parsed = sqlparse.parse(sql) 111 | return any(_parsed_is_open_quote(p) for p in parsed) 112 | 113 | 114 | def _parsed_is_open_quote(parsed): 115 | # Look for unmatched single quotes, or unmatched dollar sign quotes 116 | return any(tok.match(Token.Error, ("'", '"', "$")) for tok in parsed.flatten()) 117 | 118 | 119 | def parse_partial_identifier(word): 120 | """Attempt to parse a (partially typed) word as an identifier 121 | 122 | word may include a schema qualification, like `schema_name.partial_name` 123 | or `schema_name.` There may also be unclosed quotation marks, like 124 | `"schema`, or `schema."partial_name` 125 | 126 | :param word: string representing a (partially complete) identifier 127 | :return: sqlparse.sql.Identifier, or None 128 | """ 129 | 130 | p = sqlparse.parse(word)[0] 131 | n_tok = len(p.tokens) 132 | if n_tok == 1 and isinstance(p.tokens[0], Identifier): 133 | return p.tokens[0] 134 | if p.token_next_by(m=(Error, '"'))[1]: 135 | # An unmatched double quote, e.g. '"foo', 'foo."', or 'foo."bar' 136 | # Close the double quote, then reparse 137 | return parse_partial_identifier(word + '"') 138 | return None 139 | -------------------------------------------------------------------------------- /mssqlcli/completion_refresher.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | import logging 3 | import threading 4 | import mssqlcli.decorators as decorators 5 | from .mssqlcompleter import MssqlCompleter 6 | 7 | logger = logging.getLogger(u'mssqlcli.completion_refresher') 8 | 9 | 10 | class CompletionRefresher: 11 | 12 | refreshers = OrderedDict() 13 | 14 | def __init__(self): 15 | self._completer_thread = None 16 | self._restart_refresh = threading.Event() 17 | 18 | def refresh(self, mssqcliclient, callbacks, history=None, 19 | settings=None): 20 | """ 21 | Creates a MssqlCompleter object and populates it with the relevant 22 | completion suggestions in a background thread. 23 | 24 | mssqlcliclient - used to extract the credentials to connect 25 | to the database. 26 | settings - dict of settings for completer object 27 | callbacks - A function or a list of functions to call after the thread 28 | has completed the refresh. The newly created completion 29 | object will be passed in as an argument to each callback. 30 | """ 31 | if self.is_refreshing(): 32 | self._restart_refresh.set() 33 | return [(None, None, None, 'Auto-completion refresh restarted.')] 34 | 35 | self._completer_thread = threading.Thread( 36 | target=self._bg_refresh, 37 | args=(mssqcliclient, callbacks, history, settings), 38 | name='completion_refresh') 39 | self._completer_thread.setDaemon(True) 40 | self._completer_thread.start() 41 | return [(None, None, None, 42 | 'Auto-completion refresh started in the background.')] 43 | 44 | def is_refreshing(self): 45 | return self._completer_thread and self._completer_thread.is_alive() 46 | 47 | def _bg_refresh(self, mssqlcliclient, callbacks, history=None, 48 | settings=None): 49 | settings = settings or {} 50 | completer = MssqlCompleter(smart_completion=True, settings=settings) 51 | 52 | executor = mssqlcliclient 53 | owner_uri, error_messages = executor.connect_to_database() 54 | 55 | if not owner_uri: 56 | # If we were unable to connect, do not break the experience for the user. 57 | # Return nothing, smart completion can maintain the keywords and functions completions. 58 | logger.error(u'Completion refresher connection failure.'.join(error_messages)) 59 | return 60 | # If callbacks is a single function then push it into a list. 61 | if callable(callbacks): 62 | callbacks = [callbacks] 63 | 64 | while 1: 65 | for refresh in self.refreshers.values(): 66 | refresh(completer, executor) 67 | if self._restart_refresh.is_set(): 68 | self._restart_refresh.clear() 69 | break 70 | else: 71 | # Break out of while loop if the for loop finishes natually 72 | # without hitting the break statement. 73 | break 74 | 75 | # Start over the refresh from the beginning if the for loop hit the 76 | # break statement. 77 | continue 78 | 79 | # Load history into mssqlcompleter so it can learn user preferences 80 | n_recent = 100 81 | if history: 82 | for recent in history.get_strings()[-n_recent:]: 83 | completer.extend_query_history(recent, is_init=True) 84 | 85 | for callback in callbacks: 86 | callback(completer) 87 | 88 | 89 | def refresher(name, refreshers=CompletionRefresher.refreshers): 90 | """Decorator to populate the dictionary of refreshers with the current 91 | function. 92 | """ 93 | def wrapper(wrapped): 94 | refreshers[name] = wrapped 95 | return wrapped 96 | return wrapper 97 | 98 | 99 | @refresher('schemas') 100 | @decorators.suppress_all_exceptions() 101 | def refresh_schemas(completer, mssqlcliclient): 102 | completer.extend_schemas(mssqlcliclient.get_schemas()) 103 | 104 | 105 | @refresher('tables') 106 | @decorators.suppress_all_exceptions() 107 | def refresh_tables(completer, mssqlcliclient): 108 | completer.extend_relations(mssqlcliclient.get_tables(), kind='tables') 109 | completer.extend_columns(mssqlcliclient.get_table_columns(), kind='tables') 110 | completer.extend_foreignkeys(mssqlcliclient.get_foreign_keys()) 111 | 112 | 113 | @refresher('views') 114 | @decorators.suppress_all_exceptions() 115 | def refresh_views(completer, mssqlcliclient): 116 | completer.extend_relations(mssqlcliclient.get_views(), kind='views') 117 | completer.extend_columns(mssqlcliclient.get_view_columns(), kind='views') 118 | 119 | 120 | @refresher('databases') 121 | @decorators.suppress_all_exceptions() 122 | def refresh_databases(completer, mssqlcliclient): 123 | completer.extend_database_names(mssqlcliclient.get_databases()) 124 | 125 | 126 | @refresher('types') 127 | @decorators.suppress_all_exceptions() 128 | def refresh_types(completer, mssqlcliclient): 129 | completer.extend_datatypes(mssqlcliclient.get_user_defined_types()) 130 | -------------------------------------------------------------------------------- /mssqlcli/mssqlstyle.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import logging 4 | 5 | import pygments.styles 6 | from pygments.token import string_to_tokentype, Token 7 | from pygments.style import Style as PygmentsStyle 8 | from pygments.util import ClassNotFound 9 | from prompt_toolkit.styles.pygments import style_from_pygments_cls 10 | from prompt_toolkit.styles import merge_styles, Style 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | # map Pygments tokens (ptk 1.0) to class names (ptk 2.0). 15 | TOKEN_TO_PROMPT_STYLE = { 16 | Token.Menu.Completions.Completion.Current: 'completion-menu.completion.current', 17 | Token.Menu.Completions.Completion: 'completion-menu.completion', 18 | Token.Menu.Completions.Meta.Current: 'completion-menu.meta.completion.current', 19 | Token.Menu.Completions.Meta: 'completion-menu.meta.completion', 20 | Token.Menu.Completions.MultiColumnMeta: 'completion-menu.multi-column-meta', 21 | Token.Menu.Completions.ProgressButton: 'scrollbar.arrow', # best guess 22 | Token.Menu.Completions.ProgressBar: 'scrollbar', # best guess 23 | Token.SelectedText: 'selected', 24 | Token.SearchMatch: 'search', 25 | Token.SearchMatch.Current: 'search.current', 26 | Token.Toolbar: 'bottom-toolbar', 27 | Token.Toolbar.Off: 'bottom-toolbar.off', 28 | Token.Toolbar.On: 'bottom-toolbar.on', 29 | Token.Toolbar.Search: 'search-toolbar', 30 | Token.Toolbar.Search.Text: 'search-toolbar.text', 31 | Token.Toolbar.System: 'system-toolbar', 32 | Token.Toolbar.Arg: 'arg-toolbar', 33 | Token.Toolbar.Arg.Text: 'arg-toolbar.text', 34 | Token.Toolbar.Transaction.Valid: 'bottom-toolbar.transaction.valid', 35 | Token.Toolbar.Transaction.Failed: 'bottom-toolbar.transaction.failed', 36 | Token.Output.Header: 'output.header', 37 | Token.Output.OddRow: 'output.odd-row', 38 | Token.Output.EvenRow: 'output.even-row', 39 | } 40 | 41 | # reverse dict for cli_helpers, because they still expect Pygments tokens. 42 | PROMPT_STYLE_TO_TOKEN = { 43 | v: k for k, v in TOKEN_TO_PROMPT_STYLE.items() 44 | } 45 | 46 | 47 | def parse_pygments_style(token_name, style_object, style_dict): 48 | """Parse token type and style string. 49 | :param token_name: str name of Pygments token. Example: "Token.String" 50 | :param style_object: pygments.style.Style instance to use as base 51 | :param style_dict: dict of token names and their styles, customized to this cli 52 | """ 53 | token_type = string_to_tokentype(token_name) 54 | try: 55 | other_token_type = string_to_tokentype(style_dict[token_name]) 56 | return token_type, style_object.styles[other_token_type] 57 | except AttributeError: 58 | return token_type, style_dict[token_name] 59 | 60 | 61 | def style_factory(name, cli_style): 62 | try: 63 | style = pygments.styles.get_style_by_name(name) 64 | except ClassNotFound: 65 | style = pygments.styles.get_style_by_name('native') 66 | 67 | prompt_styles = [] 68 | # prompt-toolkit used pygments tokens for styling before, switched to style 69 | # names in 2.0. Convert old token types to new style names, for backwards compatibility. 70 | for token in cli_style: 71 | if token.startswith('Token.'): 72 | # treat as pygments token (1.0) 73 | token_type, style_value = parse_pygments_style( 74 | token, style, cli_style) 75 | if token_type in TOKEN_TO_PROMPT_STYLE: 76 | prompt_style = TOKEN_TO_PROMPT_STYLE[token_type] 77 | prompt_styles.append((prompt_style, style_value)) 78 | else: 79 | # we don't want to support tokens anymore 80 | logger.error('Unhandled style / class name: %s', token) 81 | else: 82 | # treat as prompt style name (2.0). See default style names here: 83 | # https://github.com/jonathanslenders/python-prompt-toolkit/blob/master/prompt_toolkit/styles/defaults.py 84 | prompt_styles.append((token, cli_style[token])) 85 | 86 | override_style = Style([('bottom-toolbar', 'noreverse')]) 87 | return merge_styles([ 88 | style_from_pygments_cls(style), 89 | override_style, 90 | Style(prompt_styles) 91 | ]) 92 | 93 | 94 | def style_factory_output(name, cli_style): 95 | try: 96 | style = pygments.styles.get_style_by_name(name).styles 97 | except ClassNotFound: 98 | style = pygments.styles.get_style_by_name('native').styles 99 | 100 | for token in cli_style: 101 | if token.startswith('Token.'): 102 | token_type, style_value = parse_pygments_style( 103 | token, style, cli_style) 104 | style.update({token_type: style_value}) 105 | elif token in PROMPT_STYLE_TO_TOKEN: 106 | token_type = PROMPT_STYLE_TO_TOKEN[token] 107 | style.update({token_type: cli_style[token]}) 108 | else: 109 | # TODO: cli helpers will have to switch to ptk.Style 110 | logger.error('Unhandled style / class name: %s', token) 111 | 112 | class OutputStyle(PygmentsStyle): # pylint: disable=too-few-public-methods 113 | default_style = "" 114 | styles = style 115 | 116 | return OutputStyle 117 | -------------------------------------------------------------------------------- /doc/installation/linux.md: -------------------------------------------------------------------------------- 1 | # Linux Installation 2 | Stable installations of mssql-cli on Linux are hosted in the [Microsoft Linux Software Repository](https://docs.microsoft.com/en-us/windows-server/administration/linux-package-repository-for-microsoft-software). mssql-cli supports the following Linux distributions: 3 | 4 | [**Debian-based**](#Debian-based) 5 | - [**Ubuntu**](#Ubuntu) 6 | - [Ubuntu 16.04 (Xenial)](#ubuntu-1604-Xenial) 7 | - [Ubuntu 18.04 (Bionic)](#ubuntu-1804-Bionic) 8 | - [**Debian**](#Debian) 9 | - [Debian 8](#debian-8) 10 | - [Debian 9](#debian-9) 11 | 12 | [**RPM-based**](#RPM-based) 13 | - [**CentOS**](#CentOS) 14 | - [CentOS 7](#centos-7) 15 | - [CentOS 8](#centos-8) 16 | - [**Red Hat Enterprise Linux**](#Red-Hat-Enterprise-Linux) 17 | - [RHEL 7](#RHEL-7) 18 | 19 | 20 | ## Debian-based 21 | 22 | ### Ubuntu 23 | 24 | #### Ubuntu 16.04 (Xenial) 25 | ```sh 26 | # Import the public repository GPG keys 27 | curl https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add - 28 | 29 | # Register the Microsoft Ubuntu repository 30 | sudo apt-add-repository https://packages.microsoft.com/ubuntu/16.04/prod 31 | 32 | # Update the list of products 33 | sudo apt-get update 34 | 35 | # Install mssql-cli 36 | sudo apt-get install mssql-cli 37 | 38 | # Install missing dependencies 39 | sudo apt-get install -f 40 | ``` 41 | 42 | #### Ubuntu 18.04 (Bionic) 43 | ```sh 44 | # Import the public repository GPG keys 45 | curl https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add - 46 | 47 | # Register the Microsoft Ubuntu repository 48 | sudo apt-add-repository https://packages.microsoft.com/ubuntu/18.04/prod 49 | 50 | # Update the list of products 51 | sudo apt-get update 52 | 53 | # Install mssql-cli 54 | sudo apt-get install mssql-cli 55 | 56 | # Install missing dependencies 57 | sudo apt-get install -f 58 | ``` 59 | 60 | ### Debian 61 | > `apt-transport-https` is required for importing keys. If not installed, call `sudo apt-get install curl apt-transport-https`. 62 | 63 | #### Debian 8 64 | ```sh 65 | # Import the public repository GPG keys 66 | wget -qO- https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add - 67 | 68 | # Register the Microsoft Product feed 69 | echo "deb [arch=amd64] https://packages.microsoft.com/debian/8/prod jessie main" | sudo tee /etc/apt/sources.list.d/mssql-cli.list 70 | 71 | # Update the list of products 72 | sudo apt-get update 73 | 74 | # Install mssql-cli 75 | sudo apt-get install mssql-cli 76 | 77 | # Install missing dependencies 78 | sudo apt-get install -f 79 | ``` 80 | 81 | #### Debian 9 82 | ```sh 83 | # Import the public repository GPG keys 84 | wget -qO- https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add - 85 | 86 | # Register the Microsoft Product feed 87 | echo "deb [arch=amd64] https://packages.microsoft.com/debian/9/prod stretch main" | sudo tee /etc/apt/sources.list.d/mssql-cli.list 88 | 89 | # Update the list of products 90 | sudo apt-get update 91 | 92 | # Install mssql-cli 93 | sudo apt-get install mssql-cli 94 | 95 | # Install missing dependencies 96 | sudo apt-get install -f 97 | ``` 98 | 99 | ### Upgrade on Ubuntu/Debian 100 | After registering the Microsoft repository once as superuser, 101 | from then on, you just need to use `sudo apt-get upgrade mssql-cli` to update it. 102 | 103 | ### Uninstall on Ubuntu/Debian 104 | To uninstall mssql-cli, call `sudo apt-get remove mssql-cli`. 105 | 106 | 107 | ## RPM-based 108 | 109 | ### CentOS 110 | 111 | #### CentOS 7 112 | > This package also works on Oracle Linux 7. 113 | 114 | ```sh 115 | # Import the public repository GPG keys 116 | sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc 117 | 118 | # Register the Microsoft product feed 119 | curl https://packages.microsoft.com/config/centos/7/prod.repo > /etc/yum.repos.d/msprod.repo 120 | 121 | # Install dependencies and mssql-cli 122 | sudo yum install libunwind 123 | sudo yum install mssql-cli 124 | ``` 125 | 126 | #### CentOS 8 127 | ```sh 128 | # Import the public repository GPG keys 129 | sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc 130 | 131 | # Register the Microsoft product feed 132 | curl https://packages.microsoft.com/config/centos/8/prod.repo > /etc/yum.repos.d/msprod.repo 133 | 134 | # Install dependencies and mssql-cli 135 | sudo yum install libunwind 136 | sudo yum install mssql-cli 137 | ``` 138 | 139 | ### Red Hat Enterprise Linux 140 | 141 | #### RHEL 7 142 | ```sh 143 | # Import the public repository GPG keys 144 | sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc 145 | 146 | # Register the Microsoft product feed 147 | curl https://packages.microsoft.com/config/rhel/7/prod.repo > /etc/yum.repos.d/msprod.repo 148 | 149 | # Install dependencies and mssql-cli 150 | sudo yum install libunwind 151 | sudo yum install mssql-cli 152 | ``` 153 | 154 | ### Upgrade on CentOS/RHEL 155 | After registering the Microsoft repository once as superuser, 156 | from then on, you just need to use `sudo yum update mssql-cli` to update it. 157 | 158 | ### Uninstall on CentOS/RHEL 159 | To uninstall mssql-cli, call `sudo yum remove mssql-cli`. 160 | -------------------------------------------------------------------------------- /doc/installation/pip.md: -------------------------------------------------------------------------------- 1 | # pip installation guide 2 | Installation via pip is supported, but will require .NET Core 2.0 dependencies to be installed prior. 3 | 4 | The following chart shows the .NET Core 2.0 dependencies on different Linux distributions that are officially supported. 5 | The pip installation steps will outline commands to execute to download and install the dependencies. 6 | 7 | | OS | Dependencies | 8 | | ------------------ | ------------ | 9 | | Ubuntu 14.04 | libunwind8, libicu52 | 10 | | Ubuntu 16.04 | libunwind8, libicu55 | 11 | | Ubuntu 17.04 | libunwind8, libicu57 | 12 | | Debian 8 (Jessie) | libunwind8, libicu52 | 13 | | Debian 9 (Stretch) | libunwind8, libicu57 | 14 | | CentOS 7
Oracle Linux 7
RHEL 7
OpenSUSE 42.2
Fedora 25 | libunwind, libcurl, libicu | 15 | | Fedora 26 | libunwind, libicu | 16 | 17 | ## Quick Start 18 | mssql-cli needs Python to run, and works with Python 2.7 and 3.6. 19 | 20 | mssql-cli is installed via pip. If you know pip, you can install mssql-cli using command 21 | ```shell 22 | $ pip install mssql-cli 23 | ``` 24 | This command may need to run as sudo if you are installing to the system site packages. mssql-cli can be 25 | installed using the --user option, which does not require sudo. 26 | ```shell 27 | $ pip install --user mssql-cli 28 | ``` 29 | 30 | If you are having installation issues, see the [troubleshooting](https://github.com/dbcli/mssql-cli/blob/master/doc/troubleshooting_guide.md) section for known issues and workarounds. 31 | 32 | 33 | ## Detailed Instructions 34 | 35 | For supported operating system specific installations, see one of the following links: 36 | 37 | * [Windows (x86/x64) 8.1, 10, Windows Server 2012+](#windows-installation) 38 | * [macOS (x64) 10.12+](#macos-installation) 39 | * [Linux (x64)](#linux-installation) 40 | * [Red Hat Enterprise Linux 7](#install-red-hat-enterprise-linux-rhel-7) 41 | * [CentOS 7](#install-centos-7) 42 | * [Fedora 25, Fedora 26](#install-fedora-25-fedora-26) 43 | * [Debian 8.7 or later versions](#install-ubuntu-debian-mint) 44 | * [Ubuntu 17.04, Ubuntu 16.04, Ubuntu 14.04](#install-ubuntu-debian-mint) 45 | * [Linux Mint 18, Linux Mint 17](#install-ubuntu-debian-mint) 46 | * [openSUSE 42.2 or later](#install-opensuse-422-or-later) 47 | * [SUSE Enterprise Linux (SLES) 12 SP2 or later](#install-suse-enterprise-linux-sles-12-sp2-or-later) 48 | 49 | # Windows Installation 50 | 51 | Python is not installed by default on Windows. The latest Python installation package can be downloaded from [here](https://www.python.org/downloads/). When installing, select the 'Add Python to PATH' option. Python must be in the PATH environment variable. 52 | 53 | Once Python is installed and in the PATH environment variable, open a command prompt, and install mssql-cli using the below command. 54 | 55 | ```shell 56 | C:\> pip install mssql-cli 57 | ``` 58 | NOTE: If Python was installed into the "Program Files" directory, you may need to open the command prompt as an administrator for the above command to succeed. 59 | 60 | # macOS Installation 61 | 62 | On macOS, Python 2.7 is generally pre-installed. You may have to upgrade pip with the following easy_install commands. 63 | 64 | ```shell 65 | $ sudo easy_install pip 66 | $ sudo pip install --upgrade pip 67 | $ sudo pip install mssql-cli --ignore-installed six 68 | ``` 69 | 70 | # Linux Installation 71 | > **Installing on Linux using `pip` is no longer recommended.** Please consult the [Linux Installation Guide](https://github.com/dbcli/mssql-cli/blob/master/doc/installation/linux.md) for installing mssql-cli with `apt-get` or `yum`. 72 | 73 | On Linux, Python 2.7 is generally pre-installed. There are two prerequisite packages to run mssql-cli on Linux: libunwind and libicu. 74 | 75 | ## Install Red Hat Enterprise Linux (RHEL) 7 76 | ```shell 77 | $ wget https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm 78 | $ sudo yum -y install ./epel-release-latest-7.noarch.rpm 79 | $ sudo yum -y install icu libunwind python-pip 80 | $ sudo pip install mssql-cli 81 | ``` 82 | 83 | ## Install CentOS 7 84 | ```shell 85 | $ sudo yum -y install epel-release 86 | $ sudo yum -y install libunwind libicu python-pip 87 | $ sudo pip install mssql-cli 88 | ``` 89 | 90 | ## Install Fedora 25, Fedora 26 91 | ```shell 92 | $ dnf upgrade 93 | $ dnf install libunwind libicu python-pip 94 | $ sudo pip install mssql-cli 95 | ``` 96 | 97 | ## Install Ubuntu / Debian / Mint 98 | 99 | ```shell 100 | $ sudo apt-get update & sudo apt-get install -y libunwind8 python-pip 101 | $ sudo apt-get install -y libicu60 2> /dev/null || sudo apt-get install -y libicu57 2> /dev/null || sudo apt-get install -y libicu55 2> /dev/null || sudo apt-get install -y libicu55 2> /dev/null || sudo apt-get install -y libicu52 102 | $ pip install --user mssql-cli 103 | ``` 104 | 105 | 106 | ### Install OpenSUSE 42.2 or later 107 | ```shell 108 | $ sudo zypper update 109 | $ sudo zypper install libunwind libicu python-pip 110 | $ sudo pip install mssql-cli 111 | ``` 112 | 113 | 114 | ### Install SUSE Enterprise Linux (SLES) 12 SP2 or later 115 | ```shell 116 | $ sudo zypper update 117 | $ sudo zypper install libunwind libicu python-pip 118 | $ sudo pip install mssql-cli 119 | ``` 120 | -------------------------------------------------------------------------------- /mssqlcli/packages/parseutils/ctes.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from collections import namedtuple 3 | from sqlparse import parse 4 | from sqlparse.tokens import Keyword, CTE, DML 5 | from sqlparse.sql import Identifier, IdentifierList, Parenthesis 6 | from .meta import TableMetadata, ColumnMetadata 7 | 8 | 9 | # TableExpression is a namedtuple representing a CTE, used internally 10 | # name: cte alias assigned in the query 11 | # columns: list of column names 12 | # start: index into the original string of the left parens starting the CTE 13 | # stop: index into the original string of the right parens ending the CTE 14 | TableExpression = namedtuple('TableExpression', 'name columns start stop') 15 | 16 | 17 | def isolate_query_ctes(full_text, text_before_cursor): 18 | """Simplify a query by converting CTEs into table metadata objects 19 | """ 20 | 21 | if not full_text or not full_text.strip(): 22 | return full_text, text_before_cursor, tuple() 23 | 24 | ctes, _ = extract_ctes(full_text) 25 | if not ctes: 26 | return full_text, text_before_cursor, () 27 | 28 | current_position = len(text_before_cursor) 29 | meta = [] 30 | 31 | for cte in ctes: 32 | if cte.start < current_position < cte.stop: 33 | # Currently editing a cte - treat its body as the current full_text 34 | text_before_cursor = full_text[cte.start:current_position] 35 | full_text = full_text[cte.start:cte.stop] 36 | return full_text, text_before_cursor, meta 37 | 38 | # Append this cte to the list of available table metadata 39 | cols = (ColumnMetadata(name, None, ()) for name in cte.columns) 40 | meta.append(TableMetadata(cte.name, cols)) 41 | 42 | # Editing past the last cte (ie the main body of the query) 43 | full_text = full_text[ctes[-1].stop:] 44 | text_before_cursor = text_before_cursor[ctes[-1].stop:current_position] 45 | 46 | return full_text, text_before_cursor, tuple(meta) 47 | 48 | 49 | def extract_ctes(sql): 50 | """ Extract constant table expresseions from a query 51 | 52 | Returns tuple (ctes, remainder_sql) 53 | 54 | ctes is a list of TableExpression namedtuples 55 | remainder_sql is the text from the original query after the CTEs have 56 | been stripped. 57 | """ 58 | 59 | p = parse(sql)[0] 60 | 61 | # Make sure the first meaningful token is "WITH" which is necessary to 62 | # define CTEs 63 | idx, tok = p.token_next(-1, skip_ws=True, skip_cm=True) 64 | if not (tok and tok.ttype == CTE): 65 | return [], sql 66 | 67 | # Get the next (meaningful) token, which should be the first CTE 68 | idx, tok = p.token_next(idx) 69 | if not tok: 70 | return ([], '') 71 | start_pos = token_start_pos(p.tokens, idx) 72 | ctes = [] 73 | 74 | if isinstance(tok, IdentifierList): 75 | # Multiple ctes 76 | for t in tok.get_identifiers(): 77 | cte_start_offset = token_start_pos(tok.tokens, tok.token_index(t)) 78 | cte = get_cte_from_token(t, start_pos + cte_start_offset) 79 | if not cte: 80 | continue 81 | ctes.append(cte) 82 | elif isinstance(tok, Identifier): 83 | # A single CTE 84 | cte = get_cte_from_token(tok, start_pos) 85 | if cte: 86 | ctes.append(cte) 87 | 88 | idx = p.token_index(tok) + 1 89 | 90 | # Collapse everything after the ctes into a remainder query 91 | remainder = u''.join(str(tok) for tok in p.tokens[idx:]) 92 | 93 | return ctes, remainder 94 | 95 | 96 | def get_cte_from_token(tok, pos0): 97 | cte_name = tok.get_real_name() 98 | if not cte_name: 99 | return None 100 | 101 | # Find the start position of the opening parens enclosing the cte body 102 | idx, parens = tok.token_next_by(Parenthesis) 103 | if not parens: 104 | return None 105 | 106 | start_pos = pos0 + token_start_pos(tok.tokens, idx) 107 | cte_len = len(str(parens)) # includes parens 108 | stop_pos = start_pos + cte_len 109 | 110 | column_names = extract_column_names(parens) 111 | 112 | return TableExpression(cte_name, column_names, start_pos, stop_pos) 113 | 114 | 115 | def extract_column_names(parsed): 116 | # Find the first DML token to check if it's a SELECT or INSERT/UPDATE/DELETE 117 | idx, tok = parsed.token_next_by(t=DML) 118 | tok_val = tok and tok.value.lower() 119 | 120 | if tok_val in ('insert', 'update', 'delete'): 121 | # Jump ahead to the RETURNING clause where the list of column names is 122 | idx, tok = parsed.token_next_by(idx, (Keyword, 'returning')) 123 | elif not tok_val == 'select': 124 | # Must be invalid CTE 125 | return () 126 | 127 | # The next token should be either a column name, or a list of column names 128 | idx, tok = parsed.token_next(idx, skip_ws=True, skip_cm=True) 129 | return tuple(t.get_name() for t in _identifiers(tok)) 130 | 131 | 132 | def token_start_pos(tokens, idx): 133 | return sum(len(str(t)) for t in tokens[:idx]) 134 | 135 | 136 | def _identifiers(tok): 137 | if isinstance(tok, IdentifierList): 138 | for t in tok.get_identifiers(): 139 | # NB: IdentifierList.get_identifiers() can return non-identifiers! 140 | if isinstance(t, Identifier): 141 | yield t 142 | elif isinstance(tok, Identifier): 143 | yield tok 144 | -------------------------------------------------------------------------------- /tests/test_naive_completion.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import unittest 3 | from prompt_toolkit.completion import Completion 4 | from prompt_toolkit.document import Document 5 | from mock import Mock 6 | import mssqlcli.mssqlcompleter as mssqlcompleter 7 | 8 | class NaiveCompletionTests(unittest.TestCase): 9 | 10 | def test_empty_string_completion(self): 11 | completer = self.get_completer() 12 | complete_event = self.get_complete_event() 13 | text = '' 14 | position = 0 15 | actual = list( 16 | completer.get_completions( 17 | Document(text=text, cursor_position=position), 18 | complete_event 19 | ) 20 | ) 21 | actual = list(filter(lambda e: e.display_meta_text == 'keyword', actual)) 22 | actual = set(map(lambda e: e.text, actual)) 23 | expected = set(completer.keywords_tree.keys()) 24 | assert actual == expected 25 | 26 | def test_select_keyword_completion(self): 27 | completer = self.get_completer() 28 | complete_event = self.get_complete_event() 29 | text = 'SEL' 30 | position = len('SEL') 31 | actual = list( 32 | completer.get_completions( 33 | Document(text=text, cursor_position=position), 34 | complete_event, 35 | ) 36 | ) 37 | expected = [Completion(text='SELECT', start_position=-3, display_meta="keyword")] 38 | assert self.equals(actual, expected) 39 | 40 | def test_function_name_completion(self): 41 | completer = self.get_completer() 42 | complete_event = self.get_complete_event() 43 | text = 'SELECT MA' 44 | position = len('SELECT MA') 45 | actual = list( 46 | completer.get_completions( 47 | Document(text=text, cursor_position=position), 48 | complete_event 49 | ) 50 | ) 51 | expected = [Completion(text='MAX', start_position=-2, display_meta="function")] 52 | assert self.equals(actual, expected) 53 | 54 | def test_column_name_completion(self): 55 | completer = self.get_completer() 56 | complete_event = self.get_complete_event() 57 | text = 'SELECT FROM users' 58 | position = len('SELECT ') 59 | actual = list( 60 | completer.get_completions( 61 | Document(text=text, cursor_position=position), 62 | complete_event 63 | ) 64 | ) 65 | actual = set(map(lambda e: e.text, actual)) 66 | expected = set(completer.keywords_tree.keys()) 67 | expected.update(completer.functions) 68 | assert actual == expected 69 | 70 | def test_paths_completion(self): 71 | completer = self.get_completer() 72 | complete_event = self.get_complete_event() 73 | text = r'\i ' 74 | position = len(text) 75 | actual = list( 76 | completer.get_completions( 77 | Document(text=text, cursor_position=position), 78 | complete_event 79 | ) 80 | ) 81 | expected = [Completion(text="setup.py", start_position=0, display_meta="")] 82 | assert self.contains(actual, expected) 83 | 84 | def test_alter_well_known_keywords_completion(self): 85 | completer = self.get_completer() 86 | complete_event = self.get_complete_event() 87 | text = 'ALTER ' 88 | position = len(text) 89 | actual = list( 90 | completer.get_completions( 91 | Document(text=text, cursor_position=position), 92 | complete_event 93 | ) 94 | ) 95 | expected = [ 96 | Completion(text="DATABASE", display_meta='keyword'), 97 | Completion(text="TABLE", display_meta='keyword') 98 | ] 99 | assert self.contains(actual, expected) 100 | not_expected = [Completion(text="CREATE", display_meta="keyword")] 101 | assert not self.contains(actual, not_expected) 102 | 103 | @staticmethod 104 | def get_completer(): 105 | return mssqlcompleter.MssqlCompleter(smart_completion=True) 106 | 107 | @staticmethod 108 | def get_complete_event(): 109 | return Mock() 110 | 111 | @staticmethod 112 | def equals(completion_list1, completion_list2): 113 | if len(completion_list1) != len(completion_list2): 114 | return False 115 | for e1 in completion_list1: 116 | theSame = None 117 | for e2 in completion_list2: 118 | if (e1.text == e2.text 119 | and e1.start_position == e2.start_position 120 | and e1.display_meta_text == e2.display_meta_text): 121 | theSame = e2 122 | break 123 | if theSame is None: 124 | return False 125 | return True 126 | 127 | @staticmethod 128 | def contains(completion_list1, completion_list2): 129 | for e2 in completion_list2: 130 | theSame = None 131 | for e1 in completion_list1: 132 | if (e2.text == e1.text 133 | and e2.start_position == e1.start_position 134 | and e2.display_meta_text == e1.display_meta_text): 135 | theSame = e1 136 | break 137 | if theSame is None: 138 | return False 139 | return True 140 | -------------------------------------------------------------------------------- /tests/test_telemetry.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import tempfile 3 | import json 4 | import os 5 | import unittest 6 | from stat import ST_MTIME 7 | 8 | import mssqlcli.telemetry_upload as telemetry_upload 9 | 10 | 11 | try: 12 | # Python 2.x 13 | from urllib2 import HTTPError 14 | except ImportError: 15 | # Python 3.x 16 | from urllib.error import HTTPError 17 | 18 | 19 | class TelemetryTests(unittest.TestCase): 20 | """ 21 | Tests for telemetry client. 22 | """ 23 | @staticmethod 24 | def build_telemetry_client(): 25 | import mssqlcli.telemetry as telemetry # pylint: disable=import-outside-toplevel 26 | return telemetry 27 | 28 | @unittest.skipUnless('MSSQL_CLI_TELEMETRY_OPTOUT' in os.environ.keys() and 29 | os.environ['MSSQL_CLI_TELEMETRY_OPTOUT'].lower() != 'true', 30 | "Only works when telemetry opt-out not set to true.") 31 | def test_telemetry_data_points(self): 32 | test_telemetry_client = self.build_telemetry_client() 33 | test_telemetry_client.start() 34 | 35 | try: 36 | payload = test_telemetry_client.conclude( 37 | service_endpoint_uri='https://vortex.data.microsoft.com/collect/v1/validate', 38 | separate_process=False) 39 | self.assertIsNotNone(payload, 'Payload was empty.') 40 | except HTTPError: 41 | self.fail('Recevied HTTP Error when validating payload against vortex.') 42 | 43 | payload = json.loads(payload.replace("'", '"')) 44 | 45 | expected_data_model_properties = [ 46 | 'Reserved.ChannelUsed', 47 | 'Reserved.SequenceNumber', 48 | 'Reserved.EventId', 49 | 'Reserved.SessionId', 50 | 'Reserved.TimeSinceSessionStart', 51 | 'Reserved.DataModel.Source', 52 | 'Reserved.DataModel.EntitySchemaVersion', 53 | 'Reserved.DataModel.Severity', 54 | 'Reserved.DataModel.CorrelationId', 55 | 'Context.Default.SQLTools.ExeName', 56 | 'Context.Default.SQLTools.ExeVersion', 57 | 'Context.Default.SQLTools.MacAddressHash', 58 | 'Context.Default.SQLTools.OS.Type', 59 | 'Context.Default.SQLTools.OS.Version', 60 | 'Context.Default.SQLTools.IsDocker', 61 | 'Context.Default.SQLTools.User.Id', 62 | 'Context.Default.SQLTools.User.IsMicrosoftInternal', 63 | 'Context.Default.SQLTools.User.IsOptedIn', 64 | 'Context.Default.SQLTools.ShellType', 65 | 'Context.Default.SQLTools.EnvironmentVariables', 66 | 'Context.Default.SQLTools.Locale', 67 | 'Context.Default.SQLTools.StartTime', 68 | 'Context.Default.SQLTools.EndTime', 69 | 'Context.Default.SQLTools.SessionDuration', 70 | 'Context.Default.SQLTools.PythonVersion', 71 | 'Context.Default.SQLTools.ServerVersion', 72 | 'Context.Default.SQLTools.ServerEdition', 73 | 'Context.Default.SQLTools.ConnectionType', 74 | ] 75 | 76 | for record in payload: 77 | properties = record['properties'] 78 | for prop in properties: 79 | self.assertTrue(prop in expected_data_model_properties, 80 | 'Additional property detected: {}.'.format(prop)) 81 | 82 | def test_telemetry_vortex_format(self): 83 | test_telemetry_client = self.build_telemetry_client() 84 | test_telemetry_client.start() 85 | 86 | os.environ[test_telemetry_client.MSSQL_CLI_TELEMETRY_OPT_OUT] = 'False' 87 | try: 88 | payload = test_telemetry_client.conclude( 89 | service_endpoint_uri='https://vortex.data.microsoft.com/collect/v1/validate', 90 | separate_process=False) 91 | self.assertIsNotNone(payload, 'Payload was empty.') 92 | except HTTPError: 93 | self.fail('Recevied HTTP Error when validating payload against vortex.') 94 | 95 | def test_telemetry_opt_out(self): 96 | test_telemetry_client = self.build_telemetry_client() 97 | os.environ[test_telemetry_client.MSSQL_CLI_TELEMETRY_OPT_OUT] = 'True' 98 | 99 | test_telemetry_client.start() 100 | payload = test_telemetry_client.conclude() 101 | self.assertIsNone(payload, 'Payload was uploaded when client opted out.') 102 | 103 | def test_file_time_check_rotation(self): 104 | test_telemetry_client = self.build_telemetry_client() 105 | expired_id_file = tempfile.NamedTemporaryFile() 106 | valid_id_file = tempfile.NamedTemporaryFile() 107 | try: 108 | st = os.stat(expired_id_file.name) 109 | modified_time = st[ST_MTIME] 110 | older_modified_time = modified_time - (25 * 3600) 111 | # Update modified time. 112 | os.utime(expired_id_file.name, (modified_time, older_modified_time)) 113 | 114 | # Verify both scenarios of a valid and expired id file. 115 | # pylint: disable=protected-access 116 | self.assertTrue(test_telemetry_client._user_id_file_is_old(expired_id_file.name)) 117 | self.assertFalse(test_telemetry_client._user_id_file_is_old(valid_id_file.name)) 118 | finally: 119 | expired_id_file.close() 120 | valid_id_file.close() 121 | 122 | 123 | if __name__ == u'__main__': 124 | # Enabling this env var would output what telemetry payload is sent. 125 | os.environ[telemetry_upload.DIAGNOSTICS_TELEMETRY_ENV_NAME] = 'False' 126 | unittest.main() 127 | -------------------------------------------------------------------------------- /mssqlcli/mssqlqueries.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import re 3 | 4 | def get_schemas(): 5 | """ 6 | Query string to retrieve schema names. 7 | :return: string 8 | """ 9 | sql = ''' 10 | SELECT name 11 | FROM sys.schemas 12 | ORDER BY 1''' 13 | return normalize(sql) 14 | 15 | 16 | def get_databases(): 17 | """ 18 | Query string to retrieve all database names. 19 | :return: string 20 | """ 21 | sql = ''' 22 | Select name 23 | FROM sys.databases 24 | ORDER BY 1''' 25 | return normalize(sql) 26 | 27 | 28 | def get_table_columns(): 29 | """ 30 | Query string to retrieve all table columns. 31 | :return: string 32 | """ 33 | sql = ''' 34 | SELECT isc.table_schema, 35 | isc.table_name, 36 | isc.column_name, 37 | isc.data_type, 38 | isc.column_default 39 | FROM 40 | ( 41 | SELECT table_schema, 42 | table_name, 43 | column_name, 44 | data_type, 45 | column_default 46 | FROM INFORMATION_SCHEMA.COLUMNS 47 | ) AS isc 48 | INNER JOIN 49 | ( 50 | SELECT table_schema, 51 | table_name 52 | FROM INFORMATION_SCHEMA.TABLES 53 | WHERE TABLE_TYPE = 'BASE TABLE' 54 | ) AS ist 55 | ON ist.table_name = isc.table_name AND ist.table_schema = isc.table_schema 56 | ORDER BY 1, 2''' 57 | return normalize(sql) 58 | 59 | 60 | def get_view_columns(): 61 | """ 62 | Query string to retrieve all view columns. 63 | :return: string 64 | """ 65 | sql = ''' 66 | SELECT isc.table_schema, 67 | isc.table_name, 68 | isc.column_name, 69 | isc.data_type, 70 | isc.column_default 71 | FROM 72 | ( 73 | SELECT table_schema, 74 | table_name, 75 | column_name, 76 | data_type, 77 | column_default 78 | FROM INFORMATION_SCHEMA.COLUMNS 79 | ) AS isc 80 | INNER JOIN 81 | ( 82 | SELECT table_schema, 83 | table_name 84 | FROM INFORMATION_SCHEMA.TABLES 85 | WHERE TABLE_TYPE = 'VIEW' 86 | ) AS ist 87 | ON ist.table_name = isc.table_name AND ist.table_schema = isc.table_schema 88 | ORDER BY 1, 2''' 89 | return normalize(sql) 90 | 91 | 92 | def get_views(): 93 | """ 94 | Query string to retrieve all views. 95 | :return: string 96 | """ 97 | sql = ''' 98 | SELECT table_schema, 99 | table_name 100 | FROM INFORMATION_SCHEMA.VIEWS 101 | ORDER BY 1, 2''' 102 | return normalize(sql) 103 | 104 | 105 | def get_tables(): 106 | """ 107 | Query string to retrive all tables. 108 | :return: string 109 | """ 110 | sql = ''' 111 | SELECT table_schema, 112 | table_name 113 | FROM INFORMATION_SCHEMA.TABLES 114 | WHERE table_type = 'BASE TABLE' 115 | ORDER BY 1, 2''' 116 | return normalize(sql) 117 | 118 | 119 | def get_user_defined_types(): 120 | """ 121 | Query string to retrieve all user defined types. 122 | :return: string 123 | """ 124 | sql = ''' 125 | SELECT schemas.name, 126 | types.name 127 | FROM 128 | ( 129 | SELECT name, 130 | schema_id 131 | FROM sys.types 132 | WHERE is_user_defined = 1) AS types 133 | INNER JOIN 134 | ( 135 | SELECT name, 136 | schema_id 137 | FROM sys.schemas) AS schemas 138 | ON types.schema_id = schemas.schema_id''' 139 | return normalize(sql) 140 | 141 | 142 | def get_functions(): 143 | """ 144 | Query string to retrieve stored procedures and functions. 145 | :return: string 146 | """ 147 | sql = ''' 148 | SELECT specific_schema, specific_name 149 | FROM INFORMATION_SCHEMA.ROUTINES 150 | ORDER BY 1, 2''' 151 | return normalize(sql) 152 | 153 | 154 | def get_foreignkeys(): 155 | """ 156 | Query string for returning foreign keys. 157 | :return: string 158 | """ 159 | sql = ''' 160 | SELECT 161 | kcu1.table_schema AS fk_table_schema, 162 | kcu1.table_name AS fk_table_name, 163 | kcu1.column_name AS fk_column_name, 164 | kcu2.table_schema AS referenced_table_schema, 165 | kcu2.table_name AS referenced_table_name, 166 | kcu2.column_name AS referenced_column_name 167 | FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS rc 168 | INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu1 169 | ON kcu1.constraint_catalog = rc.constraint_catalog 170 | AND kcu1.constraint_schema = rc.constraint_schema 171 | AND kcu1.constraint_name = rc.constraint_name 172 | INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu2 173 | ON kcu2.constraint_catalog = rc.unique_constraint_catalog 174 | AND kcu2.constraint_schema = rc.unique_constraint_schema 175 | AND kcu2.constraint_name = rc.unique_constraint_name 176 | AND kcu2.ordinal_position = kcu1.ordinal_position 177 | ORDER BY 3, 4''' 178 | return normalize(sql) 179 | 180 | 181 | def normalize(sql): 182 | if (sql == '' or sql is None): 183 | return sql 184 | sql = sql.replace('\r', ' ').replace('\n', ' ').strip() 185 | sql = re.sub(r' +', ' ', sql) 186 | return sql.decode('utf-8') if sys.version_info[0] < 3 else sql 187 | -------------------------------------------------------------------------------- /mssqlcli/packages/parseutils/meta.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=too-many-instance-attributes 2 | # pylint: disable=too-many-arguments 3 | 4 | from __future__ import print_function, unicode_literals 5 | from collections import namedtuple 6 | 7 | _ColumnMetadata = namedtuple( 8 | 'ColumnMetadata', 9 | ['name', 'datatype', 'foreignkeys', 'default', 'has_default'] 10 | ) 11 | 12 | 13 | def ColumnMetadata( 14 | name, datatype, foreignkeys=None, default=None, has_default=False 15 | ): 16 | return _ColumnMetadata( 17 | name, datatype, foreignkeys or [], default, has_default 18 | ) 19 | 20 | 21 | ForeignKey = namedtuple('ForeignKey', ['parentschema', 'parenttable', 22 | 'parentcolumn', 'childschema', 'childtable', 'childcolumn']) 23 | TableMetadata = namedtuple('TableMetadata', 'name columns') 24 | 25 | 26 | def parse_defaults(defaults_string): 27 | """ 28 | Yields default values for a function, given the string provided. 29 | """ 30 | if not defaults_string: 31 | return 32 | current = '' 33 | in_quote = None 34 | for char in defaults_string: 35 | if current == '' and char == ' ': 36 | # Skip space after comma separating default expressions 37 | continue 38 | if char in ('"', '\''): 39 | if in_quote and char == in_quote: 40 | # End quote 41 | in_quote = None 42 | elif not in_quote: 43 | # Begin quote 44 | in_quote = char 45 | elif char == ',' and not in_quote: 46 | # End of expression 47 | yield current 48 | current = '' 49 | continue 50 | current += char 51 | yield current 52 | 53 | 54 | class FunctionMetadata: 55 | 56 | def __init__( 57 | self, schema_name, func_name, arg_names, arg_types, arg_modes, 58 | return_type, is_aggregate, is_window, is_set_returning, arg_defaults 59 | ): 60 | """Class for describing a postgresql function""" 61 | 62 | self.schema_name = schema_name 63 | self.func_name = func_name 64 | 65 | self.arg_modes = tuple(arg_modes) if arg_modes else None 66 | self.arg_names = tuple(arg_names) if arg_names else None 67 | 68 | # Be flexible in not requiring arg_types -- use None as a placeholder 69 | # for each arg. (Used for compatibility with old versions of postgresql 70 | # where such info is hard to get. 71 | if arg_types: 72 | self.arg_types = tuple(arg_types) 73 | elif arg_modes: 74 | self.arg_types = tuple([None] * len(arg_modes)) 75 | elif arg_names: 76 | self.arg_types = tuple([None] * len(arg_names)) 77 | else: 78 | self.arg_types = None 79 | 80 | self.arg_defaults = tuple(parse_defaults(arg_defaults)) 81 | 82 | self.return_type = return_type.strip() 83 | self.is_aggregate = is_aggregate 84 | self.is_window = is_window 85 | self.is_set_returning = is_set_returning 86 | 87 | def __eq__(self, other): 88 | return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ 89 | 90 | def __ne__(self, other): 91 | return not self.__eq__(other) 92 | 93 | def _signature(self): 94 | return ( 95 | self.schema_name, self.func_name, self.arg_names, self.arg_types, 96 | self.arg_modes, self.return_type, self.is_aggregate, 97 | self.is_window, self.is_set_returning, self.arg_defaults 98 | ) 99 | 100 | def __hash__(self): 101 | return hash(self._signature()) 102 | 103 | def __repr__(self): 104 | return ( 105 | ( 106 | '%s(schema_name=%r, func_name=%r, arg_names=%r, ' 107 | 'arg_types=%r, arg_modes=%r, return_type=%r, is_aggregate=%r, ' 108 | 'is_window=%r, is_set_returning=%r, arg_defaults=%r)' 109 | ) % (self.__class__.__name__, self.schema_name, self.func_name, self.arg_names, 110 | self.arg_types, self.arg_modes, self.return_type, self.is_aggregate, 111 | self.is_window, self.is_set_returning, self.arg_defaults) 112 | ) 113 | 114 | def has_variadic(self): 115 | return self.arg_modes and any( 116 | arg_mode == 'v' for arg_mode in self.arg_modes) 117 | 118 | def args(self): 119 | """Returns a list of input-parameter ColumnMetadata namedtuples.""" 120 | if not self.arg_names: 121 | return [] 122 | modes = self.arg_modes or ['i'] * len(self.arg_names) 123 | args = [ 124 | (name, typ) 125 | for name, typ, mode in zip(self.arg_names, self.arg_types, modes) 126 | if mode in ('i', 'b', 'v') # IN, INOUT, VARIADIC 127 | ] 128 | 129 | def arg(name, typ, num): 130 | num_args = len(args) 131 | num_defaults = len(self.arg_defaults) 132 | has_default = num + num_defaults >= num_args 133 | default = ( 134 | self.arg_defaults[num - num_args + num_defaults] if has_default 135 | else None 136 | ) 137 | return ColumnMetadata(name, typ, [], default, has_default) 138 | 139 | return [arg(name, typ, num) for num, (name, typ) in enumerate(args)] 140 | 141 | def fields(self): 142 | """Returns a list of output-field ColumnMetadata namedtuples""" 143 | 144 | if self.return_type.lower() == 'void': 145 | return [] 146 | if not self.arg_modes: 147 | # For functions without output parameters, the function name 148 | # is used as the name of the output column. 149 | # E.g. 'SELECT unnest FROM unnest(...);' 150 | return [ColumnMetadata(self.func_name, self.return_type, [])] 151 | 152 | return [ColumnMetadata(name, typ, []) 153 | for name, typ, mode in zip( 154 | self.arg_names, self.arg_types, self.arg_modes) 155 | if mode in ('o', 'b', 't')] # OUT, INOUT, TABLE 156 | --------------------------------------------------------------------------------