diff -Nru python-vertica-0.7.4/CONTRIBUTING.md python-vertica-0.8.0/CONTRIBUTING.md --- python-vertica-0.7.4/CONTRIBUTING.md 2018-08-15 20:33:29.000000000 +0000 +++ python-vertica-0.8.0/CONTRIBUTING.md 2018-09-26 20:21:00.000000000 +0000 @@ -60,13 +60,48 @@ *vertica-python* comes with a test suite of its own, in the `vertica_python/tests` directory of the code base. It’s our policy to make sure all tests pass at all times. -We appreciate any and all [contributions](#tests) to the test suite! These tests use a Python module: [nosetests](https://nose.readthedocs.io). You might want to check out the Python documentation for more details. +We appreciate any and all [contributions to the test suite](#tests)! These tests use a Python module: [nosetests](https://nose.readthedocs.io). You might want to check out the Python documentation for more details. -To run the tests, you must have access to a Vertica database. Heres one way to go about it: +There are two types of tests: unit tests and integration tests. Unit tests do simple unit testing of individual classes and functions, which do not require database connection. Integration tests need to connect to a Vertica database to run stuffs, so you must have access to a Vertica database. Heres one way to go about it: - Download docker kitematic: https://kitematic.com/ - Spin up a vertica container (e.g. sumitchawla/vertica) -Set environment variables as needed by `vertica_python/tests/base.py` to match the database connection information. +Spin up your Vertica database for integration tests and then config test settings: +* Here are default settings: + ```sh + host: 'localhost' + port: 5433 + user: + database: + password: '' + log_dir: 'vp_test_log' # all test logs would write to files under this directory + log_level: logging.WARNING + ``` +* Override with a configuration file called `vertica_python/tests/common/vp_test.conf`. This is a file that would be ignored by git. We created an example `vertica_python/tests/common/vp_test.conf.example` for your reference. + ```sh + # edit under [vp_test_config] section + VP_TEST_HOST=10.0.0.2 + VP_TEST_PORT=5000 + VP_TEST_USER=dbadmin + VP_TEST_DATABASE=vdb1 + VP_TEST_PASSWORD=abcdef1234 + VP_TEST_LOG_DIR=my_log/year/month/date + VP_TEST_LOG_LEVEL=DEBUG + ``` +* Override again with VP_TEST_* environment variables + ```shell + # Set environment variables in linux + $ export VP_TEST_HOST=10.0.0.2 + $ export VP_TEST_PORT=5000 + $ export VP_TEST_USER=dbadmin + $ export VP_TEST_DATABASE=vdb1 + $ export VP_TEST_PASSWORD=abcdef1234 + $ export VP_TEST_LOG_DIR=my_log/year/month/date + $ export VP_TEST_LOG_LEVEL=DEBUG + + # Delete your environment variables after tests + $ unset VP_TEST_PASSWORD + ``` Tox (https://tox.readthedocs.io) is a tool for running those tests in different Python environments. *vertica-python* includes a `tox.ini` file that lists all Python versions we test. @@ -76,7 +111,7 @@ ``` Edit `tox.ini` envlist property to list the version(s) of Python you have installed. -Then you can run the **tox** command from any place in the *vertica-python* source tree. +Then you can run the **tox** command from any place in the *vertica-python* source tree. If VP_TEST_LOG_DIR sets to a relative path, it will be in the *vertica-python* directory no matter where you run the **tox** command. 1. Run all tests using tox: ```bash @@ -87,16 +122,28 @@ Run all tests under `test_case.py` on the python versions 2.7 and 3.5 ```bash - tox -e py27,py35 -- vertica_python/tests/test_cases.py + tox -e py27,py35 -- vertica_python/tests/integration_tests/test_cases.py + ``` + +1. Run all unit tests on the python version 3.6: + ```bash + tox -e py36 -- -a unit_tests + ``` + +1. Run all integration tests on the python version 3.4 with verbose result outputs and print any stdout immediately: + ```bash + tox -e py34 -- -v -s -a integration_tests ``` 1. Run an individual test on specified python versions: Run the test `test_case` under `test_cases.py` on the python versions 2.7 and 3.5 ```bash - tox -e py27,py35 -- vertica_python/tests/test_cases.py:TestCaseClass.test_case + tox -e py27,py35 -- vertica_python/tests/integration_tests/test_cases.py:TestCaseClass.test_case ``` +The arguments after the `--` will be substituted everywhere where you specify `{posargs}` in your test *commands* of `tox.ini`. +Run `$ nosetests --help` to see all arguments you can specify after the `--`. For more usages about [tox](https://tox.readthedocs.io), see the Python documentation. @@ -139,7 +186,7 @@ Add appropriate tests for the bug’s or feature's behavior, run the test suite again and ensure that all tests pass. Here is the guideline for writing test: - Tests should be easy for any contributor to run. Contributors may not get complete access to their Vertica database, for example, they may only have a non-admin user with write privileges to a single schema, and the database may not be the latest version. We encourage tests to use only what they need and nothing more. - - If there are requirements to the database for running a test, the test should adapt to different situations and never report a failure. For example, if a test depends on a multi-node database, it should check the number of DB nodes first, and skip itself when it connects to a single-node database. + - If there are requirements to the database for running a test, the test should adapt to different situations and never report a failure. For example, if a test depends on a multi-node database, it should check the number of DB nodes first, and skip itself when it connects to a single-node database (see helper function `require_DB_nodes_at_least()` in `vertica_python/tests/integration_tests/base.py`). ## Step 5: Push and Rebase diff -Nru python-vertica-0.7.4/debian/changelog python-vertica-0.8.0/debian/changelog --- python-vertica-0.7.4/debian/changelog 2018-08-20 12:03:12.000000000 +0000 +++ python-vertica-0.8.0/debian/changelog 2018-10-31 16:26:46.000000000 +0000 @@ -1,3 +1,16 @@ +python-vertica (0.8.0-2) unstable; urgency=medium + + * Enable basic autopkgtest + + -- Jean Baptiste Favre Wed, 31 Oct 2018 17:26:46 +0100 + +python-vertica (0.8.0-1) unstable; urgency=medium + + * New upstream version 0.8.0 + * Update Standards-Version in d/control + + -- Jean Baptiste Favre Fri, 28 Sep 2018 09:53:00 +0200 + python-vertica (0.7.4-2) unstable; urgency=medium * New upstream version 0.7.4 diff -Nru python-vertica-0.7.4/debian/control python-vertica-0.8.0/debian/control --- python-vertica-0.7.4/debian/control 2018-08-20 11:48:08.000000000 +0000 +++ python-vertica-0.8.0/debian/control 2018-10-31 10:47:12.000000000 +0000 @@ -13,7 +13,8 @@ python3-all, python3-dateutil, python3-tz -Standards-Version: 4.2.0 +Standards-Version: 4.2.1 +Testsuite: autopkgtest-pkg-python Homepage: https://github.com/vertica/vertica-python Vcs-Git: https://salsa.debian.org/python-team/modules/python-vertica.git Vcs-Browser: https://salsa.debian.org/python-team/modules/python-vertica diff -Nru python-vertica-0.7.4/.gitignore python-vertica-0.8.0/.gitignore --- python-vertica-0.7.4/.gitignore 2018-08-15 20:33:29.000000000 +0000 +++ python-vertica-0.8.0/.gitignore 2018-09-26 20:21:00.000000000 +0000 @@ -1,6 +1,9 @@ *.py[cod] test.py +# Test configuration +vp_test.conf + # C extensions *.so diff -Nru python-vertica-0.7.4/README.md python-vertica-0.8.0/README.md --- python-vertica-0.7.4/README.md 2018-08-15 20:33:29.000000000 +0000 +++ python-vertica-0.8.0/README.md 2018-09-26 20:21:00.000000000 +0000 @@ -2,6 +2,7 @@ [![PyPI version](https://badge.fury.io/py/vertica-python.svg)](https://badge.fury.io/py/vertica-python) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg)](https://opensource.org/licenses/Apache-2.0) +[![Python Version](https://img.shields.io/pypi/pyversions/vertica-python.svg)](https://www.python.org/downloads/) :loudspeaker: 08/14/2018: *vertica-python* becomes Vertica’s first officially supported open source database client, see the blog [here](https://my.vertica.com/blog/vertica-python-becomes-verticas-first-officially-supported-open-source-database-client/). @@ -45,40 +46,6 @@ https://github.com/vertica/vertica-python -## Run unit tests - -To run the tests, you must have access to a Vertica database. Heres one way to go about it: - -Download docker kitematic: -https://kitematic.com/ - -Spin up a vertica container (i use sumitchawla/vertica) - -Edit the port number in `tests/test_commons.py` to match the container. - -Install tox: -http://tox.readthedocs.io - -Edit `tox.ini` envlist property to list the version(s) of python you have installed - -Run all tests using tox: -```bash -tox -``` - -Run a test suite on specified python versions: - -Run all tests under `test_case.py` on the python versions 2.7 and 3.5 -```bash -tox -e py27,py35 -- vertica_python/tests/test_cases.py -``` - -Run an individual test on specified python versions: - -Run the test `test_case` under `test_cases.py` on the python versions 2.7 and 3.5 -```bash -tox -e py27,py35 -- vertica_python/tests/test_cases.py:TestCaseClass.test_case -``` ## Usage @@ -134,6 +101,87 @@ See more on SSL options [here](https://docs.python.org/2/library/ssl.html). +Logging is disabled by default if you do not pass values to both ```log_level``` and ```log_path```. The default value of ```log_level``` is logging.WARNING. You can find all levels [here](https://docs.python.org/3.6/library/logging.html#logging-levels). The default value of ```log_path``` is 'vertica_python.log', the log file will be in the current execution directory. For example, + +```python +import vertica_python +import logging + +## Example 1: write DEBUG level logs to './vertica_python.log' +conn_info = {'host': '127.0.0.1', + 'port': 5433, + 'user': 'some_user', + 'password': 'some_password', + 'database': 'a_database', + 'log_level': logging.DEBUG} +with vertica_python.connect(**conn_info) as connection: + # do things + +## Example 2: write WARNING level logs to './path/to/logs/client.log' +conn_info = {'host': '127.0.0.1', + 'port': 5433, + 'user': 'some_user', + 'password': 'some_password', + 'database': 'a_database', + 'log_path': 'path/to/logs/client.log'} +with vertica_python.connect(**conn_info) as connection: + # do things + +## Example 3: write INFO level logs to '/home/admin/logs/vClient.log' +conn_info = {'host': '127.0.0.1', + 'port': 5433, + 'user': 'some_user', + 'password': 'some_password', + 'database': 'a_database', + 'log_level': logging.INFO, + 'log_path': '/home/admin/logs/vClient.log'} +with vertica_python.connect(**conn_info) as connection: + # do things +``` + +Connection Failover: Supply a list of backup hosts to ```backup_server_node``` for the client to try if the primary host you specify in the connection parameters (```host```, ```port```) is unreachable. Each item in the list should be either a host string (using default port 5433) or a (host, port) tuple. A host can be a host name or an IP address. + +```python +import vertica_python + +conn_info = {'host': 'unreachable.server.com', + 'port': 888, + 'user': 'some_user', + 'password': 'some_password', + 'database': 'a_database', + 'backup_server_node': ['123.456.789.123', 'invalid.com', ('10.20.82.77', 6000)]} +connection = vertica_python.connect(**conn_info) +``` + +Connection Load Balancing helps automatically spread the overhead caused by client connections across the cluster by having hosts redirect client connections to other hosts. Both the server and the client need to enable load balancing for it to function. If the server disables connection load balancing, the load balancing request from client will be ignored. + +```python +import vertica_python + +conn_info = {'host': '127.0.0.1', + 'port': 5433, + 'user': 'some_user', + 'password': 'some_password', + 'database': 'vdb', + 'connection_load_balance': True} + +# Server enables load balancing +with connect(**conn_info) as conn: + cur = conn.cursor() + cur.execute("SELECT NODE_NAME FROM V_MONITOR.CURRENT_SESSION") + print("Client connects to primary node:", cur.fetchone()[0]) + cur.execute("SELECT SET_LOAD_BALANCE_POLICY('ROUNDROBIN')") + +with connect(**conn_info) as conn: + cur = conn.cursor() + cur.execute("SELECT NODE_NAME FROM V_MONITOR.CURRENT_SESSION") + print("Client redirects to node:", cur.fetchone()[0]) + +## Output +# Client connects to primary node: v_vdb_node0003 +# Client redirects to node: v_vdb_node0005 +``` + **Stream query results**: ```python diff -Nru python-vertica-0.7.4/setup.py python-vertica-0.8.0/setup.py --- python-vertica-0.7.4/setup.py 2018-08-15 20:33:29.000000000 +0000 +++ python-vertica-0.8.0/setup.py 2018-09-26 20:21:00.000000000 +0000 @@ -45,7 +45,7 @@ # version should use the format 'x.x.x' (instead of 'vx.x.x') setup( name='vertica-python', - version='0.7.4', + version='0.8.0', description='A native Python client for the Vertica database.', author='Justin Berka, Alex Kim, Siting Ren', author_email='justin.berka@gmail.com, alex.kim@uber.com, sitingren@hotmail.com', diff -Nru python-vertica-0.7.4/tox.ini python-vertica-0.8.0/tox.ini --- python-vertica-0.7.4/tox.ini 2018-08-15 20:33:29.000000000 +0000 +++ python-vertica-0.8.0/tox.ini 2018-09-26 20:21:00.000000000 +0000 @@ -4,7 +4,9 @@ [testenv] passenv = * commands = - nosetests {posargs} + nosetests {posargs} deps = - nose==1.3.6 - psycopg2>=2.5.1 + nose==1.3.6 + psycopg2>=2.5.1 + pytz + python-dateutil \ No newline at end of file diff -Nru python-vertica-0.7.4/.travis.yml python-vertica-0.8.0/.travis.yml --- python-vertica-0.7.4/.travis.yml 1970-01-01 00:00:00.000000000 +0000 +++ python-vertica-0.8.0/.travis.yml 2018-09-26 20:21:00.000000000 +0000 @@ -0,0 +1,29 @@ +dist: trusty +language: python + +services: + - docker + +matrix: + include: + - python: 3.6 + env: TOXENV=py36 + - python: 3.5 + env: TOXENV=py35 + - python: 3.4 + env: TOXENV=py34 + - python: 2.7 + env: TOXENV=py27 + +before_install: + - git clone https://github.com/jbfavre/docker-vertica.git + - wget $VERTICA_CE_URL -O docker-vertica/packages/vertica-ce.latest.rpm + - docker build -f docker-vertica/Dockerfile.centos.7_9.0 --build-arg VERTICA_PACKAGE=vertica-ce.latest.rpm -t jbfavre/vertica docker-vertica + - docker images + - docker run -d -p 5433:5433 jbfavre/vertica + - sleep 30 +install: + - pip install tox +script: + - export VP_TEST_USER=dbadmin + - tox diff -Nru python-vertica-0.7.4/vertica_python/__init__.py python-vertica-0.8.0/vertica_python/__init__.py --- python-vertica-0.7.4/vertica_python/__init__.py 2018-08-15 20:33:29.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/__init__.py 2018-09-26 20:21:00.000000000 +0000 @@ -56,7 +56,7 @@ 'OperationalError', 'ProgrammingError'] # The version number of this library. -version_info = (0, 7, 4) +version_info = (0, 8, 0) __version__ = '.'.join(map(str, version_info)) # The protocol version (3.0.0) implemented in this library. diff -Nru python-vertica-0.7.4/vertica_python/tests/base.py python-vertica-0.8.0/vertica_python/tests/base.py --- python-vertica-0.7.4/vertica_python/tests/base.py 2018-08-15 20:33:29.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/base.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,138 +0,0 @@ -# Copyright (c) 2018 Micro Focus or one of its affiliates. -# Copyright (c) 2018 Uber Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Copyright (c) 2013-2017 Uber Technologies, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from __future__ import print_function, division, absolute_import - -import os -import unittest - -from six import string_types - -from .. import * -from ..compat import as_text, as_str, as_bytes - -DEFAULT_VP_TEST_HOST = '127.0.0.1' -DEFAULT_VP_TEST_PORT = 5433 -DEFAULT_VP_TEST_USER = 'dbadmin' -DEFAULT_VP_TEST_PASSWD = '' -DEFAULT_VP_TEST_DB = 'docker' -DEFAULT_VP_TEST_TABLE = 'vertica_python_unit_test' - - -class VerticaPythonTestCase(unittest.TestCase): - """Base class for tests that query Vertica.""" - - @classmethod - def setUpClass(cls): - cls._host = os.getenv('VP_TEST_HOST', DEFAULT_VP_TEST_HOST) - cls._port = int(os.getenv('VP_TEST_PORT', DEFAULT_VP_TEST_PORT)) - cls._user = os.getenv('VP_TEST_USER', DEFAULT_VP_TEST_USER) - cls._password = os.getenv('VP_TEST_PASSWD', DEFAULT_VP_TEST_PASSWD) - cls._database = os.getenv('VP_TEST_DB', DEFAULT_VP_TEST_DB) - cls._table = os.getenv('VP_TEST_TABLE', DEFAULT_VP_TEST_TABLE) - - cls._conn_info = { - 'host': cls._host, - 'port': cls._port, - 'database': cls._database, - 'user': cls._user, - 'password': cls._password, - } - - @classmethod - def tearDownClass(cls): - with cls._connect() as conn: - cur = conn.cursor() - cur.execute("DROP TABLE IF EXISTS {0}".format(cls._table)) - - @classmethod - def _connect(cls): - """Connects to vertica. - - :return: a connection to vertica. - """ - return connect(**cls._conn_info) - - def _query_and_fetchall(self, query): - """Creates a new connection, executes a query and fetches all the results. - - :param query: query to execute - :return: all fetched results as returned by cursor.fetchall() - """ - with self._connect() as conn: - cur = conn.cursor() - cur.execute(query) - results = cur.fetchall() - - return results - - def _query_and_fetchone(self, query): - """Creates a new connection, executes a query and fetches one result. - - :param query: query to execute - :return: the first result fetched by cursor.fetchone() - """ - with self._connect() as conn: - cur = conn.cursor() - cur.execute(query) - result = cur.fetchone() - - return result - - def assertTextEqual(self, first, second, msg=None): - first_text = as_text(first) - second_text = as_text(second) - self.assertEqual(first=first_text, second=second_text, msg=msg) - - def assertStrEqual(self, first, second, msg=None): - first_str = as_str(first) - second_str = as_str(second) - self.assertEqual(first=first_str, second=second_str, msg=msg) - - def assertBytesEqual(self, first, second, msg=None): - first_bytes = as_bytes(first) - second_bytes = as_bytes(second) - self.assertEqual(first=first_bytes, second=second_bytes, msg=msg) - - def assertResultEqual(self, value, result, msg=None): - if isinstance(value, string_types): - self.assertTextEqual(first=value, second=result, msg=msg) - else: - self.assertEqual(first=value, second=result, msg=msg) - - def assertListOfListsEqual(self, list1, list2, msg=None): - self.assertEqual(len(list1), len(list2), msg=msg) - for l1, l2 in zip(list1, list2): - self.assertListEqual(l1, l2, msg=msg) diff -Nru python-vertica-0.7.4/vertica_python/tests/common/base.py python-vertica-0.8.0/vertica_python/tests/common/base.py --- python-vertica-0.7.4/vertica_python/tests/common/base.py 1970-01-01 00:00:00.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/common/base.py 2018-09-26 20:21:00.000000000 +0000 @@ -0,0 +1,160 @@ +# Copyright (c) 2018 Micro Focus or one of its affiliates. +# Copyright (c) 2018 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright (c) 2013-2017 Uber Technologies, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import print_function, division, absolute_import + +import os +import sys +import logging +import unittest +import inspect +import getpass +import six +if six.PY2: + from ConfigParser import ConfigParser +elif six.PY3: + from configparser import ConfigParser + +from ...compat import as_text, as_str, as_bytes +from ...vertica.log import VerticaLogging + + +default_configs = { + 'log_dir': 'vp_test_log', + 'log_level': logging.WARNING, + 'host': 'localhost', + 'port': 5433, + 'user': getpass.getuser(), + 'password': '', +} + +class VerticaPythonTestCase(unittest.TestCase): + """ + Base class for all tests + """ + + @classmethod + def _load_test_config(cls, config_list): + test_config = {} + + # load default configurations + for key in config_list: + if key != 'database': + test_config[key] = default_configs[key] + + # override with the configuration file + confparser = ConfigParser() + confparser.optionxform = str + SECTION = 'vp_test_config' # section name in the configuration file + # the configuration file is placed in the same directory as this file + conf_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), + 'vp_test.conf') + confparser.read(conf_file) + for k in config_list: + option = 'VP_TEST_' + k.upper() + if confparser.has_option(SECTION, option): + test_config[k] = confparser.get(SECTION, option) + + # override again with VP_TEST_* environment variables + for k in config_list: + env = 'VP_TEST_' + k.upper() + if env in os.environ: + test_config[k] = os.environ[env] + + # data preprocessing + # value is string when loaded from configuration file and environment variable + if 'port' in test_config: + test_config['port'] = int(test_config['port']) + if 'database' in config_list and 'user' in test_config: + test_config.setdefault('database', test_config['user']) + if 'log_level' in test_config: + levels = ['NOTSET', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] + if isinstance(test_config['log_level'], str): + if test_config['log_level'] not in levels: + raise ValueError("Invalid value for VP_TEST_LOG_LEVEL: '{}'".format(test_config['log_level'])) + test_config['log_level'] = eval('logging.' + test_config['log_level']) + if 'log_dir' in test_config: + test_config['log_dir'] = os.path.join(test_config['log_dir'], + 'py{0}{1}'.format(sys.version_info.major, sys.version_info.minor)) + + return test_config + + @classmethod + def _setup_logger(cls, tag, log_dir, log_level): + # Setup test logger + # E.g. If the class is defined in tests/integration_tests/test_dates.py + # and test cases run under python2.7, then + # the log would write to $VP_TEST_LOG_DIR/py27/integration_tests/test_dates.log + + testfile = os.path.splitext(os.path.basename(inspect.getsourcefile(cls)))[0] + logfile = os.path.join(log_dir, tag, testfile + '.log') + VerticaLogging.setup_file_logging(cls.__name__, logfile, log_level, cls.__name__) + cls.logger = logging.getLogger(cls.__name__) + return logfile + + def setUp(self): + self.logger.info('\n\n'+'-'*50+'\n Begin '+self._testMethodName+'\n'+'-'*50) + + def tearDown(self): + self.logger.info('\n'+'-'*10+' End '+self._testMethodName+' '+'-'*10+'\n') + + # Common assertions + def assertTextEqual(self, first, second, msg=None): + first_text = as_text(first) + second_text = as_text(second) + self.assertEqual(first=first_text, second=second_text, msg=msg) + + def assertStrEqual(self, first, second, msg=None): + first_str = as_str(first) + second_str = as_str(second) + self.assertEqual(first=first_str, second=second_str, msg=msg) + + def assertBytesEqual(self, first, second, msg=None): + first_bytes = as_bytes(first) + second_bytes = as_bytes(second) + self.assertEqual(first=first_bytes, second=second_bytes, msg=msg) + + def assertResultEqual(self, value, result, msg=None): + if isinstance(value, six.string_types): + self.assertTextEqual(first=value, second=result, msg=msg) + else: + self.assertEqual(first=value, second=result, msg=msg) + + def assertListOfListsEqual(self, list1, list2, msg=None): + self.assertEqual(len(list1), len(list2), msg=msg) + for l1, l2 in zip(list1, list2): + self.assertListEqual(l1, l2, msg=msg) + + diff -Nru python-vertica-0.7.4/vertica_python/tests/common/__init__.py python-vertica-0.8.0/vertica_python/tests/common/__init__.py --- python-vertica-0.7.4/vertica_python/tests/common/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/common/__init__.py 2018-09-26 20:21:00.000000000 +0000 @@ -0,0 +1,34 @@ +# Copyright (c) 2018 Micro Focus or one of its affiliates. +# Copyright (c) 2018 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright (c) 2013-2017 Uber Technologies, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. \ No newline at end of file diff -Nru python-vertica-0.7.4/vertica_python/tests/common/vp_test.conf.example python-vertica-0.8.0/vertica_python/tests/common/vp_test.conf.example --- python-vertica-0.7.4/vertica_python/tests/common/vp_test.conf.example 1970-01-01 00:00:00.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/common/vp_test.conf.example 2018-09-26 20:21:00.000000000 +0000 @@ -0,0 +1,16 @@ +# To override default configuration information for the driver test system, +# create a new file called 'vp_test.conf' in this directory with the following syntax + +[vp_test_config] + +# Connection information +VP_TEST_HOST=10.0.0.2 +VP_TEST_PORT=5000 +VP_TEST_USER=dbadmin +#VP_TEST_DATABASE=vdb1 +#VP_TEST_PASSWORD=abcdef1234 + +# Logging information +# Valid VP_TEST_LOG_LEVEL options: 'NOTSET', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL' +VP_TEST_LOG_LEVEL=DEBUG +VP_TEST_LOG_DIR=mylog/vp_tox_tests_log diff -Nru python-vertica-0.7.4/vertica_python/tests/integration_tests/base.py python-vertica-0.8.0/vertica_python/tests/integration_tests/base.py --- python-vertica-0.7.4/vertica_python/tests/integration_tests/base.py 1970-01-01 00:00:00.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/integration_tests/base.py 2018-09-26 20:21:00.000000000 +0000 @@ -0,0 +1,155 @@ +# Copyright (c) 2018 Micro Focus or one of its affiliates. +# Copyright (c) 2018 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright (c) 2013-2017 Uber Technologies, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import print_function, division, absolute_import + +from nose.plugins.attrib import attr + +from ... import errors, connect +from ..common.base import VerticaPythonTestCase + + +@attr('integration_tests') +class VerticaPythonIntegrationTestCase(VerticaPythonTestCase): + """ + Base class for tests that connect to a Vertica database to run stuffs. + + This class is responsible for managing the environment variables and + connection info used for all the tests, and provides support code + to do common assertions and execute common queries. + """ + + @classmethod + def setUpClass(cls): + config_list = ['log_dir', 'log_level', 'host', 'port', + 'user', 'password', 'database'] + cls.test_config = cls._load_test_config(config_list) + + # Test logger + logfile = cls._setup_logger('integration_tests', + cls.test_config['log_dir'], cls.test_config['log_level']) + + # Connection info + cls._conn_info = { + 'host': cls.test_config['host'], + 'port': cls.test_config['port'], + 'database': cls.test_config['database'], + 'user': cls.test_config['user'], + 'password': cls.test_config['password'], + 'log_level': cls.test_config['log_level'], + 'log_path': logfile, + } + cls.db_node_num = cls._get_node_num() + cls.logger.info("Number of database node(s) = {}".format(cls.db_node_num)) + + @classmethod + def tearDownClass(cls): + pass + + @classmethod + def _connect(cls): + """Connects to vertica. + + :return: a connection to vertica. + """ + return connect(**cls._conn_info) + + @classmethod + def _get_node_num(cls): + """Executes a query to get the number of nodes in the database + + :return: the number of database nodes + """ + with cls._connect() as conn: + cur = conn.cursor() + cur.execute("SELECT count(*) FROM nodes WHERE node_state='UP'") + return cur.fetchone()[0] + + def _query_and_fetchall(self, query): + """Creates a new connection, executes a query and fetches all the results. + + :param query: query to execute + :return: all fetched results as returned by cursor.fetchall() + """ + with self._connect() as conn: + cur = conn.cursor() + cur.execute(query) + results = cur.fetchall() + + return results + + def _query_and_fetchone(self, query): + """Creates a new connection, executes a query and fetches one result. + + :param query: query to execute + :return: the first result fetched by cursor.fetchone() + """ + with self._connect() as conn: + cur = conn.cursor() + cur.execute(query) + result = cur.fetchone() + + return result + + # Common assertions + def assertConnectionFail(self): + err_msg = 'Failed to establish a connection to the primary server or any backup address.' + with self.assertRaisesRegexp(errors.ConnectionError, err_msg): + with self._connect() as conn: + pass + + def assertConnectionSuccess(self): + try: + with self._connect() as conn: + pass + except Exception as e: + self.fail('Connection failed: {0}'.format(e)) + + # Some tests require server-side setup + # In that case, tests that depend on that setup should be skipped to prevent false failures + # Tests that depend on the server-setup should call these methods to express requirements + def require_DB_nodes_at_least(self, min_node_num): + if not isinstance(min_node_num, int): + err_msg = "Node number '{0}' must be an instance of 'int'".format(min_node_num) + raise TypeError(err_msg) + if min_node_num <= 0: + err_msg = "Node number {0} must be a positive integer".format(min_node_num) + raise ValueError(err_msg) + + if self.db_node_num < min_node_num: + msg = ("The test requires a database that has at least {0} node(s), " + "but this database has only {1} available node(s).").format( + min_node_num, self.db_node_num) + self.skipTest(msg) diff -Nru python-vertica-0.7.4/vertica_python/tests/integration_tests/__init__.py python-vertica-0.8.0/vertica_python/tests/integration_tests/__init__.py --- python-vertica-0.7.4/vertica_python/tests/integration_tests/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/integration_tests/__init__.py 2018-09-26 20:21:00.000000000 +0000 @@ -0,0 +1,34 @@ +# Copyright (c) 2018 Micro Focus or one of its affiliates. +# Copyright (c) 2018 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright (c) 2013-2017 Uber Technologies, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. \ No newline at end of file diff -Nru python-vertica-0.7.4/vertica_python/tests/integration_tests/test_column.py python-vertica-0.8.0/vertica_python/tests/integration_tests/test_column.py --- python-vertica-0.7.4/vertica_python/tests/integration_tests/test_column.py 1970-01-01 00:00:00.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/integration_tests/test_column.py 2018-09-26 20:21:00.000000000 +0000 @@ -0,0 +1,53 @@ +# Copyright (c) 2018 Micro Focus or one of its affiliates. +# Copyright (c) 2018 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright (c) 2013-2017 Uber Technologies, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import print_function, division, absolute_import + +from .base import VerticaPythonIntegrationTestCase + + +class ColumnTestCase(VerticaPythonIntegrationTestCase): + def test_column_names_query(self): + columns = ['isocode', 'name', u'\uFF04'] + + with self._connect() as conn: + cur = conn.cursor() + cur.execute(u""" + SELECT 'US' AS {0}, 'United States' AS {1}, 'USD' AS {2} + UNION ALL SELECT 'CA', 'Canada', 'CAD' + UNION ALL SELECT 'MX', 'Mexico', 'MXN' """.format(*columns)) + description = cur.description + + self.assertListEqual([d.name for d in description], columns) diff -Nru python-vertica-0.7.4/vertica_python/tests/integration_tests/test_connection.py python-vertica-0.8.0/vertica_python/tests/integration_tests/test_connection.py --- python-vertica-0.7.4/vertica_python/tests/integration_tests/test_connection.py 1970-01-01 00:00:00.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/integration_tests/test_connection.py 2018-09-26 20:21:00.000000000 +0000 @@ -0,0 +1,62 @@ +# Copyright (c) 2018 Micro Focus or one of its affiliates. +# Copyright (c) 2018 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright (c) 2013-2017 Uber Technologies, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import print_function, division, absolute_import + +import getpass +from .base import VerticaPythonIntegrationTestCase + + +class ConnectionTestCase(VerticaPythonIntegrationTestCase): + def test_client_os_user_name_metadata(self): + value = getpass.getuser() + + # Metadata client_os_user_name sent from client should be captured into system tables + query = 'SELECT client_os_user_name FROM v_monitor.current_session' + res = self._query_and_fetchone(query) + self.assertEqual(res[0], value) + + query = 'SELECT client_os_user_name FROM v_monitor.sessions WHERE session_id=(SELECT current_session())' + res = self._query_and_fetchone(query) + self.assertEqual(res[0], value) + + query = 'SELECT client_os_user_name FROM v_monitor.user_sessions WHERE session_id=(SELECT current_session())' + res = self._query_and_fetchone(query) + self.assertEqual(res[0], value) + + query = 'SELECT client_os_user_name FROM v_internal.dc_session_starts WHERE session_id=(SELECT current_session())' + res = self._query_and_fetchone(query) + self.assertEqual(res[0], value) + diff -Nru python-vertica-0.7.4/vertica_python/tests/integration_tests/test_cursor.py python-vertica-0.8.0/vertica_python/tests/integration_tests/test_cursor.py --- python-vertica-0.7.4/vertica_python/tests/integration_tests/test_cursor.py 1970-01-01 00:00:00.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/integration_tests/test_cursor.py 2018-09-26 20:21:00.000000000 +0000 @@ -0,0 +1,554 @@ +# Copyright (c) 2018 Micro Focus or one of its affiliates. +# Copyright (c) 2018 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright (c) 2013-2017 Uber Technologies, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import print_function, division, absolute_import + +import datetime +import logging +import os as _os +import re +import tempfile + +from .base import VerticaPythonIntegrationTestCase +from ... import errors + + +class CursorTestCase(VerticaPythonIntegrationTestCase): + def setUp(self): + super(CursorTestCase, self).setUp() + self._table = 'cursor_test' + self._init_table() + + def _init_table(self): + with self._connect() as conn: + cur = conn.cursor() + # clean old table + cur.execute("DROP TABLE IF EXISTS {0}".format(self._table)) + + # create test table + cur.execute("""CREATE TABLE {0} ( + a INT, + b VARCHAR(32) + ) + """.format(self._table)) + + def tearDown(self): + with self._connect() as conn: + cur = conn.cursor() + cur.execute("DROP TABLE IF EXISTS {0}".format(self._table)) + super(CursorTestCase, self).tearDown() + + def test_inline_commit(self): + with self._connect() as conn: + cur = conn.cursor() + cur.execute( + "INSERT INTO {0} (a, b) VALUES (1, 'aa'); COMMIT;".format(self._table)) + cur.execute("SELECT a, b FROM {0} WHERE a = 1".format(self._table)) + + # unknown rowcount + self.assertEqual(cur.rowcount, -1) + + res = cur.fetchall() + self.assertEqual(cur.rowcount, 1) + + self.assertListOfListsEqual(res, [[1, 'aa']]) + + def test_multi_inserts_and_transaction(self): + with self._connect() as conn1, self._connect() as conn2: + cur1 = conn1.cursor() + cur2 = conn2.cursor() + + # insert data without a commit + cur1.execute("INSERT INTO {0} (a, b) VALUES (2, 'bb')".format(self._table)) + + # verify we can see it from this cursor + cur1.execute("SELECT a, b FROM {0} WHERE a = 2".format(self._table)) + res_from_cur_1_before_commit = cur1.fetchall() + self.assertListOfListsEqual(res_from_cur_1_before_commit, [[2, 'bb']]) + + # verify we cant see it from other cursor + cur2.execute("SELECT a, b FROM {0} WHERE a = 2".format(self._table)) + + res_from_cur2_before_commit = cur2.fetchall() + self.assertListOfListsEqual(res_from_cur2_before_commit, []) + + # insert more data then commit + cur1.execute("INSERT INTO {0} (a, b) VALUES (3, 'cc')".format(self._table)) + cur1.execute("COMMIT") + + # verify we can see it from this cursor + cur1.execute( + "SELECT a, b FROM {0} WHERE a = 2 OR a = 3 ORDER BY a".format(self._table)) + res_from_cur1_after_commit = cur1.fetchall() + self.assertListOfListsEqual(res_from_cur1_after_commit, [[2, 'bb'], [3, 'cc']]) + + # verify we can see it from other cursor + cur2.execute( + "SELECT a, b FROM {0} WHERE a = 2 OR a = 3 ORDER BY a".format(self._table)) + res_from_cur2_after_commit = cur2.fetchall() + self.assertListOfListsEqual(res_from_cur2_after_commit, [[2, 'bb'], [3, 'cc']]) + + def test_conn_commit(self): + with self._connect() as conn: + cur = conn.cursor() + cur.execute("INSERT INTO {0} (a, b) VALUES (5, 'cc')".format(self._table)) + conn.commit() + + with self._connect() as conn: + cur = conn.cursor() + cur.execute("SELECT a, b FROM {0} WHERE a = 5".format(self._table)) + res = cur.fetchall() + + self.assertListOfListsEqual(res, [[5, 'cc']]) + + def test_delete(self): + with self._connect() as conn: + cur = conn.cursor() + + cur.execute("INSERT INTO {0} (a, b) VALUES (5, 'cc')".format(self._table)) + self.assertEqual(cur.rowcount, -1) + update_res = cur.fetchall() + self.assertListOfListsEqual(update_res, [[1]]) + conn.commit() + + # validate delete count + cur.execute("DELETE FROM {0} WHERE a = 5".format(self._table)) + self.assertEqual(cur.rowcount, -1) + delete_res = cur.fetchall() + self.assertListOfListsEqual(delete_res, [[1]]) + conn.commit() + + # validate deleted + cur.execute("SELECT a, b FROM {0} WHERE a = 5".format(self._table)) + res = cur.fetchall() + self.assertListOfListsEqual(res, []) + + def test_update(self): + with self._connect() as conn: + cur = conn.cursor() + + cur.execute("INSERT INTO {0} (a, b) VALUES (5, 'cc')".format(self._table)) + # validate insert count + insert_res = cur.fetchall() + self.assertListOfListsEqual(insert_res, [[1]], msg='Bad INSERT response') + conn.commit() + + cur.execute("UPDATE {0} SET b = 'ff' WHERE a = 5".format(self._table)) + # validate update count + assert cur.rowcount == -1 + update_res = cur.fetchall() + self.assertListOfListsEqual(update_res, [[1]], msg='Bad UPDATE response') + conn.commit() + + cur.execute("SELECT a, b FROM {0} WHERE a = 5".format(self._table)) + res = cur.fetchall() + self.assertListOfListsEqual(res, [[5, 'ff']]) + + def test_copy_null(self): + with self._connect() as conn: + cur = conn.cursor() + cur.copy("COPY {0} (a, b) FROM STDIN DELIMITER ','".format(self._table), + "1,\n,foo") + cur.execute("SELECT a, b FROM {0} ORDER BY a ASC".format(self._table)) + res = cur.fetchall() + self.assertListOfListsEqual(res, [[None, 'foo'], [1, None]]) + + def test_copy_with_string(self): + with self._connect() as conn1, self._connect() as conn2: + cur1 = conn1.cursor() + cur2 = conn2.cursor() + + cur1.copy("COPY {0} (a, b) FROM STDIN DELIMITER ','".format(self._table), + "1,foo\n2,bar") + # no commit necessary for copy + cur1.execute("SELECT a, b FROM {0} WHERE a = 1".format(self._table)) + res_from_cur1 = cur1.fetchall() + self.assertListOfListsEqual(res_from_cur1, [[1, 'foo']]) + + cur2.execute("SELECT a, b FROM {0} WHERE a = 2".format(self._table)) + res_from_cur2 = cur2.fetchall() + self.assertListOfListsEqual(res_from_cur2, [[2, 'bar']]) + + def test_copy_with_file(self): + with tempfile.TemporaryFile() as tmpfile, self._connect() as conn1, self._connect() as conn2: + if _os.name != 'posix' or _os.sys.platform == 'cygwin': + f = getattr(tmpfile, 'file') + else: + f = tmpfile + + f.write(b"1,foo\n2,bar") + # move rw pointer to top of file + f.seek(0) + + cur1 = conn1.cursor() + cur2 = conn2.cursor() + + cur1.copy("COPY {0} (a, b) FROM STDIN DELIMITER ','".format(self._table), + f) + # no commit necessary for copy + cur1.execute("SELECT a, b FROM {0} WHERE a = 1".format(self._table)) + res_from_cur1 = cur1.fetchall() + self.assertListOfListsEqual(res_from_cur1, [[1, 'foo']]) + + cur2.execute("SELECT a, b FROM {0} WHERE a = 2".format(self._table)) + res_from_cur2 = cur2.fetchall() + self.assertListOfListsEqual(res_from_cur2, [[2, 'bar']]) + + # unit test for #78 + def test_copy_with_data_in_buffer(self): + with self._connect() as conn: + cur = conn.cursor() + + cur.execute("SELECT 1;") + res = cur.fetchall() + self.assertListOfListsEqual(res, [[1]]) + + cur.copy("COPY {0} (a, b) FROM STDIN DELIMITER ','".format(self._table), + "1,foo\n2,bar") + + cur.execute("SELECT 1;") + res = cur.fetchall() + self.assertListOfListsEqual(res, [[1]]) + + # unit test for #213 + def test_cmd_after_invalid_copy_stmt(self): + with self._connect() as conn: + cur = conn.cursor() + + cur.execute("SELECT 1;") + res = cur.fetchall() + self.assertListOfListsEqual(res, [[1]]) + + res = [[]] + try: + cur.copy("COPY non_existing_tab(a, b) FROM STDIN DELIMITER ','", "FAIL") + except errors.Error as e: + cur.execute("SELECT 1;") + res = cur.fetchall() + + self.assertListOfListsEqual(res, [[1]]) + + # unit test for #213 + def test_cmd_after_rejected_copy_data(self): + with self._connect() as conn: + cur = conn.cursor() + + cur.execute("SELECT 1;") + res = cur.fetchall() + self.assertListOfListsEqual(res, [[1]]) + + res = [[]] + try: + cur.copy("COPY {0} (a, b) FROM STDIN DELIMITER ',' ABORT ON ERROR".format(self._table), + "FAIL") + except errors.Error as e: + cur.execute("SELECT 1;") + res = cur.fetchall() + + self.assertListOfListsEqual(res, [[1]]) + + def test_with_conn(self): + with self._connect() as conn: + cur = conn.cursor() + + cur.execute("INSERT INTO {0} (a, b) VALUES (1, 'aa'); COMMIT;".format(self._table)) + cur.execute("SELECT a, b FROM {0} WHERE a = 1".format(self._table)) + res = cur.fetchall() + self.assertListOfListsEqual(res, [[1, 'aa']]) + + def test_iterator(self): + with self._connect() as conn: + cur = conn.cursor() + values = [[1, 'aa'], [2, 'bb'], [3, 'cc']] + + for n, s in values: + cur.execute("INSERT INTO {0} (a, b) VALUES (:n, :s)".format(self._table), + {'n': n, 's': s}) + conn.commit() + + cur.execute("SELECT a, b FROM {0} ORDER BY a ASC".format(self._table)) + + for val, res in zip(sorted(values), cur.iterate()): + self.assertListEqual(res, val) + + remaining = cur.fetchall() + self.assertListOfListsEqual(remaining, []) + + def test_mid_iterator_execution(self): + with self._connect() as conn: + cur = conn.cursor() + values = [[1, 'aa'], [2, 'bb'], [3, 'cc']] + + for n, s in values: + cur.execute("INSERT INTO {0} (a, b) VALUES (:n, :s)".format(self._table), + {'n': n, 's': s}) + conn.commit() + + cur.execute("SELECT a, b FROM {0} ORDER BY a ASC".format(self._table)) + + for val, res in zip(sorted(values), cur.iterate()): + self.assertListEqual(res, val) + break # stop after one comparison + + # make new query and verify result + cur.execute("SELECT COUNT(*) FROM {0}".format(self._table)) + res = cur.fetchall() + self.assertListOfListsEqual(res, [[3]]) + + def test_query_errors(self): + with self._connect() as conn: + cur = conn.cursor() + + # create table syntax error + with self.assertRaises(errors.VerticaSyntaxError): + cur.execute("""CREATE TABLE {0}_fail ( + a INT, + b VARCHAR(32),,, + ); + """.format(self._table)) + + # select table not found error + cur.execute("INSERT INTO {0} (a, b) VALUES (1, 'aa'); COMMIT;".format(self._table)) + with self.assertRaises(errors.QueryError): + cur.execute("SELECT * FROM {0}_fail".format(self._table)) + + # verify cursor still usable after errors + cur.execute("SELECT a, b FROM {0} WHERE a = 1".format(self._table)) + res = cur.fetchall() + self.assertListOfListsEqual(res, [[1, 'aa']]) + + def test_cursor_close_and_reuse(self): + with self._connect() as conn: + cur = conn.cursor() + + # insert data + cur.execute("INSERT INTO {0} (a, b) VALUES (2, 'bb'); COMMIT;".format(self._table)) + + # (query -> close -> reopen) * 3 times + for _ in range(3): + cur.execute("SELECT a, b FROM {0} WHERE a = 2".format(self._table)) + res = cur.fetchall() + self.assertListOfListsEqual(res, [[2, 'bb']]) + + # close and reopen cursor + cur.close() + cur = conn.cursor() + + # unit test for #74 + def test_nextset(self): + with self._connect() as conn: + cur = conn.cursor() + + cur.execute("SELECT 1; SELECT 2;") + + res1 = cur.fetchall() + self.assertListOfListsEqual(res1, [[1]]) + self.assertIsNone(cur.fetchone()) + self.assertTrue(cur.nextset()) + + res2 = cur.fetchall() + self.assertListOfListsEqual(res2, [[2]]) + self.assertIsNone(cur.fetchone()) + self.assertFalse(cur.nextset()) + + # unit test for #74 + def test_nextset_with_delete(self): + with self._connect() as conn: + cur = conn.cursor() + + # insert data + cur.execute("INSERT INTO {0} (a, b) VALUES (1, 'aa')".format(self._table)) + cur.execute("INSERT INTO {0} (a, b) VALUES (2, 'bb')".format(self._table)) + conn.commit() + + cur.execute(""" + SELECT * FROM {0} ORDER BY a ASC; + DELETE FROM {0}; + SELECT * FROM {0} ORDER BY a ASC; + """.format(self._table)) + + # check first select results + res1 = cur.fetchall() + self.assertListOfListsEqual(res1, [[1, 'aa'], [2, 'bb']]) + self.assertIsNone(cur.fetchone()) + self.assertTrue(cur.nextset()) + + # check delete results + res2 = cur.fetchall() + self.assertListOfListsEqual(res2, [[2]]) + self.assertIsNone(cur.fetchone()) + self.assertTrue(cur.nextset()) + + # check second select results + res3 = cur.fetchall() + self.assertListOfListsEqual(res3, []) + self.assertIsNone(cur.fetchone()) + self.assertFalse(cur.nextset()) + + # unit test for #124 + def test_nextset_with_error(self): + with self._connect() as conn: + cur = conn.cursor() + + cur.execute("SELECT 1; SELECT a; SELECT 2") + + # verify data from first query + res1 = cur.fetchall() + self.assertListOfListsEqual(res1, [[1]]) + self.assertIsNone(cur.fetchone()) + + # second statement results in a query error + with self.assertRaises(errors.MissingColumn): + cur.nextset() + + # unit test for #144 + def test_empty_query(self): + with self._connect() as conn: + cur = conn.cursor() + + cur.execute("") + res = cur.fetchall() + self.assertListOfListsEqual(res, []) + + def test_format_quote_unicode(self): + with self._connect() as conn: + cur = conn.cursor() + bad_word = u'Fr\xfchst\xfcck' + formatted_word = u''.join((u'"', re.escape(bad_word), u'"')) + self.assertEqual(formatted_word, cur.format_quote(bad_word, True)) + + # unit test for #175 + def test_datetime_types(self): + with self._connect() as conn: + cur = conn.cursor() + + # clean old table + cur.execute("DROP TABLE IF EXISTS {0}".format(self._table)) + + # create test table + cur.execute("""CREATE TABLE {0} ( + a INT, + b VARCHAR(32), + c TIMESTAMP, + d DATE, + e TIME + ) + """.format(self._table)) + + cur.execute("INSERT INTO {0} (a, b, c, d, e) VALUES (:n, :s, :dt, :d, :t)".format(self._table), + {'n': 10, 's': 'aa', + 'dt': datetime.datetime(2018, 9, 7, 15, 38, 19, 769000), + 'd': datetime.date(2018, 9, 7), + 't': datetime.time(13, 50, 9)}) + conn.commit() + + cur.execute("SELECT a, b, c, d, e FROM {0}".format(self._table)) + res = cur.fetchall() + self.assertListOfListsEqual(res, [[10, 'aa', datetime.datetime(2018, 9, 7, 15, 38, 19, 769000), + datetime.date(2018, 9, 7), datetime.time(13, 50, 9)]]) + + # clean up + cur.execute("DROP TABLE IF EXISTS {0}".format(self._table)) + + +class ExecutemanyTestCase(VerticaPythonIntegrationTestCase): + def setUp(self): + super(ExecutemanyTestCase, self).setUp() + self._table = 'executemany_test' + self._init_table() + + def _init_table(self): + with self._connect() as conn: + cur = conn.cursor() + # clean old table + cur.execute("DROP TABLE IF EXISTS {0}".format(self._table)) + + # create test table + cur.execute("""CREATE TABLE {0} ( + a INT, + b VARCHAR(32) + ) + """.format(self._table)) + + def tearDown(self): + with self._connect() as conn: + cur = conn.cursor() + cur.execute("DROP TABLE IF EXISTS {0}".format(self._table)) + super(ExecutemanyTestCase, self).tearDown() + + def _test_executemany(self, table, seq_of_values): + with self._connect() as conn: + cur = conn.cursor() + + cur.executemany("INSERT INTO {0} (a, b) VALUES (%s, %s)".format(table), + seq_of_values) + conn.commit() + + cur.execute("SELECT * FROM {0} ORDER BY a ASC, b ASC".format(table)) + + # check first select results + res1 = cur.fetchall() + seq_of_values_to_compare = sorted([list(values) for values in seq_of_values]) + self.assertListOfListsEqual(res1, seq_of_values_to_compare) + self.assertIsNone(cur.fetchone()) + + def test_executemany(self): + self._test_executemany(self._table, [(1, 'aa'), (2, 'bb')]) + + def test_executemany_quoted_path(self): + table = '.'.join(['"{}"'.format(s.strip('"')) for s in self._table.split('.')]) + self._test_executemany(table, [(1, 'aa'), (2, 'bb')]) + + def test_executemany_utf8(self): + self._test_executemany(self._table, [(1, u'a\xfc'), (2, u'bb')]) + + def test_executemany_null(self): + seq_of_values_1 = ((None, 'foo'), [2, None]) + seq_of_values_2 = ({'a': None, 'b': 'bar'}, {'a': 4, 'b': None}) + seq_of_values_to_compare = [[None, 'bar'], [None, 'foo'], [2, None], [4, None]] + with self._connect() as conn: + cur = conn.cursor() + + cur.executemany("INSERT INTO {0} (a, b) VALUES (%s, %s)".format(self._table), + seq_of_values_1) + conn.commit() + cur.executemany("INSERT INTO {0} (a, b) VALUES (:a, :b)".format(self._table), + seq_of_values_2) + conn.commit() + + cur.execute("SELECT * FROM {0} ORDER BY a ASC, b ASC".format(self._table)) + res = cur.fetchall() + self.assertListOfListsEqual(res, seq_of_values_to_compare) + self.assertIsNone(cur.fetchone()) diff -Nru python-vertica-0.7.4/vertica_python/tests/integration_tests/test_datatypes.py python-vertica-0.8.0/vertica_python/tests/integration_tests/test_datatypes.py --- python-vertica-0.7.4/vertica_python/tests/integration_tests/test_datatypes.py 1970-01-01 00:00:00.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/integration_tests/test_datatypes.py 2018-09-26 20:21:00.000000000 +0000 @@ -0,0 +1,60 @@ +# Copyright (c) 2018 Micro Focus or one of its affiliates. +# Copyright (c) 2018 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright (c) 2013-2017 Uber Technologies, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import print_function, division, absolute_import + +from decimal import Decimal + +from .base import VerticaPythonIntegrationTestCase + + +class TypeTestCase(VerticaPythonIntegrationTestCase): + def test_decimal_query(self): + value = Decimal(0.42) + query = "SELECT {0}::numeric".format(value) + res = self._query_and_fetchone(query) + self.assertAlmostEqual(res[0], value) + + def test_boolean_query__true(self): + value = True + query = "SELECT {0}::boolean".format(value) + res = self._query_and_fetchone(query) + self.assertEqual(res[0], value) + + def test_boolean_query__false(self): + value = False + query = "SELECT {0}::boolean".format(value) + res = self._query_and_fetchone(query) + self.assertEqual(res[0], value) diff -Nru python-vertica-0.7.4/vertica_python/tests/integration_tests/test_dates.py python-vertica-0.8.0/vertica_python/tests/integration_tests/test_dates.py --- python-vertica-0.7.4/vertica_python/tests/integration_tests/test_dates.py 1970-01-01 00:00:00.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/integration_tests/test_dates.py 2018-09-26 20:21:00.000000000 +0000 @@ -0,0 +1,154 @@ +# Copyright (c) 2018 Micro Focus or one of its affiliates. +# Copyright (c) 2018 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright (c) 2013-2017 Uber Technologies, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import print_function, division, absolute_import + +from collections import namedtuple +from datetime import date + +from .base import VerticaPythonIntegrationTestCase +from ... import errors + +DateTestingCase = namedtuple("DateTestingCase", ["string", "template", "date"]) + + +class DateParsingTestCase(VerticaPythonIntegrationTestCase): + """Testing DATE type parsing with focus on 'AD'/'BC'. + + Note: the 'BC' or 'AD' era indicators in Vertica's date format seem to make Vertica behave as + follows: + 1. Both 'BC' and 'AD' are simply a flags that tell Vertica: include era indicator if the + date is Before Christ + 2. Dates in 'AD' will never include era indicator + """ + + def _test_dates(self, test_cases, msg=None): + with self._connect() as conn: + cur = conn.cursor() + for tc in test_cases: + cur.execute("SELECT TO_DATE('{0}', '{1}')".format(tc.string, tc.template)) + res = cur.fetchall() + self.assertListOfListsEqual(res, [[tc.date]], msg=msg) + + def _test_not_supported(self, test_cases, msg=None): + with self._connect() as conn: + cur = conn.cursor() + for tc in test_cases: + with self.assertRaises(errors.NotSupportedError, msg=msg): + cur.execute("SELECT TO_DATE('{0}', '{1}')".format(tc.string, tc.template)) + res = cur.fetchall() + self.assertListOfListsEqual(res, [[tc.date]]) + + def test_no_to_no(self): + test_cases = [ + DateTestingCase('1985-10-25', 'YYYY-MM-DD', date(1985, 10, 25)), + DateTestingCase('1955-11-12', 'YYYY-MM-DD', date(1955, 11, 12)), + DateTestingCase('1885-01-01', 'YYYY-MM-DD', date(1885, 1, 1)), + DateTestingCase('2015-10-21', 'YYYY-MM-DD', date(2015, 10, 21)), + ] + self._test_dates(test_cases=test_cases, msg='no indicator -> no indicator') + + def test_ad_to_no(self): + test_cases = [ + DateTestingCase('1985-10-25 AD', 'YYYY-MM-DD', date(1985, 10, 25)), + DateTestingCase('1955-11-12 AD', 'YYYY-MM-DD', date(1955, 11, 12)), + DateTestingCase('1885-01-01 AD', 'YYYY-MM-DD', date(1885, 1, 1)), + DateTestingCase('2015-10-21 AD', 'YYYY-MM-DD', date(2015, 10, 21)), + ] + self._test_dates(test_cases=test_cases, msg='AD indicator -> no indicator') + + def test_bc_to_no(self): + test_cases = [ + DateTestingCase('1985-10-25 BC', 'YYYY-MM-DD', date(1985, 10, 25)), + DateTestingCase('1955-11-12 BC', 'YYYY-MM-DD', date(1955, 11, 12)), + DateTestingCase('1885-01-01 BC', 'YYYY-MM-DD', date(1885, 1, 1)), + DateTestingCase('2015-10-21 BC', 'YYYY-MM-DD', date(2015, 10, 21)), + ] + self._test_dates(test_cases=test_cases, msg='BC indicator -> no indicator') + + def test_no_to_ad(self): + test_cases = [ + DateTestingCase('1985-10-25', 'YYYY-MM-DD AD', date(1985, 10, 25)), + DateTestingCase('1955-11-12', 'YYYY-MM-DD AD', date(1955, 11, 12)), + DateTestingCase('1885-01-01', 'YYYY-MM-DD AD', date(1885, 1, 1)), + DateTestingCase('2015-10-21', 'YYYY-MM-DD AD', date(2015, 10, 21)), + ] + self._test_dates(test_cases=test_cases, msg='no indicator -> AD indicator') + + def test_ad_to_ad(self): + test_cases = [ + DateTestingCase('1985-10-25 AD', 'YYYY-MM-DD AD', date(1985, 10, 25)), + DateTestingCase('1955-11-12 AD', 'YYYY-MM-DD AD', date(1955, 11, 12)), + DateTestingCase('1885-01-01 AD', 'YYYY-MM-DD AD', date(1885, 1, 1)), + DateTestingCase('2015-10-21 AD', 'YYYY-MM-DD AD', date(2015, 10, 21)), + ] + self._test_dates(test_cases=test_cases, msg='AD indicator -> AD indicator') + + def test_bc_to_ad(self): + test_cases = [ + DateTestingCase('1985-10-25 BC', 'YYYY-MM-DD AD', date(1985, 10, 25)), + DateTestingCase('1955-11-12 BC', 'YYYY-MM-DD AD', date(1955, 11, 12)), + DateTestingCase('1885-01-01 BC', 'YYYY-MM-DD AD', date(1885, 1, 1)), + DateTestingCase('2015-10-21 BC', 'YYYY-MM-DD AD', date(2015, 10, 21)), + ] + self._test_not_supported(test_cases=test_cases, msg='BC indicator -> AD indicator') + + def test_no_to_bc(self): + test_cases = [ + DateTestingCase('1985-10-25', 'YYYY-MM-DD BC', date(1985, 10, 25)), + DateTestingCase('1955-11-12', 'YYYY-MM-DD BC', date(1955, 11, 12)), + DateTestingCase('1885-01-01', 'YYYY-MM-DD BC', date(1885, 1, 1)), + DateTestingCase('2015-10-21', 'YYYY-MM-DD BC', date(2015, 10, 21)), + ] + self._test_dates(test_cases=test_cases, msg='no indicator -> BC indicator') + + def test_ad_to_bc(self): + test_cases = [ + DateTestingCase('1985-10-25 AD', 'YYYY-MM-DD BC', date(1985, 10, 25)), + DateTestingCase('1955-11-12 AD', 'YYYY-MM-DD BC', date(1955, 11, 12)), + DateTestingCase('1885-01-01 AD', 'YYYY-MM-DD BC', date(1885, 1, 1)), + DateTestingCase('2015-10-21 AD', 'YYYY-MM-DD BC', date(2015, 10, 21)), + ] + self._test_dates(test_cases=test_cases, msg='AD indicator -> BC indicator') + + def test_bc_to_bc(self): + test_cases = [ + DateTestingCase('1985-10-25 BC', 'YYYY-MM-DD BC', date(1985, 10, 25)), + DateTestingCase('1955-11-12 BC', 'YYYY-MM-DD BC', date(1955, 11, 12)), + DateTestingCase('1885-01-01 BC', 'YYYY-MM-DD BC', date(1885, 1, 1)), + DateTestingCase('2015-10-21 BC', 'YYYY-MM-DD BC', date(2015, 10, 21)), + ] + self._test_not_supported(test_cases=test_cases, msg='BC indicator -> BC indicator') + diff -Nru python-vertica-0.7.4/vertica_python/tests/integration_tests/test_errors.py python-vertica-0.8.0/vertica_python/tests/integration_tests/test_errors.py --- python-vertica-0.7.4/vertica_python/tests/integration_tests/test_errors.py 1970-01-01 00:00:00.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/integration_tests/test_errors.py 2018-09-26 20:21:00.000000000 +0000 @@ -0,0 +1,64 @@ +# Copyright (c) 2018 Micro Focus or one of its affiliates. +# Copyright (c) 2018 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright (c) 2013-2017 Uber Technologies, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import print_function, division, absolute_import + +from .base import VerticaPythonIntegrationTestCase + +from ... import errors + + +class ErrorTestCase(VerticaPythonIntegrationTestCase): + def test_missing_schema(self): + with self._connect() as conn: + cur = conn.cursor() + with self.assertRaises(errors.MissingSchema): + cur.execute("SELECT 1 FROM missing_schema.table") + + def test_missing_relation(self): + with self._connect() as conn: + cur = conn.cursor() + with self.assertRaises(errors.MissingRelation): + cur.execute("SELECT 1 FROM missing_table") + + def test_duplicate_object(self): + with self._connect() as conn: + cur = conn.cursor() + cur.execute("DROP TABLE IF EXISTS duplicate_table") + query = "CREATE TABLE duplicate_table (a BOOLEAN)" + cur.execute(query) + with self.assertRaises(errors.DuplicateObject): + cur.execute(query) + cur.execute("DROP TABLE IF EXISTS duplicate_table") diff -Nru python-vertica-0.7.4/vertica_python/tests/integration_tests/test_loadbalance.py python-vertica-0.8.0/vertica_python/tests/integration_tests/test_loadbalance.py --- python-vertica-0.7.4/vertica_python/tests/integration_tests/test_loadbalance.py 1970-01-01 00:00:00.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/integration_tests/test_loadbalance.py 2018-09-26 20:21:00.000000000 +0000 @@ -0,0 +1,311 @@ +# Copyright (c) 2018 Micro Focus or one of its affiliates. +# Copyright (c) 2018 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright (c) 2013-2017 Uber Technologies, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import print_function, division, absolute_import + +from .base import VerticaPythonIntegrationTestCase + +class LoadBalanceTestCase(VerticaPythonIntegrationTestCase): + def setUp(self): + super(LoadBalanceTestCase, self).setUp() + self._host, self._port = self.test_config['host'], self.test_config['port'] + + + def tearDown(self): + self._conn_info['host'] = self._host + self._conn_info['port'] = self._port + self._conn_info['connection_load_balance'] = False + self._conn_info['backup_server_node'] = [] + + with self._connect() as conn: + cur = conn.cursor() + cur.execute("SELECT set_load_balance_policy('NONE')") + cur.execute("DROP TABLE IF EXISTS test_loadbalance") + super(LoadBalanceTestCase, self).tearDown() + + def get_node_num(self): + with self._connect() as conn: + cur = conn.cursor() + cur.execute("SELECT count(*) FROM nodes WHERE node_state='UP'") + db_node_num = cur.fetchone()[0] + return db_node_num + + def test_loadbalance_option_disabled(self): + if 'connection_load_balance' in self._conn_info: + del self._conn_info['connection_load_balance'] + self.assertConnectionSuccess() + + self._conn_info['connection_load_balance'] = False + self.assertConnectionSuccess() + + def test_loadbalance_random(self): + self.require_DB_nodes_at_least(3) + self._conn_info['connection_load_balance'] = True + rowsToInsert = 3 * self.db_node_num + + with self._connect() as conn: + cur = conn.cursor() + cur.execute("SELECT set_load_balance_policy('RANDOM')") + cur.execute("DROP TABLE IF EXISTS test_loadbalance") + cur.execute("CREATE TABLE test_loadbalance (n varchar)") + # record which node the client has connected to + for i in range(rowsToInsert): + with self._connect() as conn1: + cur1 = conn1.cursor() + cur1.execute("INSERT INTO test_loadbalance (SELECT node_name FROM sessions " + "WHERE session_id = (SELECT current_session()))") + + cur.execute("SELECT count(DISTINCT n)>1 FROM test_loadbalance") + res = cur.fetchone() + self.assertTrue(res[0]) + + def test_loadbalance_none(self): + # Client turns on connection_load_balance but server is unsupported + with self._connect() as conn: + cur = conn.cursor() + cur.execute("SELECT set_load_balance_policy('NONE')") + self._conn_info['connection_load_balance'] = True + + # Client will proceed with the existing connection with initiator + self.assertConnectionSuccess() + + # Test for multi-node DB + self.require_DB_nodes_at_least(3) + rowsToInsert = 3 * self.db_node_num + + with self._connect() as conn: + cur = conn.cursor() + cur.execute("DROP TABLE IF EXISTS test_loadbalance") + cur.execute("CREATE TABLE test_loadbalance (n varchar)") + # record which node the client has connected to + for i in range(rowsToInsert): + with self._connect() as conn1: + cur1 = conn1.cursor() + cur1.execute("INSERT INTO test_loadbalance (SELECT node_name FROM sessions " + "WHERE session_id = (SELECT current_session()))") + + cur.execute("SELECT count(DISTINCT n)=1 FROM test_loadbalance") + res = cur.fetchone() + self.assertTrue(res[0]) + + def test_loadbalance_roundrobin(self): + self.require_DB_nodes_at_least(3) + self._conn_info['connection_load_balance'] = True + rowsToInsert = 3 * self.db_node_num + + with self._connect() as conn: + cur = conn.cursor() + cur.execute("SELECT set_load_balance_policy('ROUNDROBIN')") + cur.execute("DROP TABLE IF EXISTS test_loadbalance") + cur.execute("CREATE TABLE test_loadbalance (n varchar)") + # record which node the client has connected to + for i in range(rowsToInsert): + with self._connect() as conn1: + cur1 = conn1.cursor() + cur1.execute("INSERT INTO test_loadbalance (SELECT node_name FROM sessions " + "WHERE session_id = (SELECT current_session()))") + + cur.execute("SELECT count(n)=3 FROM test_loadbalance GROUP BY n") + res = cur.fetchall() + # verify that all db_node_num nodes are represented equally + self.assertEqual(len(res), self.db_node_num) + for i in res: + self.assertEqual(i, [True]) + + def test_failover_empty_backup(self): + # Connect to primary server + if 'backup_server_node' in self._conn_info: + del self._conn_info['backup_server_node'] + self.assertConnectionSuccess() + self._conn_info['backup_server_node'] = [] + self.assertConnectionSuccess() + + # Set primary server to invalid host and port + self._conn_info['host'] = 'invalidhost' + self._conn_info['port'] = 9999 + + # Fail to connect to primary server + self.assertConnectionFail() + + def test_failover_one_backup(self): + # Set primary server to invalid host and port + self._conn_info['host'] = 'invalidhost' + self._conn_info['port'] = 9999 + + # One valid address in backup_server_node: port is an integer + self._conn_info['backup_server_node'] = [(self._host, self._port)] + self.assertConnectionSuccess() + + # One valid address in backup_server_node: port is a string + self._conn_info['backup_server_node'] = [(self._host, str(self._port))] + self.assertConnectionSuccess() + + # One invalid address in backup_server_node: DNS failed, Name or service not known + self._conn_info['backup_server_node'] = [('invalidhost2', 8888)] + self.assertConnectionFail() + + # One invalid address in backup_server_node: DNS failed, Name or service not known + self._conn_info['backup_server_node'] = [('123.456.789.123', 8888)] + self.assertConnectionFail() + + # One invalid address in backup_server_node: DNS failed, Address family for hostname not supported + self._conn_info['backup_server_node'] = [('fd76:6572:7469:6361:0:242:ac11:4', 8888)] + self.assertConnectionFail() + + # One invalid address in backup_server_node: Wrong port, connection refused + self._conn_info['backup_server_node'] = [(self._host, 8888)] + self.assertConnectionFail() + + def test_failover_multi_backup(self): + # Set primary server to invalid host and port + self._conn_info['host'] = 'invalidhost' + self._conn_info['port'] = 9999 + + # One valid and two invalid addresses in backup_server_node + self._conn_info['backup_server_node'] = [(self._host, self._port), 'invalidhost2','foo'] + self.assertConnectionSuccess() + self._conn_info['backup_server_node'] = ['foo', (self._host, self._port), ('123.456.789.1', 888)] + self.assertConnectionSuccess() + self._conn_info['backup_server_node'] = ['foo', ('invalidhost2', 8888), (self._host, self._port)] + self.assertConnectionSuccess() + + # Three invalid addresses in backup_server_node + self._conn_info['backup_server_node'] = ['foo', (self._host, 9999), ('123.456.789.1', 888)] + self.assertConnectionFail() + + def test_failover_backup_format(self): + # Set primary server to invalid host and port + self._conn_info['host'] = 'invalidhost' + self._conn_info['port'] = 9999 + + err_msg = 'Connection option "backup_server_node" must be a list' + with self.assertRaisesRegexp(TypeError, err_msg): + self._conn_info['backup_server_node'] = (self._host, self._port) + with self._connect() as conn: + pass + + err_msg = ('Each item of connection option "backup_server_node"' + ' must be a host string or a \(host, port\) tuple') + with self.assertRaisesRegexp(TypeError, err_msg): + self._conn_info['backup_server_node'] = [9999] + with self._connect() as conn: + pass + + with self.assertRaisesRegexp(TypeError, err_msg): + self._conn_info['backup_server_node'] = [(self._host, self._port, 'foo', 9999)] + with self._connect() as conn: + pass + + err_msg = 'Host must be a string: invalid value: .*' + with self.assertRaisesRegexp(TypeError, err_msg): + self._conn_info['backup_server_node'] = [(9999, self._port)] + with self._connect() as conn: + pass + + with self.assertRaisesRegexp(TypeError, err_msg): + self._conn_info['backup_server_node'] = [(9999, 'port_num')] + with self._connect() as conn: + pass + + err_msg = 'Port must be an integer or a string: invalid value: .*' + with self.assertRaisesRegexp(TypeError, err_msg): + self._conn_info['backup_server_node'] = [(self._host, 5433.0022)] + with self._connect() as conn: + pass + + err_msg = 'Port .* is not a valid string: invalid literal for int\(\) with base 10: .*' + with self.assertRaisesRegexp(ValueError, err_msg): + self._conn_info['backup_server_node'] = [(self._host, 'port_num')] + with self._connect() as conn: + pass + + with self.assertRaisesRegexp(ValueError, err_msg): + self._conn_info['backup_server_node'] = [(self._host, '5433.0022')] + with self._connect() as conn: + pass + + err_msg = 'Invalid port number: .*' + with self.assertRaisesRegexp(ValueError, err_msg): + self._conn_info['backup_server_node'] = [(self._host, -1000)] + with self._connect() as conn: + pass + + with self.assertRaisesRegexp(ValueError, err_msg): + self._conn_info['backup_server_node'] = [(self._host, 66000)] + with self._connect() as conn: + pass + + with self.assertRaisesRegexp(ValueError, err_msg): + self._conn_info['backup_server_node'] = [(self._host, '-1000')] + with self._connect() as conn: + pass + + with self.assertRaisesRegexp(ValueError, err_msg): + self._conn_info['backup_server_node'] = [(self._host, '66000')] + with self._connect() as conn: + pass + + def test_failover_with_loadbalance_roundrobin(self): + self.require_DB_nodes_at_least(3) + + # Set primary server to invalid host and port + self._conn_info['host'] = 'invalidhost' + self._conn_info['port'] = 9999 + self.assertConnectionFail() + + self._conn_info['backup_server_node'] = [('invalidhost2', 8888), (self._host, self._port)] + self.assertConnectionSuccess() + + self._conn_info['connection_load_balance'] = True + rowsToInsert = 3 * self.db_node_num + + with self._connect() as conn: + cur = conn.cursor() + cur.execute("SELECT set_load_balance_policy('ROUNDROBIN')") + cur.execute("DROP TABLE IF EXISTS test_loadbalance") + cur.execute("CREATE TABLE test_loadbalance (n varchar)") + # record which node the client has connected to + for i in range(rowsToInsert): + with self._connect() as conn1: + cur1 = conn1.cursor() + cur1.execute("INSERT INTO test_loadbalance (SELECT node_name FROM sessions " + "WHERE session_id = (SELECT current_session()))") + + cur.execute("SELECT count(n)=3 FROM test_loadbalance GROUP BY n") + res = cur.fetchall() + # verify that all db_node_num nodes are represented equally + self.assertEqual(len(res), self.db_node_num) + for i in res: + self.assertEqual(i, [True]) diff -Nru python-vertica-0.7.4/vertica_python/tests/integration_tests/test_timezones.py python-vertica-0.8.0/vertica_python/tests/integration_tests/test_timezones.py --- python-vertica-0.7.4/vertica_python/tests/integration_tests/test_timezones.py 1970-01-01 00:00:00.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/integration_tests/test_timezones.py 2018-09-26 20:21:00.000000000 +0000 @@ -0,0 +1,87 @@ +# Copyright (c) 2018 Micro Focus or one of its affiliates. +# Copyright (c) 2018 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright (c) 2013-2017 Uber Technologies, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import print_function, division, absolute_import + +from collections import namedtuple +from datetime import datetime +import pytz + +from .base import VerticaPythonIntegrationTestCase + +TimeZoneTestingCase = namedtuple("TimeZoneTestingCase", ["string", "template", "timestamp"]) + + +class TimeZoneTestCase(VerticaPythonIntegrationTestCase): + def _test_ts(self, test_cases): + with self._connect() as conn: + cur = conn.cursor() + for tc in test_cases: + cur.execute("SELECT TO_TIMESTAMP('{0}', '{1}')".format(tc.string, tc.template)) + res = cur.fetchone() + self.assertEqual(tc.timestamp.toordinal(), res[0].toordinal()) + + def test_simple_ts_query(self): + template = 'YYYY-MM-DD HH:MI:SS.MS' + test_cases = [ + TimeZoneTestingCase( + string='2016-05-15 13:15:17.789', template=template, + timestamp=datetime(year=2016, month=5, day=15, hour=13, minute=15, second=17, + microsecond=789000) + ), + ] + self._test_ts(test_cases=test_cases) + + def test_simple_ts_with_tz_query(self): + template = 'YYYY-MM-DD HH:MI:SS.MS TZ' + test_cases = [ + TimeZoneTestingCase( + string='2016-05-15 13:15:17.789 UTC', template=template, + timestamp=datetime(year=2016, month=5, day=15, hour=13, minute=15, second=17, + microsecond=789000, tzinfo=pytz.utc) + ), + ] + self._test_ts(test_cases=test_cases) + + def test_simple_ts_with_offset_query(self): + template = 'YYYY-MM-DD HH:MI:SS.MS+00' + test_cases = [ + TimeZoneTestingCase( + string='2016-05-15 13:15:17.789 UTC', template=template, + timestamp=datetime(year=2016, month=5, day=15, hour=13, minute=15, second=17, + microsecond=789000, tzinfo=pytz.utc) + ), + ] + self._test_ts(test_cases=test_cases) diff -Nru python-vertica-0.7.4/vertica_python/tests/integration_tests/test_unicode.py python-vertica-0.8.0/vertica_python/tests/integration_tests/test_unicode.py --- python-vertica-0.7.4/vertica_python/tests/integration_tests/test_unicode.py 1970-01-01 00:00:00.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/integration_tests/test_unicode.py 2018-09-26 20:21:00.000000000 +0000 @@ -0,0 +1,126 @@ +# Copyright (c) 2018 Micro Focus or one of its affiliates. +# Copyright (c) 2018 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright (c) 2013-2017 Uber Technologies, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import print_function, division, absolute_import + +from .base import VerticaPythonIntegrationTestCase + + +class UnicodeTestCase(VerticaPythonIntegrationTestCase): + def test_unicode_query(self): + value = u'\u16a0' + query = u"SELECT '{0}'".format(value) + + with self._connect() as conn: + cur = conn.cursor() + cur.execute(query) + res = cur.fetchone() + + self.assertResultEqual(value, res[0]) + + def test_unicode_list_parameter(self): + values = [u'\u00f1', 'foo', 3] + query = u"SELECT {0}".format(", ".join(["%s"] * len(values))) + + with self._connect() as conn: + cur = conn.cursor() + cur.execute(query, tuple(values)) + results = cur.fetchone() + + for val, res in zip(values, results): + self.assertResultEqual(val, res) + + def test_unicode_named_parameter_binding(self): + values = [u'\u16b1', 'foo', 3] + keys = [u'\u16a0', 'foo', 3] + + query = u"SELECT {0}".format(", ".join([u":{0}".format(key) for key in keys])) + + with self._connect() as conn: + cur = conn.cursor() + cur.execute(query, dict(zip(keys, values))) + results = cur.fetchone() + + for val, res in zip(values, results): + self.assertResultEqual(val, res) + + def test_string_query(self): + value = u'test' + query = u"SELECT '{0}'".format(value) + + with self._connect() as conn: + cur = conn.cursor() + cur.execute(query) + res = cur.fetchone() + + self.assertEqual(value, res[0]) + + def test_string_named_parameter_binding(self): + key = u'test' + value = u'value' + query = u"SELECT :{0}".format(key) + + with self._connect() as conn: + cur = conn.cursor() + cur.execute(query, {key: value}) + res = cur.fetchone() + + self.assertResultEqual(value, res[0]) + + # unit test for issue #160 + def test_null_named_parameter_binding(self): + key = u'test' + value = None + query = u"SELECT :{0}".format(key) + + with self._connect() as conn: + cur = conn.cursor() + cur.execute(query, {key: value}) + res = cur.fetchone() + + self.assertResultEqual(value, res[0]) + + # unit test for issue #160 + def test_null_list_parameter(self): + values = [u'\u00f1', 'foo', None] + query = u"SELECT {0}".format(", ".join(["%s"] * len(values))) + + with self._connect() as conn: + cur = conn.cursor() + cur.execute(query, tuple(values)) + results = cur.fetchone() + + for val, res in zip(values, results): + self.assertResultEqual(val, res) diff -Nru python-vertica-0.7.4/vertica_python/tests/test_column.py python-vertica-0.8.0/vertica_python/tests/test_column.py --- python-vertica-0.7.4/vertica_python/tests/test_column.py 2018-08-15 20:33:29.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/test_column.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,53 +0,0 @@ -# Copyright (c) 2018 Micro Focus or one of its affiliates. -# Copyright (c) 2018 Uber Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Copyright (c) 2013-2017 Uber Technologies, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from __future__ import print_function, division, absolute_import - -from .base import VerticaPythonTestCase - - -class ColumnTestCase(VerticaPythonTestCase): - def test_column_names_query(self): - columns = ['isocode', 'name'] - - with self._connect() as conn: - cur = conn.cursor() - cur.execute(""" - SELECT 'US' AS {0}, 'United States' AS {1} - UNION ALL SELECT 'CA', 'Canada' - UNION ALL SELECT 'MX', 'Mexico' """.format(*columns)) - description = cur.description - - self.assertListEqual([d.name for d in description], columns) diff -Nru python-vertica-0.7.4/vertica_python/tests/test_cursor.py python-vertica-0.8.0/vertica_python/tests/test_cursor.py --- python-vertica-0.7.4/vertica_python/tests/test_cursor.py 2018-08-15 20:33:29.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/test_cursor.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,438 +0,0 @@ -# Copyright (c) 2018 Micro Focus or one of its affiliates. -# Copyright (c) 2018 Uber Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Copyright (c) 2013-2017 Uber Technologies, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from __future__ import print_function, division, absolute_import - -import logging -import os as _os -import tempfile - -from .base import VerticaPythonTestCase -from .. import errors - -logger = logging.getLogger('vertica') - - -class CursorTestCase(VerticaPythonTestCase): - def setUp(self): - self._init_table() - - def tearDown(self): - # self._init_table() - pass - - def _init_table(self): - with self._connect() as conn: - cur = conn.cursor() - # clean old table - cur.execute("DROP TABLE IF EXISTS {0}".format(self._table)) - - # create test table - cur.execute("""CREATE TABLE {0} ( - a INT, - b VARCHAR(32) - ) - """.format(self._table)) - - def test_inline_commit(self): - with self._connect() as conn: - cur = conn.cursor() - cur.execute( - "INSERT INTO {0} (a, b) VALUES (1, 'aa'); COMMIT;".format(self._table)) - cur.execute("SELECT a, b FROM {0} WHERE a = 1".format(self._table)) - - # unknown rowcount - self.assertEqual(cur.rowcount, -1) - - res = cur.fetchall() - self.assertEqual(cur.rowcount, 1) - - self.assertListOfListsEqual(res, [[1, 'aa']]) - - def test_multi_inserts_and_transaction(self): - with self._connect() as conn1, self._connect() as conn2: - cur1 = conn1.cursor() - cur2 = conn2.cursor() - - # insert data without a commit - cur1.execute("INSERT INTO {0} (a, b) VALUES (2, 'bb')".format(self._table)) - - # verify we can see it from this cursor - cur1.execute("SELECT a, b FROM {0} WHERE a = 2".format(self._table)) - res_from_cur_1_before_commit = cur1.fetchall() - self.assertListOfListsEqual(res_from_cur_1_before_commit, [[2, 'bb']]) - - # verify we cant see it from other cursor - cur2.execute("SELECT a, b FROM {0} WHERE a = 2".format(self._table)) - - res_from_cur2_before_commit = cur2.fetchall() - self.assertListOfListsEqual(res_from_cur2_before_commit, []) - - # insert more data then commit - cur1.execute("INSERT INTO {0} (a, b) VALUES (3, 'cc')".format(self._table)) - cur1.execute("COMMIT") - - # verify we can see it from this cursor - cur1.execute( - "SELECT a, b FROM {0} WHERE a = 2 OR a = 3 ORDER BY a".format(self._table)) - res_from_cur1_after_commit = cur1.fetchall() - self.assertListOfListsEqual(res_from_cur1_after_commit, [[2, 'bb'], [3, 'cc']]) - - # verify we can see it from other cursor - cur2.execute( - "SELECT a, b FROM {0} WHERE a = 2 OR a = 3 ORDER BY a".format(self._table)) - res_from_cur2_after_commit = cur2.fetchall() - self.assertListOfListsEqual(res_from_cur2_after_commit, [[2, 'bb'], [3, 'cc']]) - - def test_conn_commit(self): - with self._connect() as conn: - cur = conn.cursor() - cur.execute("INSERT INTO {0} (a, b) VALUES (5, 'cc')".format(self._table)) - conn.commit() - - with self._connect() as conn: - cur = conn.cursor() - cur.execute("SELECT a, b FROM {0} WHERE a = 5".format(self._table)) - res = cur.fetchall() - - self.assertListOfListsEqual(res, [[5, 'cc']]) - - def test_delete(self): - with self._connect() as conn: - cur = conn.cursor() - - cur.execute("INSERT INTO {0} (a, b) VALUES (5, 'cc')".format(self._table)) - self.assertEqual(cur.rowcount, -1) - update_res = cur.fetchall() - self.assertListOfListsEqual(update_res, [[1]]) - conn.commit() - - # validate delete count - cur.execute("DELETE FROM {0} WHERE a = 5".format(self._table)) - self.assertEqual(cur.rowcount, -1) - delete_res = cur.fetchall() - self.assertListOfListsEqual(delete_res, [[1]]) - conn.commit() - - # validate deleted - cur.execute("SELECT a, b FROM {0} WHERE a = 5".format(self._table)) - res = cur.fetchall() - self.assertListOfListsEqual(res, []) - - def test_update(self): - with self._connect() as conn: - cur = conn.cursor() - - cur.execute("INSERT INTO {0} (a, b) VALUES (5, 'cc')".format(self._table)) - # validate insert count - insert_res = cur.fetchall() - self.assertListOfListsEqual(insert_res, [[1]], msg='Bad INSERT response') - conn.commit() - - cur.execute("UPDATE {0} SET b = 'ff' WHERE a = 5".format(self._table)) - # validate update count - assert cur.rowcount == -1 - update_res = cur.fetchall() - self.assertListOfListsEqual(update_res, [[1]], msg='Bad UPDATE response') - conn.commit() - - cur.execute("SELECT a, b FROM {0} WHERE a = 5".format(self._table)) - res = cur.fetchall() - self.assertListOfListsEqual(res, [[5, 'ff']]) - - def test_copy_with_string(self): - with self._connect() as conn1, self._connect() as conn2: - cur1 = conn1.cursor() - cur2 = conn2.cursor() - - cur1.copy("COPY {0} (a, b) FROM STDIN DELIMITER ','".format(self._table), - "1,foo\n2,bar") - # no commit necessary for copy - cur1.execute("SELECT a, b FROM {0} WHERE a = 1".format(self._table)) - res_from_cur1 = cur1.fetchall() - self.assertListOfListsEqual(res_from_cur1, [[1, 'foo']]) - - cur2.execute("SELECT a, b FROM {0} WHERE a = 2".format(self._table)) - res_from_cur2 = cur2.fetchall() - self.assertListOfListsEqual(res_from_cur2, [[2, 'bar']]) - - def test_copy_with_file(self): - with tempfile.TemporaryFile() as tmpfile, self._connect() as conn1, self._connect() as conn2: - if _os.name != 'posix' or _os.sys.platform == 'cygwin': - f = getattr(tmpfile, 'file') - else: - f = tmpfile - - f.write(b"1,foo\n2,bar") - # move rw pointer to top of file - f.seek(0) - - cur1 = conn1.cursor() - cur2 = conn2.cursor() - - cur1.copy("COPY {0} (a, b) FROM STDIN DELIMITER ','".format(self._table), - f) - # no commit necessary for copy - cur1.execute("SELECT a, b FROM {0} WHERE a = 1".format(self._table)) - res_from_cur1 = cur1.fetchall() - self.assertListOfListsEqual(res_from_cur1, [[1, 'foo']]) - - cur2.execute("SELECT a, b FROM {0} WHERE a = 2".format(self._table)) - res_from_cur2 = cur2.fetchall() - self.assertListOfListsEqual(res_from_cur2, [[2, 'bar']]) - - # unit test for #78 - def test_copy_with_data_in_buffer(self): - with self._connect() as conn: - cur = conn.cursor() - - cur.execute("SELECT 1;") - res = cur.fetchall() - self.assertListOfListsEqual(res, [[1]]) - - cur.copy("COPY {0} (a, b) FROM STDIN DELIMITER ','".format(self._table), - "1,foo\n2,bar") - - cur.execute("SELECT 1;") - res = cur.fetchall() - self.assertListOfListsEqual(res, [[1]]) - - def test_with_conn(self): - with self._connect() as conn: - cur = conn.cursor() - - cur.execute("INSERT INTO {0} (a, b) VALUES (1, 'aa'); COMMIT;".format(self._table)) - cur.execute("SELECT a, b FROM {0} WHERE a = 1".format(self._table)) - res = cur.fetchall() - self.assertListOfListsEqual(res, [[1, 'aa']]) - - def test_iterator(self): - with self._connect() as conn: - cur = conn.cursor() - values = [[1, 'aa'], [2, 'bb'], [3, 'cc']] - - for n, s in values: - cur.execute("INSERT INTO {0} (a, b) VALUES (:n, :s)".format(self._table), - {'n': n, 's': s}) - conn.commit() - - cur.execute("SELECT a, b FROM {0} ORDER BY a ASC".format(self._table)) - - for val, res in zip(sorted(values), cur.iterate()): - self.assertListEqual(res, val) - - remaining = cur.fetchall() - self.assertListOfListsEqual(remaining, []) - - def test_mid_iterator_execution(self): - with self._connect() as conn: - cur = conn.cursor() - values = [[1, 'aa'], [2, 'bb'], [3, 'cc']] - - for n, s in values: - cur.execute("INSERT INTO {0} (a, b) VALUES (:n, :s)".format(self._table), - {'n': n, 's': s}) - conn.commit() - - cur.execute("SELECT a, b FROM {0} ORDER BY a ASC".format(self._table)) - - for val, res in zip(sorted(values), cur.iterate()): - self.assertListEqual(res, val) - break # stop after one comparison - - # make new query and verify result - cur.execute("SELECT COUNT(*) FROM {0}".format(self._table)) - res = cur.fetchall() - self.assertListOfListsEqual(res, [[3]]) - - def test_query_errors(self): - with self._connect() as conn: - cur = conn.cursor() - - # create table syntax error - with self.assertRaises(errors.VerticaSyntaxError): - cur.execute("""CREATE TABLE {0}_fail ( - a INT, - b VARCHAR(32),,, - ); - """.format(self._table)) - - # select table not found error - cur.execute("INSERT INTO {0} (a, b) VALUES (1, 'aa'); COMMIT;".format(self._table)) - with self.assertRaises(errors.QueryError): - cur.execute("SELECT * FROM {0}_fail".format(self._table)) - - # verify cursor still usable after errors - cur.execute("SELECT a, b FROM {0} WHERE a = 1".format(self._table)) - res = cur.fetchall() - self.assertListOfListsEqual(res, [[1, 'aa']]) - - def test_cursor_close_and_reuse(self): - with self._connect() as conn: - cur = conn.cursor() - - # insert data - cur.execute("INSERT INTO {0} (a, b) VALUES (2, 'bb'); COMMIT;".format(self._table)) - - # (query -> close -> reopen) * 3 times - for _ in range(3): - cur.execute("SELECT a, b FROM {0} WHERE a = 2".format(self._table)) - res = cur.fetchall() - self.assertListOfListsEqual(res, [[2, 'bb']]) - - # close and reopen cursor - cur.close() - cur = conn.cursor() - - # unit test for #74 - def test_nextset(self): - with self._connect() as conn: - cur = conn.cursor() - - cur.execute("SELECT 1; SELECT 2;") - - res1 = cur.fetchall() - self.assertListOfListsEqual(res1, [[1]]) - self.assertIsNone(cur.fetchone()) - self.assertTrue(cur.nextset()) - - res2 = cur.fetchall() - self.assertListOfListsEqual(res2, [[2]]) - self.assertIsNone(cur.fetchone()) - self.assertFalse(cur.nextset()) - - # unit test for #74 - def test_nextset_with_delete(self): - with self._connect() as conn: - cur = conn.cursor() - - # insert data - cur.execute("INSERT INTO {0} (a, b) VALUES (1, 'aa')".format(self._table)) - cur.execute("INSERT INTO {0} (a, b) VALUES (2, 'bb')".format(self._table)) - conn.commit() - - cur.execute(""" - SELECT * FROM {0} ORDER BY a ASC; - DELETE FROM {0}; - SELECT * FROM {0} ORDER BY a ASC; - """.format(self._table)) - - # check first select results - res1 = cur.fetchall() - self.assertListOfListsEqual(res1, [[1, 'aa'], [2, 'bb']]) - self.assertIsNone(cur.fetchone()) - self.assertTrue(cur.nextset()) - - # check delete results - res2 = cur.fetchall() - self.assertListOfListsEqual(res2, [[2]]) - self.assertIsNone(cur.fetchone()) - self.assertTrue(cur.nextset()) - - # check second select results - res3 = cur.fetchall() - self.assertListOfListsEqual(res3, []) - self.assertIsNone(cur.fetchone()) - self.assertFalse(cur.nextset()) - - # unit test for #124 - def test_nextset_with_error(self): - with self._connect() as conn: - cur = conn.cursor() - - cur.execute("SELECT 1; SELECT a; SELECT 2") - - # verify data from first query - res1 = cur.fetchall() - self.assertListOfListsEqual(res1, [[1]]) - self.assertIsNone(cur.fetchone()) - - # second statement results in a query error - with self.assertRaises(errors.MissingColumn): - cur.nextset() - - # unit test for #144 - def test_empty_query(self): - with self._connect() as conn: - cur = conn.cursor() - - cur.execute("") - res = cur.fetchall() - self.assertListOfListsEqual(res, []) - - -class TestExecutemany(VerticaPythonTestCase): - def setUp(self): - self._init_table() - - def tearDown(self): - # self._init_table() - pass - - def _init_table(self): - with self._connect() as conn: - cur = conn.cursor() - # clean old table - cur.execute("DROP TABLE IF EXISTS {0}".format(self._table)) - - # create test table - cur.execute("""CREATE TABLE {0} ( - a INT, - b VARCHAR(32) - ) - """.format(self._table)) - - def _test_executemany(self, table, seq_of_values): - with self._connect() as conn: - cur = conn.cursor() - - cur.executemany("INSERT INTO {0} (a, b) VALUES (%s, %s)".format(table), - seq_of_values) - conn.commit() - - cur.execute("SELECT * FROM {0} ORDER BY a ASC, b ASC".format(table)) - - # check first select results - res1 = cur.fetchall() - seq_of_values_to_compare = sorted([list(values) for values in seq_of_values]) - self.assertListOfListsEqual(res1, seq_of_values_to_compare) - self.assertIsNone(cur.fetchone()) - - def test_executemany(self): - self._test_executemany(self._table, [(1, 'aa'), (2, 'bb')]) - - def test_executemany_quoted_path(self): - table = '.'.join(['"{}"'.format(s.strip('"')) for s in self._table.split('.')]) - self._test_executemany(table, [(1, 'aa'), (2, 'bb')]) diff -Nru python-vertica-0.7.4/vertica_python/tests/test_datatypes.py python-vertica-0.8.0/vertica_python/tests/test_datatypes.py --- python-vertica-0.7.4/vertica_python/tests/test_datatypes.py 2018-08-15 20:33:29.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/test_datatypes.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,60 +0,0 @@ -# Copyright (c) 2018 Micro Focus or one of its affiliates. -# Copyright (c) 2018 Uber Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Copyright (c) 2013-2017 Uber Technologies, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from __future__ import print_function, division, absolute_import - -from decimal import Decimal - -from .base import VerticaPythonTestCase - - -class TypeTestCase(VerticaPythonTestCase): - def test_decimal_query(self): - value = Decimal(0.42) - query = "SELECT {0}::numeric".format(value) - res = self._query_and_fetchone(query) - self.assertAlmostEqual(res[0], value) - - def test_boolean_query__true(self): - value = True - query = "SELECT {0}::boolean".format(value) - res = self._query_and_fetchone(query) - self.assertEqual(res[0], value) - - def test_boolean_query__false(self): - value = False - query = "SELECT {0}::boolean".format(value) - res = self._query_and_fetchone(query) - self.assertEqual(res[0], value) diff -Nru python-vertica-0.7.4/vertica_python/tests/test_dates.py python-vertica-0.8.0/vertica_python/tests/test_dates.py --- python-vertica-0.7.4/vertica_python/tests/test_dates.py 2018-08-15 20:33:29.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/test_dates.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,273 +0,0 @@ -# Copyright (c) 2018 Micro Focus or one of its affiliates. -# Copyright (c) 2018 Uber Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Copyright (c) 2013-2017 Uber Technologies, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from __future__ import print_function, division, absolute_import - -from collections import namedtuple -from datetime import date, datetime - -from .base import VerticaPythonTestCase -from .. import errors -from ..vertica.column import timestamp_parse - -DateTestingCase = namedtuple("DateTestingCase", ["string", "template", "date"]) -TimestampTestingCase = namedtuple("TimestampTestingCase", ["string", "timestamp"]) - - -class DateParsingTestCase(VerticaPythonTestCase): - """Testing DATE type parsing with focus on 'AD'/'BC'. - - Note: the 'BC' or 'AD' era indicators in Vertica's date format seem to make Vertica behave as - follows: - 1. Both 'BC' and 'AD' are simply a flags that tell Vertica: include era indicator if the - date is Before Christ - 2. Dates in 'AD' will never include era indicator - """ - - def _test_dates(self, test_cases, msg=None): - with self._connect() as conn: - cur = conn.cursor() - for tc in test_cases: - cur.execute("SELECT TO_DATE('{0}', '{1}')".format(tc.string, tc.template)) - res = cur.fetchall() - self.assertListOfListsEqual(res, [[tc.date]], msg=msg) - - def _test_not_supported(self, test_cases, msg=None): - with self._connect() as conn: - cur = conn.cursor() - for tc in test_cases: - with self.assertRaises(errors.NotSupportedError, msg=msg): - cur.execute("SELECT TO_DATE('{0}', '{1}')".format(tc.string, tc.template)) - res = cur.fetchall() - self.assertListOfListsEqual(res, [[tc.date]]) - - def test_no_to_no(self): - test_cases = [ - DateTestingCase('1985-10-25', 'YYYY-MM-DD', date(1985, 10, 25)), - DateTestingCase('1955-11-12', 'YYYY-MM-DD', date(1955, 11, 12)), - DateTestingCase('1885-01-01', 'YYYY-MM-DD', date(1885, 1, 1)), - DateTestingCase('2015-10-21', 'YYYY-MM-DD', date(2015, 10, 21)), - ] - self._test_dates(test_cases=test_cases, msg='no indicator -> no indicator') - - def test_ad_to_no(self): - test_cases = [ - DateTestingCase('1985-10-25 AD', 'YYYY-MM-DD', date(1985, 10, 25)), - DateTestingCase('1955-11-12 AD', 'YYYY-MM-DD', date(1955, 11, 12)), - DateTestingCase('1885-01-01 AD', 'YYYY-MM-DD', date(1885, 1, 1)), - DateTestingCase('2015-10-21 AD', 'YYYY-MM-DD', date(2015, 10, 21)), - ] - self._test_dates(test_cases=test_cases, msg='AD indicator -> no indicator') - - def test_bc_to_no(self): - test_cases = [ - DateTestingCase('1985-10-25 BC', 'YYYY-MM-DD', date(1985, 10, 25)), - DateTestingCase('1955-11-12 BC', 'YYYY-MM-DD', date(1955, 11, 12)), - DateTestingCase('1885-01-01 BC', 'YYYY-MM-DD', date(1885, 1, 1)), - DateTestingCase('2015-10-21 BC', 'YYYY-MM-DD', date(2015, 10, 21)), - ] - self._test_dates(test_cases=test_cases, msg='BC indicator -> no indicator') - - def test_no_to_ad(self): - test_cases = [ - DateTestingCase('1985-10-25', 'YYYY-MM-DD AD', date(1985, 10, 25)), - DateTestingCase('1955-11-12', 'YYYY-MM-DD AD', date(1955, 11, 12)), - DateTestingCase('1885-01-01', 'YYYY-MM-DD AD', date(1885, 1, 1)), - DateTestingCase('2015-10-21', 'YYYY-MM-DD AD', date(2015, 10, 21)), - ] - self._test_dates(test_cases=test_cases, msg='no indicator -> AD indicator') - - def test_ad_to_ad(self): - test_cases = [ - DateTestingCase('1985-10-25 AD', 'YYYY-MM-DD AD', date(1985, 10, 25)), - DateTestingCase('1955-11-12 AD', 'YYYY-MM-DD AD', date(1955, 11, 12)), - DateTestingCase('1885-01-01 AD', 'YYYY-MM-DD AD', date(1885, 1, 1)), - DateTestingCase('2015-10-21 AD', 'YYYY-MM-DD AD', date(2015, 10, 21)), - ] - self._test_dates(test_cases=test_cases, msg='AD indicator -> AD indicator') - - def test_bc_to_ad(self): - test_cases = [ - DateTestingCase('1985-10-25 BC', 'YYYY-MM-DD AD', date(1985, 10, 25)), - DateTestingCase('1955-11-12 BC', 'YYYY-MM-DD AD', date(1955, 11, 12)), - DateTestingCase('1885-01-01 BC', 'YYYY-MM-DD AD', date(1885, 1, 1)), - DateTestingCase('2015-10-21 BC', 'YYYY-MM-DD AD', date(2015, 10, 21)), - ] - self._test_not_supported(test_cases=test_cases, msg='BC indicator -> AD indicator') - - def test_no_to_bc(self): - test_cases = [ - DateTestingCase('1985-10-25', 'YYYY-MM-DD BC', date(1985, 10, 25)), - DateTestingCase('1955-11-12', 'YYYY-MM-DD BC', date(1955, 11, 12)), - DateTestingCase('1885-01-01', 'YYYY-MM-DD BC', date(1885, 1, 1)), - DateTestingCase('2015-10-21', 'YYYY-MM-DD BC', date(2015, 10, 21)), - ] - self._test_dates(test_cases=test_cases, msg='no indicator -> BC indicator') - - def test_ad_to_bc(self): - test_cases = [ - DateTestingCase('1985-10-25 AD', 'YYYY-MM-DD BC', date(1985, 10, 25)), - DateTestingCase('1955-11-12 AD', 'YYYY-MM-DD BC', date(1955, 11, 12)), - DateTestingCase('1885-01-01 AD', 'YYYY-MM-DD BC', date(1885, 1, 1)), - DateTestingCase('2015-10-21 AD', 'YYYY-MM-DD BC', date(2015, 10, 21)), - ] - self._test_dates(test_cases=test_cases, msg='AD indicator -> BC indicator') - - def test_bc_to_bc(self): - test_cases = [ - DateTestingCase('1985-10-25 BC', 'YYYY-MM-DD BC', date(1985, 10, 25)), - DateTestingCase('1955-11-12 BC', 'YYYY-MM-DD BC', date(1955, 11, 12)), - DateTestingCase('1885-01-01 BC', 'YYYY-MM-DD BC', date(1885, 1, 1)), - DateTestingCase('2015-10-21 BC', 'YYYY-MM-DD BC', date(2015, 10, 21)), - ] - self._test_not_supported(test_cases=test_cases, msg='BC indicator -> BC indicator') - - -class TimestampParsingTestCase(VerticaPythonTestCase): - def _test_timestamps(self, test_cases, msg=None): - for tc in test_cases: - self.assertEqual(timestamp_parse(tc.string), tc.timestamp, msg=msg) - - def test_timestamp_second_resolution(self): - test_cases = [ # back to the future dates - TimestampTestingCase( - '1985-10-26 01:25:01', - datetime(year=1985, month=10, day=26, hour=1, minute=25, second=1) - ), - TimestampTestingCase( - '1955-11-12 22:55:02', - datetime(year=1955, month=11, day=12, hour=22, minute=55, second=2) - ), - TimestampTestingCase( - '2015-10-21 11:12:03', - datetime(year=2015, month=10, day=21, hour=11, minute=12, second=3) - ), - TimestampTestingCase( - '1885-01-01 01:02:04', - datetime(year=1885, month=1, day=1, hour=1, minute=2, second=4) - ), - TimestampTestingCase( - '1885-09-02 02:03:05', - datetime(year=1885, month=9, day=2, hour=2, minute=3, second=5) - ), - ] - self._test_timestamps(test_cases=test_cases, msg='timestamp second resolution') - - def test_timestamp_microsecond_resolution(self): - test_cases = [ # back to the future dates - TimestampTestingCase( - '1985-10-26 01:25:01.1', - datetime(year=1985, month=10, day=26, hour=1, minute=25, second=1, - microsecond=100000) - ), - TimestampTestingCase( - '1955-11-12 22:55:02.01', - datetime(year=1955, month=11, day=12, hour=22, minute=55, second=2, - microsecond=10000) - ), - TimestampTestingCase( - '2015-10-21 11:12:03.001', - datetime(year=2015, month=10, day=21, hour=11, minute=12, second=3, - microsecond=1000) - ), - TimestampTestingCase( - '1885-01-01 01:02:04.000001', - datetime(year=1885, month=1, day=1, hour=1, minute=2, second=4, - microsecond=1) - ), - TimestampTestingCase( - '1885-09-02 02:03:05.002343', - datetime(year=1885, month=9, day=2, hour=2, minute=3, second=5, - microsecond=2343) - ), - ] - self._test_timestamps(test_cases=test_cases, msg='timestamp microsecond resolution') - - def test_timestamp_year_over_9999_second_resolution(self): - """Asserts that years over 9999 are truncated to 9999""" - test_cases = [ - TimestampTestingCase( - '19850-10-26 01:25:01', - datetime(year=9999, month=10, day=26, hour=1, minute=25, second=1) - ), - TimestampTestingCase( - '10000-11-12 22:55:02', - datetime(year=9999, month=11, day=12, hour=22, minute=55, second=2) - ), - TimestampTestingCase( - '9999-10-21 11:12:03', - datetime(year=9999, month=10, day=21, hour=11, minute=12, second=3) - ), - TimestampTestingCase( - '18850-01-01 01:02:04', - datetime(year=9999, month=1, day=1, hour=1, minute=2, second=4) - ), - TimestampTestingCase( - '18850-09-02 02:03:05', - datetime(year=9999, month=9, day=2, hour=2, minute=3, second=5) - ), - ] - self._test_timestamps(test_cases=test_cases, msg='timestamp past 9999 second resolution') - - def test_timestamp_year_over_9999_microsecond_resolution(self): - test_cases = [ - TimestampTestingCase( - '19850-10-26 01:25:01.1', - datetime(year=9999, month=10, day=26, hour=1, minute=25, second=1, - microsecond=100000) - ), - TimestampTestingCase( - '10000-11-12 22:55:02.01', - datetime(year=9999, month=11, day=12, hour=22, minute=55, second=2, - microsecond=10000) - ), - TimestampTestingCase( - '9999-10-21 11:12:03.001', - datetime(year=9999, month=10, day=21, hour=11, minute=12, second=3, - microsecond=1000) - ), - TimestampTestingCase( - '18850-01-01 01:02:04.000001', - datetime(year=9999, month=1, day=1, hour=1, minute=2, second=4, - microsecond=1) - ), - TimestampTestingCase( - '18850-09-02 02:03:05.002343', - datetime(year=9999, month=9, day=2, hour=2, minute=3, second=5, - microsecond=2343) - ), - ] - self._test_timestamps(test_cases=test_cases, - msg='timestamp past 9999 microsecond resolution') diff -Nru python-vertica-0.7.4/vertica_python/tests/test_errors.py python-vertica-0.8.0/vertica_python/tests/test_errors.py --- python-vertica-0.7.4/vertica_python/tests/test_errors.py 2018-08-15 20:33:29.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/test_errors.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,66 +0,0 @@ -# Copyright (c) 2018 Micro Focus or one of its affiliates. -# Copyright (c) 2018 Uber Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Copyright (c) 2013-2017 Uber Technologies, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from __future__ import print_function, division, absolute_import - -from .base import VerticaPythonTestCase - -from .. import errors - - -class ErrorTestCase(VerticaPythonTestCase): - def setUp(self): - with self._connect() as conn: - cur = conn.cursor() - cur.execute("DROP TABLE IF EXISTS {0}".format(self._table)) - - def test_missing_schema(self): - with self._connect() as conn: - cur = conn.cursor() - with self.assertRaises(errors.MissingSchema): - cur.execute("SELECT 1 FROM missing_schema.table") - - def test_missing_relation(self): - with self._connect() as conn: - cur = conn.cursor() - with self.assertRaises(errors.MissingRelation): - cur.execute("SELECT 1 FROM missing_table") - - def test_duplicate_object(self): - with self._connect() as conn: - cur = conn.cursor() - cur.execute("CREATE TABLE {0} (a BOOLEAN)".format(self._table)) - with self.assertRaises(errors.DuplicateObject): - cur.execute("CREATE TABLE {0} (a BOOLEAN)".format(self._table)) diff -Nru python-vertica-0.7.4/vertica_python/tests/test_timezones.py python-vertica-0.8.0/vertica_python/tests/test_timezones.py --- python-vertica-0.7.4/vertica_python/tests/test_timezones.py 2018-08-15 20:33:29.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/test_timezones.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,87 +0,0 @@ -# Copyright (c) 2018 Micro Focus or one of its affiliates. -# Copyright (c) 2018 Uber Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Copyright (c) 2013-2017 Uber Technologies, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from __future__ import print_function, division, absolute_import - -from collections import namedtuple -from datetime import datetime -import pytz - -from .base import VerticaPythonTestCase - -TimeZoneTestingCase = namedtuple("TimeZoneTestingCase", ["string", "template", "timestamp"]) - - -class TimeZoneTestCase(VerticaPythonTestCase): - def _test_ts(self, test_cases): - with self._connect() as conn: - cur = conn.cursor() - for tc in test_cases: - cur.execute("SELECT TO_TIMESTAMP('{0}', '{1}')".format(tc.string, tc.template)) - res = cur.fetchone() - self.assertEqual(tc.timestamp.toordinal(), res[0].toordinal()) - - def test_simple_ts_query(self): - template = 'YYYY-MM-DD HH:MI:SS.MS' - test_cases = [ - TimeZoneTestingCase( - string='2016-05-15 13:15:17.789', template=template, - timestamp=datetime(year=2016, month=5, day=15, hour=13, minute=15, second=17, - microsecond=789000) - ), - ] - self._test_ts(test_cases=test_cases) - - def test_simple_ts_with_tz_query(self): - template = 'YYYY-MM-DD HH:MI:SS.MS TZ' - test_cases = [ - TimeZoneTestingCase( - string='2016-05-15 13:15:17.789 UTC', template=template, - timestamp=datetime(year=2016, month=5, day=15, hour=13, minute=15, second=17, - microsecond=789000, tzinfo=pytz.utc) - ), - ] - self._test_ts(test_cases=test_cases) - - def test_simple_ts_with_offset_query(self): - template = 'YYYY-MM-DD HH:MI:SS.MS+00' - test_cases = [ - TimeZoneTestingCase( - string='2016-05-15 13:15:17.789 UTC', template=template, - timestamp=datetime(year=2016, month=5, day=15, hour=13, minute=15, second=17, - microsecond=789000, tzinfo=pytz.utc) - ), - ] - self._test_ts(test_cases=test_cases) diff -Nru python-vertica-0.7.4/vertica_python/tests/test_unicode.py python-vertica-0.8.0/vertica_python/tests/test_unicode.py --- python-vertica-0.7.4/vertica_python/tests/test_unicode.py 2018-08-15 20:33:29.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/test_unicode.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,126 +0,0 @@ -# Copyright (c) 2018 Micro Focus or one of its affiliates. -# Copyright (c) 2018 Uber Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Copyright (c) 2013-2017 Uber Technologies, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from __future__ import print_function, division, absolute_import - -from .base import VerticaPythonTestCase - - -class UnicodeTestCase(VerticaPythonTestCase): - def test_unicode_query(self): - value = u'\u16a0' - query = u"SELECT '{0}'".format(value) - - with self._connect() as conn: - cur = conn.cursor() - cur.execute(query) - res = cur.fetchone() - - self.assertResultEqual(value, res[0]) - - def test_unicode_list_parameter(self): - values = [u'\u00f1', 'foo', 3] - query = u"SELECT {0}".format(", ".join(["%s"] * len(values))) - - with self._connect() as conn: - cur = conn.cursor() - cur.execute(query, tuple(values)) - results = cur.fetchone() - - for val, res in zip(values, results): - self.assertResultEqual(val, res) - - def test_unicode_named_parameter_binding(self): - values = [u'\u16b1', 'foo', 3] - keys = [u'\u16a0', 'foo', 3] - - query = u"SELECT {0}".format(", ".join([u":{0}".format(key) for key in keys])) - - with self._connect() as conn: - cur = conn.cursor() - cur.execute(query, dict(zip(keys, values))) - results = cur.fetchone() - - for val, res in zip(values, results): - self.assertResultEqual(val, res) - - def test_string_query(self): - value = u'test' - query = u"SELECT '{0}'".format(value) - - with self._connect() as conn: - cur = conn.cursor() - cur.execute(query) - res = cur.fetchone() - - self.assertEqual(value, res[0]) - - def test_string_named_parameter_binding(self): - key = u'test' - value = u'value' - query = u"SELECT :{0}".format(key) - - with self._connect() as conn: - cur = conn.cursor() - cur.execute(query, {key: value}) - res = cur.fetchone() - - self.assertResultEqual(value, res[0]) - - # unit test for issue #160 - def test_null_named_parameter_binding(self): - key = u'test' - value = None - query = u"SELECT :{0}".format(key) - - with self._connect() as conn: - cur = conn.cursor() - cur.execute(query, {key: value}) - res = cur.fetchone() - - self.assertResultEqual(value, res[0]) - - # unit test for issue #160 - def test_null_list_parameter(self): - values = [u'\u00f1', 'foo', None] - query = u"SELECT {0}".format(", ".join(["%s"] * len(values))) - - with self._connect() as conn: - cur = conn.cursor() - cur.execute(query, tuple(values)) - results = cur.fetchone() - - for val, res in zip(values, results): - self.assertResultEqual(val, res) diff -Nru python-vertica-0.7.4/vertica_python/tests/unit_tests/base.py python-vertica-0.8.0/vertica_python/tests/unit_tests/base.py --- python-vertica-0.7.4/vertica_python/tests/unit_tests/base.py 1970-01-01 00:00:00.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/unit_tests/base.py 2018-09-26 20:21:00.000000000 +0000 @@ -0,0 +1,56 @@ +# Copyright (c) 2018 Micro Focus or one of its affiliates. +# Copyright (c) 2018 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright (c) 2013-2017 Uber Technologies, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import print_function, division, absolute_import + +from nose.plugins.attrib import attr +from ..common.base import VerticaPythonTestCase + +@attr('unit_tests') +class VerticaPythonUnitTestCase(VerticaPythonTestCase): + """ + Base class for tests that do not require database connection; + simple unit testing of individual classes and functions + """ + @classmethod + def setUpClass(cls): + cls.test_config = cls._load_test_config(['log_dir', 'log_level']) + cls._setup_logger('unit_tests', cls.test_config['log_dir'], cls.test_config['log_level']) + + @classmethod + def tearDownClass(cls): + pass + + diff -Nru python-vertica-0.7.4/vertica_python/tests/unit_tests/__init__.py python-vertica-0.8.0/vertica_python/tests/unit_tests/__init__.py --- python-vertica-0.7.4/vertica_python/tests/unit_tests/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/unit_tests/__init__.py 2018-09-26 20:21:00.000000000 +0000 @@ -0,0 +1,34 @@ +# Copyright (c) 2018 Micro Focus or one of its affiliates. +# Copyright (c) 2018 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright (c) 2013-2017 Uber Technologies, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. \ No newline at end of file diff -Nru python-vertica-0.7.4/vertica_python/tests/unit_tests/test_timestamps.py python-vertica-0.8.0/vertica_python/tests/unit_tests/test_timestamps.py --- python-vertica-0.7.4/vertica_python/tests/unit_tests/test_timestamps.py 1970-01-01 00:00:00.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/tests/unit_tests/test_timestamps.py 2018-09-26 20:21:00.000000000 +0000 @@ -0,0 +1,162 @@ +# Copyright (c) 2018 Micro Focus or one of its affiliates. +# Copyright (c) 2018 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright (c) 2013-2017 Uber Technologies, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import print_function, division, absolute_import + +from collections import namedtuple +from datetime import datetime + +from .base import VerticaPythonUnitTestCase +from ...vertica.column import timestamp_parse + +TimestampTestingCase = namedtuple("TimestampTestingCase", ["string", "timestamp"]) + + +class TimestampParsingTestCase(VerticaPythonUnitTestCase): + def _test_timestamps(self, test_cases, msg=None): + for tc in test_cases: + self.assertEqual(timestamp_parse(tc.string), tc.timestamp, msg=msg) + + def test_timestamp_second_resolution(self): + test_cases = [ # back to the future dates + TimestampTestingCase( + '1985-10-26 01:25:01', + datetime(year=1985, month=10, day=26, hour=1, minute=25, second=1) + ), + TimestampTestingCase( + '1955-11-12 22:55:02', + datetime(year=1955, month=11, day=12, hour=22, minute=55, second=2) + ), + TimestampTestingCase( + '2015-10-21 11:12:03', + datetime(year=2015, month=10, day=21, hour=11, minute=12, second=3) + ), + TimestampTestingCase( + '1885-01-01 01:02:04', + datetime(year=1885, month=1, day=1, hour=1, minute=2, second=4) + ), + TimestampTestingCase( + '1885-09-02 02:03:05', + datetime(year=1885, month=9, day=2, hour=2, minute=3, second=5) + ), + ] + self._test_timestamps(test_cases=test_cases, msg='timestamp second resolution') + + def test_timestamp_microsecond_resolution(self): + test_cases = [ # back to the future dates + TimestampTestingCase( + '1985-10-26 01:25:01.1', + datetime(year=1985, month=10, day=26, hour=1, minute=25, second=1, + microsecond=100000) + ), + TimestampTestingCase( + '1955-11-12 22:55:02.01', + datetime(year=1955, month=11, day=12, hour=22, minute=55, second=2, + microsecond=10000) + ), + TimestampTestingCase( + '2015-10-21 11:12:03.001', + datetime(year=2015, month=10, day=21, hour=11, minute=12, second=3, + microsecond=1000) + ), + TimestampTestingCase( + '1885-01-01 01:02:04.000001', + datetime(year=1885, month=1, day=1, hour=1, minute=2, second=4, + microsecond=1) + ), + TimestampTestingCase( + '1885-09-02 02:03:05.002343', + datetime(year=1885, month=9, day=2, hour=2, minute=3, second=5, + microsecond=2343) + ), + ] + self._test_timestamps(test_cases=test_cases, msg='timestamp microsecond resolution') + + def test_timestamp_year_over_9999_second_resolution(self): + # Asserts that years over 9999 are truncated to 9999 + test_cases = [ + TimestampTestingCase( + '19850-10-26 01:25:01', + datetime(year=9999, month=10, day=26, hour=1, minute=25, second=1) + ), + TimestampTestingCase( + '10000-11-12 22:55:02', + datetime(year=9999, month=11, day=12, hour=22, minute=55, second=2) + ), + TimestampTestingCase( + '9999-10-21 11:12:03', + datetime(year=9999, month=10, day=21, hour=11, minute=12, second=3) + ), + TimestampTestingCase( + '18850-01-01 01:02:04', + datetime(year=9999, month=1, day=1, hour=1, minute=2, second=4) + ), + TimestampTestingCase( + '18850-09-02 02:03:05', + datetime(year=9999, month=9, day=2, hour=2, minute=3, second=5) + ), + ] + self._test_timestamps(test_cases=test_cases, msg='timestamp past 9999 second resolution') + + def test_timestamp_year_over_9999_microsecond_resolution(self): + test_cases = [ + TimestampTestingCase( + '19850-10-26 01:25:01.1', + datetime(year=9999, month=10, day=26, hour=1, minute=25, second=1, + microsecond=100000) + ), + TimestampTestingCase( + '10000-11-12 22:55:02.01', + datetime(year=9999, month=11, day=12, hour=22, minute=55, second=2, + microsecond=10000) + ), + TimestampTestingCase( + '9999-10-21 11:12:03.001', + datetime(year=9999, month=10, day=21, hour=11, minute=12, second=3, + microsecond=1000) + ), + TimestampTestingCase( + '18850-01-01 01:02:04.000001', + datetime(year=9999, month=1, day=1, hour=1, minute=2, second=4, + microsecond=1) + ), + TimestampTestingCase( + '18850-09-02 02:03:05.002343', + datetime(year=9999, month=9, day=2, hour=2, minute=3, second=5, + microsecond=2343) + ), + ] + self._test_timestamps(test_cases=test_cases, + msg='timestamp past 9999 microsecond resolution') diff -Nru python-vertica-0.7.4/vertica_python/vertica/column.py python-vertica-0.8.0/vertica_python/vertica/column.py --- python-vertica-0.7.4/vertica_python/vertica/column.py 2018-08-15 20:33:29.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/vertica/column.py 2018-09-26 20:21:00.000000000 +0000 @@ -128,13 +128,20 @@ return date(*map(lambda x: min(int(x), 9999), s.split('-'))) +def time_parse(s): + s = as_str(s) + if len(s) == 8: + return datetime.strptime(s, '%H:%M:%S').time() + return datetime.strptime(s, '%H:%M:%S.%f').time() + + ColumnTuple = namedtuple('Column', ['name', 'type_code', 'display_size', 'internal_size', 'precision', 'scale', 'null_ok']) class Column(object): def __init__(self, col, unicode_error=None): - self.name = col['name'].decode() + self.name = col['name'].decode(UTF_8) self.type_code = col['data_type_oid'] self.display_size = None self.internal_size = col['data_type_size'] @@ -185,7 +192,7 @@ ('char', lambda s: str(s, encoding=UTF_8, errors=unicode_error)), ('varchar', lambda s: str(s, encoding=UTF_8, errors=unicode_error)), ('date', date_parse), - ('time', None), + ('time', time_parse), ('timestamp', timestamp_parse), ('timestamp_tz', timestamp_tz_parse), ('interval', None), diff -Nru python-vertica-0.7.4/vertica_python/vertica/connection.py python-vertica-0.8.0/vertica_python/vertica/connection.py --- python-vertica-0.7.4/vertica_python/vertica/connection.py 2018-08-15 20:33:29.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/vertica/connection.py 2018-09-26 20:21:00.000000000 +0000 @@ -39,20 +39,31 @@ import logging import socket import ssl +import os +import getpass +import errno +import uuid from struct import unpack +from collections import deque # noinspection PyCompatibility,PyUnresolvedReferences from builtins import str -from six import raise_from +from six import raise_from, string_types, integer_types from .. import errors from ..vertica import messages from ..vertica.cursor import Cursor from ..vertica.messages.message import BackendMessage, FrontendMessage from ..vertica.messages.frontend_messages import CancelRequest +from ..vertica.log import VerticaLogging -logger = logging.getLogger('vertica') - +DEFAULT_HOST = 'localhost' +DEFAULT_USER = getpass.getuser() +DEFAULT_PORT = 5433 +DEFAULT_PASSWORD = '' +DEFAULT_READ_TIMEOUT = 600 +DEFAULT_LOG_LEVEL = logging.WARNING +DEFAULT_LOG_PATH = 'vertica_python.log' ASCII = 'ascii' @@ -61,6 +72,99 @@ return Connection(kwargs) +class _AddressList(object): + def __init__(self, host, port, backup_nodes, logger): + """Creates a new deque with the primary host first, followed by any backup hosts""" + + self._logger = logger + + # Format of items in deque: (host, port, is_dns_resolved) + self.address_deque = deque() + + # load primary host into address_deque + self._append(host, port) + + # load backup nodes into address_deque + if not isinstance(backup_nodes, list): + err_msg = 'Connection option "backup_server_node" must be a list' + self._logger.error(err_msg) + raise TypeError(err_msg) + + # Each item in backup_nodes should be either + # a host name or IP address string (using default port) or + # a (host, port) tuple + for node in backup_nodes: + if isinstance(node, string_types): + self._append(node, DEFAULT_PORT) + elif isinstance(node, tuple) and len(node) == 2: + self._append(node[0], node[1]) + else: + err_msg = ('Each item of connection option "backup_server_node"' + ' must be a host string or a (host, port) tuple') + self._logger.error(err_msg) + raise TypeError(err_msg) + + self._logger.debug('Address list: {0}'.format(list(self.address_deque))) + + def _append(self, host, port): + if not isinstance(host, string_types): + err_msg = 'Host must be a string: invalid value: {0}'.format(host) + self._logger.error(err_msg) + raise TypeError(err_msg) + + if not isinstance(port, (string_types, integer_types)): + err_msg = 'Port must be an integer or a string: invalid value: {0}'.format(port) + self._logger.error(err_msg) + raise TypeError(err_msg) + elif isinstance(port, string_types): + try: + port = int(port) + except ValueError as e: + err_msg = 'Port "{0}" is not a valid string: {1}'.format(port, e) + self._logger.error(err_msg) + raise ValueError(err_msg) + + if port < 0 or port > 65535: + err_msg = 'Invalid port number: {0}'.format(port) + self._logger.error(err_msg) + raise ValueError(err_msg) + + self.address_deque.append((host, port, False)) + + def push(self, host, port): + self.address_deque.appendleft((host, port, False)) + + def pop(self): + self.address_deque.popleft() + + def peek(self): + # do lazy DNS resolution, return the leftmost DNS-resolved address + if len(self.address_deque) == 0: + return None + + while len(self.address_deque) > 0: + host, port, is_dns_resolved = self.address_deque[0] + if is_dns_resolved: + # return a resolved address + self._logger.debug('Peek at address list: {0}'.format(list(self.address_deque))) + return (host, port) + else: + # DNS resolve a single host name to multiple IP addresses + self.address_deque.popleft() + try: + resolved_hosts = socket.getaddrinfo(host, port, socket.AF_INET, socket.SOCK_STREAM) + except Exception as e: + self._logger.warning('Error resolving host "{0}" on port {1}: {2}'.format(host, port, e)) + continue + + # add resolved IP addresses to deque + for res in reversed(resolved_hosts): + family, socktype, proto, canonname, sockaddr = res + self.address_deque.appendleft((sockaddr[0], sockaddr[1], True)) + + return None + + class Connection(object): def __init__(self, options=None): self.parameters = {} @@ -73,12 +177,39 @@ options = options or {} self.options = {key: value for key, value in options.items() if value is not None} + self.options.setdefault('host', DEFAULT_HOST) + self.options.setdefault('port', DEFAULT_PORT) + self.options.setdefault('user', DEFAULT_USER) + self.options.setdefault('database', self.options['user']) + self.options.setdefault('password', DEFAULT_PASSWORD) + self.options.setdefault('read_timeout', DEFAULT_READ_TIMEOUT) + + # Set up connection logger + logger_name = 'vertica_{0}_{1}'.format(id(self), str(uuid.uuid4())) # must be a unique value + self._logger = logging.getLogger(logger_name) + + if 'log_level' not in self.options and 'log_path' not in self.options: + # logger is disabled by default + self._logger.disabled = True + else: + self.options.setdefault('log_level', DEFAULT_LOG_LEVEL) + self.options.setdefault('log_path', DEFAULT_LOG_PATH) + VerticaLogging.setup_file_logging(logger_name, self.options['log_path'], + self.options['log_level'], id(self)) + + self.address_list = _AddressList(self.options['host'], self.options['port'], + self.options.get('backup_server_node', []), self._logger) + # we only support one cursor per connection self.options.setdefault('unicode_error', None) - self._cursor = Cursor(self, None, unicode_error=self.options['unicode_error']) - self.options.setdefault('port', 5433) - self.options.setdefault('read_timeout', 600) + self._cursor = Cursor(self, self._logger, cursor_type=None, + unicode_error=self.options['unicode_error']) + + self._logger.info('Connecting as user "{}" to database "{}" on host "{}" with port {}'.format( + self.options['user'], self.options['database'], + self.options['host'], self.options['port'])) self.startup_connection() + self._logger.info('Connection is ready') def __enter__(self): return self @@ -146,40 +277,125 @@ self.backend_key = None self.transaction_status = None self.socket = None + self.address_list = _AddressList(self.options['host'], self.options['port'], + self.options.get('backup_server_node', []), self._logger) def _socket(self): - if self.socket is not None: + if self.socket: return self.socket - host = self.options.get('host') - port = self.options.get('port') - connection_timeout = self.options.get('connection_timeout') + # the initial establishment of the client connection + raw_socket = self.establish_connection() + + # enable load balancing + load_balance_options = self.options.get('connection_load_balance') + self._logger.debug('Connection load balance option is {0}'.format( + 'enabled' if load_balance_options else 'disabled')) + if load_balance_options: + raw_socket = self.balance_load(raw_socket) + + # enable SSL + ssl_options = self.options.get('ssl') + self._logger.debug('SSL option is {0}'.format('enabled' if ssl_options else 'disabled')) + if ssl_options: + raw_socket = self.enable_ssl(raw_socket, ssl_options) + + self.socket = raw_socket + return self.socket + + def create_socket(self): + # Address family IPv6 (socket.AF_INET6) is not supported raw_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) raw_socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + connection_timeout = self.options.get('connection_timeout') if connection_timeout is not None: + self._logger.debug('Set socket connection timeout: {0}'.format(connection_timeout)) raw_socket.settimeout(connection_timeout) - raw_socket.connect((host, port)) + return raw_socket - ssl_options = self.options.get('ssl') - if ssl_options is not None and ssl_options is not False: - from ssl import CertificateError, SSLError - raw_socket.sendall(messages.SslRequest().get_message()) - response = raw_socket.recv(1) - if response in ('S', b'S'): - try: - if isinstance(ssl_options, ssl.SSLContext): - raw_socket = ssl_options.wrap_socket(raw_socket, server_hostname=host) - else: - raw_socket = ssl.wrap_socket(raw_socket) - except CertificateError as e: - raise_from(errors.ConnectionError, e) - except SSLError as e: - raise_from(errors.ConnectionError, e) - else: - raise errors.SSLNotSupported("SSL requested but not supported by server") + def balance_load(self, raw_socket): + # Send load balance request and read server response + raw_socket.sendall(messages.LoadBalanceRequest().get_message()) + response = raw_socket.recv(1) - self.socket = raw_socket - return self.socket + if response in (b'Y', 'Y'): + size = unpack('!I', raw_socket.recv(4))[0] + if size < 4: + err_msg = "Bad message size: {0}".format(size) + self._logger.error(err_msg) + raise errors.MessageError(err_msg) + res = BackendMessage.from_type(type_=response, data=raw_socket.recv(size-4)) + host = res.get_host() + port = res.get_port() + self._logger.info('Load balancing to host "{0}" on port {1}'.format(host, port)) + + socket_host, socket_port = raw_socket.getpeername() + if host == socket_host and port == socket_port: + self._logger.info('Already connecting to host "{0}" on port {1}. Ignore load balancing.'.format(host, port)) + return raw_socket + + # Push the new host onto the address list before connecting again. Note that this + # will leave the originally-specified host as the first failover possibility. + self.address_list.push(host, port) + raw_socket.close() + raw_socket = self.establish_connection() + else: + self._logger.warning("Load balancing requested but not supported by server") + + return raw_socket + + def enable_ssl(self, raw_socket, ssl_options): + from ssl import CertificateError, SSLError + # Send SSL request and read server response + raw_socket.sendall(messages.SslRequest().get_message()) + response = raw_socket.recv(1) + if response in ('S', b'S'): + self._logger.info('Enabling SSL') + try: + if isinstance(ssl_options, ssl.SSLContext): + host, port = raw_socket.getpeername() + raw_socket = ssl_options.wrap_socket(raw_socket, server_hostname=host) + else: + raw_socket = ssl.wrap_socket(raw_socket) + except CertificateError as e: + raise_from(errors.ConnectionError, e) + except SSLError as e: + raise_from(errors.ConnectionError, e) + else: + err_msg = "SSL requested but not supported by server" + self._logger.error(err_msg) + raise errors.SSLNotSupported(err_msg) + return raw_socket + + def establish_connection(self): + addr = self.address_list.peek() + raw_socket = None + last_exception = None + + # Failover: loop to try all addresses + while addr: + last_exception = None + host, port = addr + + self._logger.info('Establishing connection to host "{0}" on port {1}'.format(host, port)) + try: + raw_socket = self.create_socket() + raw_socket.connect((host, port)) + break + except Exception as e: + self._logger.info('Failed to connect to host "{0}" on port {1}: {2}'.format(host, port, e)) + last_exception = e + self.address_list.pop() + addr = self.address_list.peek() + raw_socket.close() + + # all of the addresses failed + if raw_socket is None or last_exception: + err_msg = 'Failed to establish a connection to the primary server or any backup address.' + self._logger.error(err_msg) + raise errors.ConnectionError(err_msg) + + return raw_socket def ssl(self): return self.socket is not None and isinstance(self.socket, ssl.SSLSocket) @@ -196,14 +412,14 @@ if not isinstance(message, FrontendMessage): raise TypeError("invalid message: ({0})".format(message)) - logger.debug('=> %s', message) - + self._logger.debug('=> %s', message) + sock = self._socket() try: for data in message.fetch_message(): try: - self._socket().sendall(data) + sock.sendall(data) except Exception: - logger.error("couldn't send message") + self._logger.error("couldn't send message") raise except Exception as e: @@ -234,7 +450,7 @@ if size < 4: raise errors.MessageError("Bad message size: {0}".format(size)) message = BackendMessage.from_type(type_, self.read_bytes(size - 4)) - logger.debug('<= %s', message) + self._logger.debug('<= %s', message) return message except (SystemError, IOError) as e: self.close_socket() diff -Nru python-vertica-0.7.4/vertica_python/vertica/cursor.py python-vertica-0.8.0/vertica_python/vertica/cursor.py --- python-vertica-0.7.4/vertica_python/vertica/cursor.py 2018-08-15 20:33:29.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/vertica/cursor.py 2018-09-26 20:21:00.000000000 +0000 @@ -36,7 +36,7 @@ from __future__ import print_function, division, absolute_import -import logging +import datetime import re try: @@ -63,7 +63,6 @@ from ..vertica import messages from ..vertica.column import Column -logger = logging.getLogger('vertica') UTF_8 = 'utf-8' @@ -87,8 +86,9 @@ # NOTE: this is used in executemany and is here for pandas compatibility _insert_statement = re.compile(RE_BASIC_INSERT_STAT, re.U | re.I) - def __init__(self, connection, cursor_type=None, unicode_error=None): + def __init__(self, connection, logger, cursor_type=None, unicode_error=None): self.connection = connection + self._logger = logger self.cursor_type = cursor_type self.unicode_error = unicode_error if unicode_error is not None else 'strict' self._closed = False @@ -326,6 +326,7 @@ while True: message = self.connection.read_message() + self._message = message if isinstance(message, messages.ErrorResponse): raise errors.QueryError.from_error_response(message, sql) @@ -378,8 +379,10 @@ if isinstance(param, string_types): param = self.format_quote(as_text(param), is_csv) + elif isinstance(param, (datetime.datetime, datetime.date, datetime.time)): + param = self.format_quote(as_text(str(param)), is_csv) elif param is None: - param = NULL + param = '' if is_csv else NULL else: param = str(param) value = as_text(param) @@ -394,8 +397,10 @@ for param in parameters: if isinstance(param, string_types): param = self.format_quote(as_text(param), is_csv) + elif isinstance(param, (datetime.datetime, datetime.date, datetime.time)): + param = self.format_quote(as_text(str(param)), is_csv) elif param is None: - param = NULL + param = '' if is_csv else NULL else: param = str(param) value = as_text(param) @@ -411,6 +416,6 @@ def format_quote(self, param, is_csv): # TODO Make sure adapt() behaves properly if is_csv: - return '"{0}"'.format(re.escape(param)) + return u'"{0}"'.format(re.escape(param)) else: return QuotedString(param.encode(UTF_8, self.unicode_error)).getquoted() diff -Nru python-vertica-0.7.4/vertica_python/vertica/log.py python-vertica-0.8.0/vertica_python/vertica/log.py --- python-vertica-0.7.4/vertica_python/vertica/log.py 1970-01-01 00:00:00.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/vertica/log.py 2018-09-26 20:21:00.000000000 +0000 @@ -0,0 +1,71 @@ +# Copyright (c) 2018 Micro Focus or one of its affiliates. +# Copyright (c) 2018 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright (c) 2013-2017 Uber Technologies, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +from __future__ import print_function, division, absolute_import + +import os +import logging + +class VerticaLogging(object): + + @classmethod + def setup_file_logging(cls, logger_name, logfile, log_level=logging.INFO, context=''): + logger = logging.getLogger(logger_name) + formatter = logging.Formatter( + fmt=('%(asctime)s.%(msecs)03d [%(module)s] ' + '{}/%(process)d:0x%(thread)x <%(levelname)s> ' + '%(message)s'.format(context)), + datefmt='%Y-%m-%d %H:%M:%S') + cls.ensure_dir_exists(logfile) + file_handler = logging.FileHandler(logfile) + file_handler.setFormatter(formatter) + logger.addHandler(file_handler) + logger.setLevel(log_level) + + @classmethod + def ensure_dir_exists(cls, filepath): + """Ensure that a directory exists + + If it doesn't exist, try to create it and protect against a race condition + if another process is doing the same. + """ + directory = os.path.dirname(filepath) + if directory != '' and not os.path.exists(directory): + try: + os.makedirs(directory) + except OSError as e: + if e.errno != errno.EEXIST: + raise diff -Nru python-vertica-0.7.4/vertica_python/vertica/messages/backend_messages/__init__.py python-vertica-0.8.0/vertica_python/vertica/messages/backend_messages/__init__.py --- python-vertica-0.7.4/vertica_python/vertica/messages/backend_messages/__init__.py 2018-08-15 20:33:29.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/vertica/messages/backend_messages/__init__.py 2018-09-26 20:21:00.000000000 +0000 @@ -44,6 +44,7 @@ from .data_row import DataRow from .empty_query_response import EmptyQueryResponse from .error_response import ErrorResponse +from .load_balance_response import LoadBalanceResponse from .no_data import NoData from .notice_response import NoticeResponse from .parameter_description import ParameterDescription @@ -57,4 +58,4 @@ __all__ = ['RowDescription', 'ReadyForQuery', 'PortalSuspended', 'ParseComplete', 'ParameterStatus', 'NoticeResponse', 'NoData', 'ErrorResponse', 'EmptyQueryResponse', 'DataRow', 'CopyInResponse', 'CommandComplete', 'CloseComplete', 'BindComplete', 'Authentication', - 'BackendKeyData', 'ParameterDescription', 'Unknown'] + 'BackendKeyData', 'ParameterDescription', 'Unknown', 'LoadBalanceResponse'] diff -Nru python-vertica-0.7.4/vertica_python/vertica/messages/backend_messages/load_balance_response.py python-vertica-0.8.0/vertica_python/vertica/messages/backend_messages/load_balance_response.py --- python-vertica-0.7.4/vertica_python/vertica/messages/backend_messages/load_balance_response.py 1970-01-01 00:00:00.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/vertica/messages/backend_messages/load_balance_response.py 2018-09-26 20:21:00.000000000 +0000 @@ -0,0 +1,56 @@ +# Copyright (c) 2018 Micro Focus or one of its affiliates. +# Copyright (c) 2018 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright (c) 2013-2017 Uber Technologies, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import print_function, division, absolute_import + +from ..message import BackendMessage +from struct import unpack + +class LoadBalanceResponse(BackendMessage): + message_id = b'Y' + + def __init__(self, data): + BackendMessage.__init__(self) + unpacked = unpack('!I{0}sx'.format(len(data) - 5), data) + self.port = unpacked[0] + self.host = unpacked[1] + + def get_port(self): + return self.port + + def get_host(self): + return self.host + +BackendMessage.register(LoadBalanceResponse) diff -Nru python-vertica-0.7.4/vertica_python/vertica/messages/backend_messages/parameter_status.py python-vertica-0.8.0/vertica_python/vertica/messages/backend_messages/parameter_status.py --- python-vertica-0.7.4/vertica_python/vertica/messages/backend_messages/parameter_status.py 2018-08-15 20:33:29.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/vertica/messages/backend_messages/parameter_status.py 2018-09-26 20:21:00.000000000 +0000 @@ -46,9 +46,12 @@ def __init__(self, data): BackendMessage.__init__(self) null_byte = data.find(b'\x00') - unpacked = unpack('{0}sx{1}sx'.format(null_byte - 1, len(data) - null_byte - 1), data) + unpacked = unpack('{0}sx{1}sx'.format(null_byte, len(data) - null_byte - 2), data) self.name = unpacked[0] self.value = unpacked[1] + def __str__(self): + return "ParameterStatus: {} = {}".format(self.name, self.value) + BackendMessage.register(ParameterStatus) diff -Nru python-vertica-0.7.4/vertica_python/vertica/messages/frontend_messages/__init__.py python-vertica-0.8.0/vertica_python/vertica/messages/frontend_messages/__init__.py --- python-vertica-0.7.4/vertica_python/vertica/messages/frontend_messages/__init__.py 2018-08-15 20:33:29.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/vertica/messages/frontend_messages/__init__.py 2018-09-26 20:21:00.000000000 +0000 @@ -46,6 +46,7 @@ from .describe import Describe from .execute import Execute from .flush import Flush +from .load_balance_request import LoadBalanceRequest from .parse import Parse from .password import Password from .query import Query @@ -55,5 +56,5 @@ from .terminate import Terminate __all__ = ['Bind', 'Query', 'CancelRequest', 'Close', 'CopyData', 'CopyDone', 'CopyFail', - 'CopyStream', 'Describe', 'Execute', 'Flush', 'Parse', 'Terminate', 'Password', - 'SslRequest', 'Startup', 'Sync'] + 'CopyStream', 'Describe', 'Execute', 'Flush', 'LoadBalanceRequest', 'Parse', + 'Password', 'SslRequest', 'Startup', 'Sync', 'Terminate'] \ No newline at end of file diff -Nru python-vertica-0.7.4/vertica_python/vertica/messages/frontend_messages/load_balance_request.py python-vertica-0.8.0/vertica_python/vertica/messages/frontend_messages/load_balance_request.py --- python-vertica-0.7.4/vertica_python/vertica/messages/frontend_messages/load_balance_request.py 1970-01-01 00:00:00.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/vertica/messages/frontend_messages/load_balance_request.py 2018-09-26 20:21:00.000000000 +0000 @@ -0,0 +1,49 @@ +# Copyright (c) 2018 Micro Focus or one of its affiliates. +# Copyright (c) 2018 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright (c) 2013-2017 Uber Technologies, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import print_function, division, absolute_import + +from struct import pack + +from ..message import BulkFrontendMessage + + +class LoadBalanceRequest(BulkFrontendMessage): + message_id = None + LOADBALANCE_REQUEST = 80936960 + + def read_bytes(self): + bytes_ = pack('!I', self.LOADBALANCE_REQUEST) + return bytes_ diff -Nru python-vertica-0.7.4/vertica_python/vertica/messages/frontend_messages/startup.py python-vertica-0.8.0/vertica_python/vertica/messages/frontend_messages/startup.py --- python-vertica-0.7.4/vertica_python/vertica/messages/frontend_messages/startup.py 2018-08-15 20:33:29.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/vertica/messages/frontend_messages/startup.py 2018-09-26 20:21:00.000000000 +0000 @@ -37,6 +37,7 @@ import platform import os +import getpass import uuid from struct import pack @@ -63,6 +64,7 @@ self._platform = platform.platform().encode(ASCII) self._pid = '{0}'.format(os.getpid()).encode(ASCII) self._label = self._type + b'-' + self._version + b'-' + str(uuid.uuid1()).encode(ASCII) + self._os_user_name = getpass.getuser().encode(ASCII) def read_bytes(self): bytes_ = pack('!I', vertica_python.PROTOCOL_VERSION) @@ -76,6 +78,7 @@ bytes_ += pack('11sx{0}sx'.format(len(self._type)), b'client_type', self._type) bytes_ += pack('14sx{0}sx'.format(len(self._version)), b'client_version', self._version) bytes_ += pack('9sx{0}sx'.format(len(self._platform)), b'client_os', self._platform) + bytes_ += pack('19sx{0}sx'.format(len(self._os_user_name)), b'client_os_user_name', self._os_user_name) bytes_ += pack('10sx{0}sx'.format(len(self._pid)), b'client_pid', self._pid) bytes_ += pack('x') diff -Nru python-vertica-0.7.4/vertica_python/vertica/messages/message.py python-vertica-0.8.0/vertica_python/vertica/messages/message.py --- python-vertica-0.7.4/vertica_python/vertica/messages/message.py 2018-08-15 20:33:29.000000000 +0000 +++ python-vertica-0.8.0/vertica_python/vertica/messages/message.py 2018-09-26 20:21:00.000000000 +0000 @@ -70,6 +70,9 @@ return msg_with_size + def __str__(self): + return self.__class__.__name__ + # noinspection PyAbstractClass class BackendMessage(Message):