pax_global_header00006660000000000000000000000064146474015110014515gustar00rootroot0000000000000052 comment=ab8122ef22701e2038b862ae2f24e3b328003113 vertica-python-1.4.0/000077500000000000000000000000001464740151100144735ustar00rootroot00000000000000vertica-python-1.4.0/.github/000077500000000000000000000000001464740151100160335ustar00rootroot00000000000000vertica-python-1.4.0/.github/workflows/000077500000000000000000000000001464740151100200705ustar00rootroot00000000000000vertica-python-1.4.0/.github/workflows/ci.yaml000066400000000000000000000126001464740151100213460ustar00rootroot00000000000000name: CI on: [push, pull_request] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', 'pypy3.10'] env: REALM: test USER: oauth_user PASSWORD: password CLIENT_ID: vertica CLIENT_SECRET: P9f8350QQIUhFfK1GF5sMhq4Dm3P6Sbs steps: - name: Check out repository uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Set up a Keycloak docker container timeout-minutes: 5 run: | docker network create -d bridge my-network docker run -d -p 8080:8080 \ --name keycloak --network my-network \ -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin \ quay.io/keycloak/keycloak:23.0.4 start-dev docker container ls - name: Set up a Vertica server docker container timeout-minutes: 15 run: | docker run -d -p 5433:5433 -p 5444:5444 \ --name vertica_docker --network my-network \ opentext/vertica-ce:24.2.0-1 echo "Vertica startup ..." until docker exec vertica_docker test -f /data/vertica/VMart/agent_start.out; do \ echo "..."; \ sleep 3; \ done; echo "Vertica is up" docker exec -u dbadmin vertica_docker /opt/vertica/bin/vsql -c "\l" docker exec -u dbadmin vertica_docker /opt/vertica/bin/vsql -c "select version()" - name: Configure Keycloak run: | echo "Wait for keycloak ready ..." bash -c 'while true; do curl -s localhost:8080 &>/dev/null; ret=$?; [[ $ret -eq 0 ]] && break; echo "..."; sleep 3; done' docker exec -i keycloak /bin/bash < access_token.txt docker exec -u dbadmin vertica_docker /opt/vertica/bin/vsql -c "CREATE AUTHENTICATION v_oauth METHOD 'oauth' HOST '0.0.0.0/0';" docker exec -u dbadmin vertica_docker /opt/vertica/bin/vsql -c "ALTER AUTHENTICATION v_oauth SET client_id = '${CLIENT_ID}';" docker exec -u dbadmin vertica_docker /opt/vertica/bin/vsql -c "ALTER AUTHENTICATION v_oauth SET client_secret = '${CLIENT_SECRET}';" docker exec -u dbadmin vertica_docker /opt/vertica/bin/vsql -c "ALTER AUTHENTICATION v_oauth SET discovery_url = 'http://`hostname`:8080/realms/${REALM}/.well-known/openid-configuration';" docker exec -u dbadmin vertica_docker /opt/vertica/bin/vsql -c "ALTER AUTHENTICATION v_oauth SET introspect_url = 'http://`hostname`:8080/realms/${REALM}/protocol/openid-connect/token/introspect';" docker exec -u dbadmin vertica_docker /opt/vertica/bin/vsql -c "SELECT * FROM client_auth WHERE auth_name='v_oauth';" docker exec -u dbadmin vertica_docker /opt/vertica/bin/vsql -c "CREATE USER ${USER};" docker exec -u dbadmin vertica_docker /opt/vertica/bin/vsql -c "GRANT AUTHENTICATION v_oauth TO ${USER};" docker exec -u dbadmin vertica_docker /opt/vertica/bin/vsql -c "GRANT ALL ON SCHEMA PUBLIC TO ${USER};" # A dbadmin-specific authentication record (connect remotely) is needed after setting up an OAuth user docker exec -u dbadmin vertica_docker /opt/vertica/bin/vsql -c "CREATE AUTHENTICATION v_dbadmin_hash METHOD 'hash' HOST '0.0.0.0/0';" docker exec -u dbadmin vertica_docker /opt/vertica/bin/vsql -c "ALTER AUTHENTICATION v_dbadmin_hash PRIORITY 10000;" docker exec -u dbadmin vertica_docker /opt/vertica/bin/vsql -c "GRANT AUTHENTICATION v_dbadmin_hash TO dbadmin;" - name: Install dependencies run: pip install tox - name: Run tests run: | export VP_TEST_USER=dbadmin export VP_TEST_OAUTH_ACCESS_TOKEN=`cat access_token.txt` export VP_TEST_OAUTH_USER=${USER} tox -e py vertica-python-1.4.0/.gitignore000066400000000000000000000007441464740151100164700ustar00rootroot00000000000000*.py[cod] test.py # Test configuration vp_test.conf # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib64 # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox nosetests.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject .DS_Store *.iml # vagrant .vagrant # pycharm .idea # default virtual environment /env/ # pyenv .python-version # backup files end in ~ *~vertica-python-1.4.0/AUTHORS000066400000000000000000000047731464740151100155560ustar00rootroot00000000000000vertica-python was originally created in 2013 at Uber Technologies, Inc. This is a non-exhaustive list of vertica-python contributors. This does not necessarily list everyone who has contributed code. To see the full list of contributors, see the revision history in source control Aditya Sarawgi Aiden Scandella Aiden Scandella Alex Kim Alex Kim Alvin Wu asamasoma Ben Feinstein Berton Earnshaw Brad Hurley Carl Ekerot Conrad Whelan Deepak Prakash Dennis O'Brien Eric Sayle esmioley Flo Fredrik Appelros Gabriel Haim Heli Wang Heli Wang Ian Zelikman Jakub Jedelsky James Brown James Brown jberka Jean Baptiste Favre Jeff Kim Johan Olofsson Josh Varner jsingh3 Justin Berka Justin Berka Kenneth Tran kennethdamica Kevin Sweeney Konstantin lemeryfertitta Lubo Lubo Slivka luke Mark R. nchammas Nicholas Travers Paulius Šileikis Peder Andersen pedercandersen Peter M. Landwehr Praveen Jain Rahul Agarwal rforgione Richard Tom Ruwen Jin Sevag Hanssian Shiva M Steven van Stiphout Thom Neale Thom Neale Timothy Heys tudit tumuluru twneale Udit Tumuluri zer0n vertica-python-1.4.0/CONTRIBUTING.md000066400000000000000000000264771464740151100167440ustar00rootroot00000000000000First off, thank you for considering contributing to *vertica-python* and helping make it even better than it is today! This document will guide you through the contribution process. There are a number of ways you can help: - [Bug Reports](#bug-reports) - [Feature Requests](#feature-requests) - [Code Contributions](#code-contributions) # Bug Reports If you find a bug, submit an [issue](https://github.com/vertica/vertica-python/issues) with a complete and reproducible bug report. If the issue can't be reproduced, it will be closed. If you opened an issue, but figured out the answer later on your own, comment on the issue to let people know, then close the issue. For issues (e.g. security related issues) that are **not suitable** to be reported publicly on the GitHub issue system, report your issues to [Vertica open source team](mailto:vertica-opensrc@opentext.com) directly or file a case with Vertica support if you have a support account. # Feature Requests Feel free to share your ideas for how to improve *vertica-python*. We’re always open to suggestions. You can open an [issue](https://github.com/vertica/vertica-python/issues) with details describing what feature(s) you'd like added or changed. If you would like to implement the feature yourself, open an issue to ask before working on it. Once approved, please refer to the [Code Contributions](#code-contributions) section. # Code Contributions ## Step 1: Fork Fork the project [on Github](https://github.com/vertica/vertica-python) and check out your copy locally. ```shell git clone git@github.com:YOURUSERNAME/vertica-python.git cd vertica-python ``` Your GitHub repository **YOURUSERNAME/vertica-python** will be called "origin" in Git. You should also setup **vertica/vertica-python** as an "upstream" remote. ```shell git remote add upstream git@github.com:vertica/vertica-python.git git fetch upstream ``` ### Configure Git for the first time Make sure git knows your [name](https://help.github.com/articles/setting-your-username-in-git/ "Set commit username in Git") and [email address](https://help.github.com/articles/setting-your-commit-email-address-in-git/ "Set commit email address in Git"): ```shell git config --global user.name "John Smith" git config --global user.email "email@example.com" ``` ## Step 2: Branch Create a new branch for the work with a descriptive name: ```shell git checkout -b my-fix-branch ``` ## Step 3: Install dependencies Install the Python dependencies for development: ```shell pip install -r requirements-dev.txt ``` If you do Kerberos development, you need to install additional [dependencies](README.md#using-kerberos-authentication). ## Step 4: Get the test suite running *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 to the test suite](#tests)! These tests use a Python module: [pytest](https://docs.pytest.org/en/latest/). You might want to check out the pytest documentation for more details. 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. We recommend using a non-production database, because some tests need the superuser permission to manipulate global settings and potentially break that database. Heres one way to go about it: - Spin up a vertica docker container (e.g. [vertica/vertica-ce](https://hub.docker.com/r/vertica/vertica-ce)) 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. Tox is installed with the `requirements-dev.txt`, discussed above. 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. 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. Examples of running tests: ```bash # Run all tests using tox: tox # Run tests on specified python versions with `tox -e ENV,ENV` tox -e py37,py38 # Run specific tests by filename (e.g.) `test_notice.py` tox -- vertica_python/tests/unit_tests/test_notice.py # Run all unit tests on the python version 3.9: tox -e py39 -- -m unit_tests # Run all integration tests on the python version 3.10 with verbose result outputs: tox -e py310 -- -v -m integration_tests # Run an individual test on specified python versions. # e.g.: Run the test `test_error_message` under `test_notice.py` on the python versions 3.8 and 3.9 tox -e py38,py39 -- vertica_python/tests/unit_tests/test_notice.py::NoticeTestCase::test_error_message ``` The arguments after the `--` will be substituted everywhere where you specify `{posargs}` in your test *commands* of `tox.ini`, which are sent to pytest. See `pytest --help` to see all arguments you can specify after the `--`. You might also run `pytest` directly, which will evaluate tests in your current Python environment, rather than across the Python environments/versions that are enumerated in `tox.ini`. For more usages about [tox](https://tox.readthedocs.io), see the Python documentation. The Github Actions [CI workflow](.github/workflows/ci.yaml) committed as part of the project will automatically run test suite through different python versions. These CI tests must pass before any PR will be considered. This CI workflow can be run on your forked repository after you enabling Github Actions on your fork. ## Step 5: Implement your fix or feature At this point, you're ready to make your changes! Feel free to ask for help; everyone is a beginner at first. ### License Headers Every file in this project must use the following Apache 2.0 header (with the appropriate year or years in the "[yyyy]" box; if a copyright statement from another party is already present in the code, you may add the statement on top of the existing copyright statement): ``` Copyright (c) [yyyy] Open Text. 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. ``` ### Commits Make some changes on your branch, then stage and commit as often as necessary: ```shell git add . git commit -m 'Added two more tests for #166' ``` When writing the commit message, try to describe precisely what the commit does. The commit message should be in lines of 72 chars maximum. Include the issue number `#N`, if the commit is related to an issue. ### Tests 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 (see helper function `require_DB_nodes_at_least()` in `vertica_python/tests/integration_tests/base.py`). ## Step 6: Push and Rebase You can publish your work on GitHub just by doing: ```shell git push origin my-fix-branch ``` When you go to your GitHub page, you will notice commits made on your local branch is pushed to the remote repository. When upstream (vertica/vertica-python) has changed, you should rebase your work. The **rebase** command creates a linear history by moving your local commits onto the tip of the upstream commits. You can rebase your branch locally and force-push to your GitHub repository by doing: ```shell git checkout my-fix-branch git fetch upstream git rebase upstream/master git push -f origin my-fix-branch ``` ## Step 7: Make a Pull Request When you think your work is ready to be pulled into *vertica-python*, you should create a pull request(PR) at GitHub. A good pull request means: - commits with one logical change in each - well-formed messages for each commit - documentation and tests, if needed Go to https://github.com/YOURUSERNAME/vertica-python and [make a Pull Request](https://help.github.com/articles/creating-a-pull-request/) to `vertica:master`. ### Sign the CLA Before we can accept a pull request, we first ask people to sign a Contributor License Agreement (or CLA). We ask this so that we know that contributors have the right to donate the code. You should notice a comment from **CLAassistant** on your pull request page, follow this comment to sign the CLA electronically. ### Review Pull requests are usually reviewed within a few days. If there are comments to address, apply your changes in new commits, rebase your branch and force-push to the same branch, re-run the test suite to ensure tests are still passing. We care about quality, Vertica has internal test suites to run as well, so your pull request won't be merged until all internal tests pass. In order to produce a clean commit history, our maintainers would do squash merging once your PR is approved, which means combining all commits of your PR into a single commit in the master branch. That's it! Thank you for your code contribution! After your pull request is merged, you can safely delete your branch and pull the changes from the upstream repository. vertica-python-1.4.0/LICENSE000066400000000000000000000261351464740151100155070ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. vertica-python-1.4.0/MANIFEST.in000066400000000000000000000002101464740151100162220ustar00rootroot00000000000000include LICENSE include requirements.txt include README.md recursive-include vertica_python *.py recursive-exclude vertica_python *.pyc vertica-python-1.4.0/README.md000066400000000000000000001605051464740151100157610ustar00rootroot00000000000000# vertica-python [![PyPI version](https://img.shields.io/pypi/v/vertica-python?color=brightgreen&label=PyPI%20package)](https://pypi.org/project/vertica-python/) [![Conda Version](https://img.shields.io/conda/vn/conda-forge/vertica-python?color=yellowgreen)](https://anaconda.org/conda-forge/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/) [![Downloads](https://pepy.tech/badge/vertica-python/week)](https://pepy.tech/project/vertica-python) *vertica-python* is a native Python client for the Vertica (http://www.vertica.com) database. *vertica-python* is the replacement of the deprecated Python client *vertica_db_client*, which was removed since Vertica server version 9.3. :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/). Please check out [release notes](https://github.com/vertica/vertica-python/releases) to learn about the latest improvements. vertica-python has been tested with Vertica 24.2.0 and Python 3.7/3.8/3.9/3.10/3.11/3.12. Feel free to submit issues and/or pull requests (Read up on our [contributing guidelines](#contributing-guidelines)). ## Installation To install vertica-python with pip: ```bash # Latest release version pip install vertica-python # Latest commit on master branch pip install git+https://github.com/vertica/vertica-python.git@master ``` To install vertica-python from source, run the following command from the root directory: python setup.py install Source code for vertica-python can be found at: https://github.com/vertica/vertica-python #### Using Kerberos authentication vertica-python has optional Kerberos authentication support for Unix-like systems, which requires you to install the [kerberos](https://pypi.org/project/kerberos/) package: pip install kerberos Note that `kerberos` is a python extension module, which means you need to install `python-dev`. The command depends on the package manager and will look like sudo [yum|apt-get|etc] install python-dev Then see [this section](#kerberos-authentication) for how to config Kerberos for a connection. ## Usage :scroll: The basic vertica-python usage is common to all the database adapters implementing the [DB-API v2.0](https://www.python.org/dev/peps/pep-0249/) protocol. ### Create a connection The example below shows how to create a `Connection` object: ```python import vertica_python conn_info = {'host': '127.0.0.1', 'port': 5433, 'user': 'some_user', 'password': 'some_password', 'database': 'a_database', # autogenerated session label by default, 'session_label': 'some_label', # default throw error on invalid UTF-8 results 'unicode_error': 'strict', # SSL is disabled by default 'ssl': False, # autocommit is off by default 'autocommit': True, # using server-side prepared statements is disabled by default 'use_prepared_statements': False, # connection timeout is not enabled by default # 5 seconds timeout for a socket operation (Establishing a TCP connection or read/write operation) 'connection_timeout': 5} # simple connection, with manual close try: connection = vertica_python.connect(**conn_info) # do things finally: connection.close() # using `with` for auto connection closing after usage with vertica_python.connect(**conn_info) as connection: # do things ``` | Connection Option | Description | | ------------- | ------------- | | host | The server host of the connection. This can be a host name or an IP address.
**_Default_**: "localhost" | | port | The port of the connection.
**_Default_**: 5433 | | user | The database user name to use to connect to the database.
**_Default_**:
    (for non-OAuth connections) OS login user name
    (for OAuth connections) "" | | password | The password to use to log into the database.
**_Default_**: "" | | database | The database name.
**_Default_**: "" | | autocommit | See [Autocommit](#autocommit).
**_Default_**: False | | backup_server_node | See [Connection Failover](#connection-failover).
**_Default_**: [] | | binary_transfer | See [Data Transfer Format](#data-transfer-format).
**_Default_**: False (use text format transfer) | | connection_load_balance | See [Connection Load Balancing](#connection-load-balancing).
**_Default_**: False (disabled) | | connection_timeout | The number of seconds (can be a nonnegative floating point number) the client waits for a socket operation (Establishing a TCP connection or read/write operation).
**_Default_**: None (no timeout) | | disable_copy_local | See [COPY FROM LOCAL](#method-2-copy-from-local-sql-with-cursorexecute).
**_Default_**: False | | kerberos_host_name | See [Kerberos Authentication](#kerberos-authentication).
**_Default_**: the value of connection option `host` | | kerberos_service_name | See [Kerberos Authentication](#kerberos-authentication).
**_Default_**: "vertica" | | log_level | See [Logging](#logging). | | log_path | See [Logging](#logging). | | oauth_access_token | See [OAuth Authentication](#oauth-authentication).
**_Default_**: "" | | request_complex_types | See [SQL Data conversion to Python objects](#sql-data-conversion-to-python-objects).
**_Default_**: True | | session_label | Sets a label for the connection on the server. This value appears in the client_label column of the _v_monitor.sessions_ system table.
**_Default_**: an auto-generated label with format of `vertica-python-{version}-{random_uuid}` | | ssl | See [TLS/SSL](#tlsssl).
**_Default_**: None (tlsmode="prefer") | | tlsmode | Controls whether the connection to the server uses TLS encryption.
See [TLS/SSL](#tlsssl).
**_Default_**: "prefer" | | tls_cafile | The name of a file containing trusted SSL certificate authority (CA) certificate(s).
See [TLS/SSL](#tlsssl). | | tls_certfile | The name of a file containing client's certificate(s).
See [TLS/SSL](#tlsssl). | | tls_keyfile | The name of a file containing client's private key.
See [TLS/SSL](#tlsssl). | | unicode_error | See [UTF-8 encoding issues](#utf-8-encoding-issues).
**_Default_**: 'strict' (throw error on invalid UTF-8 results) | | use_prepared_statements | See [Passing parameters to SQL queries](#passing-parameters-to-sql-queries).
**_Default_**: False | | workload | Sets the workload name associated with this session. Valid values are workload names that already exist in a workload routing rule on the server. If a workload name that doesn't exist is entered, the server will reject it and it will be set to the default.
**_Default_**: "" | | dsn | See [Set Properties with Connection String](#set-properties-with-connection-string). | Below are a few important connection topics you may deal with, or you can skip and jump to the next section: [Send Queries and Retrieve Results](#send-queries-and-retrieve-results) #### Set Properties with Connection String Another way to set connection properties is passing a connection string to the keyword parameter `dsn` of `vertica_python.connect(dsn='...', **kwargs)`. The connection string is of the form: ``` vertica://(user):(password)@(host):(port)/(database)?(arg1=val1&arg2=val2&...) ``` The connection string would be parsed by `vertica_python.parse_dsn(connection_str)`, and the parsing result (a dictionary of keywords and values) would be merged with _kwargs_. If the same keyword is specified in both the sources, the _kwargs_ value overrides the parsed _dsn_ value. The `(arg1=val1&arg2=val2&...)` section can handle string/numeric/boolean values, blank and invalid value would be ignored. ```python import vertica_python connection_str = ('vertica://admin@localhost:5433/db1?connection_load_balance=True&connection_timeout=1.5&' 'session_label=vpclient+123%7E456') print(vertica_python.parse_dsn(connection_str)) # {'user': 'admin', 'host': 'localhost', 'port': 5433, 'database': 'db1', # 'connection_load_balance': True, 'connection_timeout': 1.5, 'session_label': 'vpclient 123~456'} additional_info = { 'password': 'some_password', 'backup_server_node': ['10.6.7.123', ('10.20.82.77', 6000)] # invalid value to be set in a connection string } with vertica_python.connect(dsn=connection_str, **additional_info) as conn: # do things ``` #### TLS/SSL There are two options to control client-server TLS: `tlsmode` and `ssl`. If both are set, `tlsmode` takes precedence. `ssl` can be a bool or a `ssl.SSLContext` object. Here is the value mapping between `ssl` (exclude `ssl.SSLContext`) and `tlsmode`: | `tlsmode` | `ssl` | Description | | ------------- | ------------- | ---| | 'disable' | False | only try a non-TLS connection. | | 'prefer' | (not set) | (Default) first try a TLS connection; if TLS is disabled on the server, then fallback to a non-TLS connection.
Note: If TLS is enabled on the server and TLS connection fails, the client rejects the connection. | | 'require' | True | connects using TLS without verifying certificates. If the TLS connection attempt fails, the client rejects the connection. | | 'verify-ca' || connects using TLS and confirms that the server certificate has been signed by a trusted certificate authority. | | 'verify-full' || connects using TLS, confirms that the server certificate has been signed by a trusted certificate authority, and verifies that the host name matches the name provided in the server certificate. | When `tlsmode` is 'verify-ca' or 'verify-full', these options take certificate/key files: `tls_cafile`, `tls_certfile` and `tls_keyfile`. Otherwise, these options are ignored. `tlsmode` example: ```python # [TLSMode: require] import vertica_python conn_info = {'host': '127.0.0.1', 'user': 'some_user', 'database': 'a_database', 'tlsmode': 'require'} connection = vertica_python.connect(**conn_info) ``` ```python # [TLSMode: verify-ca] import vertica_python conn_info = {'host': '127.0.0.1', 'user': 'some_user', 'database': 'a_database', 'tlsmode': 'verify-ca', 'tls_cafile': '/path/to/ca_file.pem' # CA certificate used to verify server certificate } connection = vertica_python.connect(**conn_info) ``` ```python # [TLSMode: verify-full] + Mutual Mode import vertica_python conn_info = {'host': '127.0.0.1', 'user': 'some_user', 'database': 'a_database', 'tlsmode': 'verify-full', 'tls_cafile' = '/path/to/ca_file.pem' # CA certificate used to verify server certificate 'tls_certfile' = '/path/to/client.pem', # (for mutual mode) client certificate 'tls_keyfile' = '/path/to/client.key' # (for mutual mode) client private key } connection = vertica_python.connect(**conn_info) ``` You can pass an `ssl.SSLContext` object to `ssl` to customize the underlying SSL connection options. See more on SSL options [here](https://docs.python.org/3/library/ssl.html). Server mode TLS examples: ```python import vertica_python import ssl # [TLSMode: require] # Ensure connection is encrypted. ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE conn_info = {'host': '127.0.0.1', 'port': 5433, 'user': 'some_user', 'database': 'a_database', 'ssl': ssl_context} connection = vertica_python.connect(**conn_info) # [TLSMode: verify-ca] # Ensure connection is encrypted, and client trusts server certificate. ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ssl_context.verify_mode = ssl.CERT_REQUIRED ssl_context.check_hostname = False ssl_context.load_verify_locations(cafile='/path/to/ca_file.pem') # CA certificate used to verify server certificate conn_info = {'host': '127.0.0.1', 'port': 5433, 'user': 'some_user', 'database': 'a_database', 'ssl': ssl_context} connection = vertica_python.connect(**conn_info) # [TLSMode: verify-full] # Ensure connection is encrypted, client trusts server certificate, # and server hostname matches the one listed in the server certificate. ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ssl_context.verify_mode = ssl.CERT_REQUIRED ssl_context.check_hostname = True ssl_context.load_verify_locations(cafile='/path/to/ca_file.pem') # CA certificate used to verify server certificate conn_info = {'host': '127.0.0.1', 'port': 5433, 'user': 'some_user', 'database': 'a_database', 'ssl': ssl_context} connection = vertica_python.connect(**conn_info) ``` Mutual mode TLS example: ```python import vertica_python import ssl # [TLSMode: verify-full] # Ensure connection is encrypted, client trusts server certificate, # and server hostname matches the one listed in the server certificate. ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ssl_context.verify_mode = ssl.CERT_REQUIRED ssl_context.check_hostname = True ssl_context.load_verify_locations(cafile='/path/to/ca_file.pem') # CA certificate used to verify server certificate # For Mutual mode, provide client certificate and client private key to ssl_context. # CA certificate used to verify client certificate should be set at the server side. ssl_context.load_cert_chain(certfile='/path/to/client.pem', keyfile='/path/to/client.key') conn_info = {'host': '127.0.0.1', 'port': 5433, 'user': 'some_user', 'database': 'a_database', 'ssl': ssl_context} connection = vertica_python.connect(**conn_info) ``` #### Kerberos Authentication In order to use Kerberos authentication, install [dependencies](#using-kerberos-authentication) first, and it is the user's responsibility to ensure that an Ticket-Granting Ticket (TGT) is available and valid. Whether a TGT is available can be easily determined by running the `klist` command. If no TGT is available, then it first must be obtained by running the `kinit` command or by logging in. You can pass in optional arguments to customize the authentication. The arguments are `kerberos_service_name`, which defaults to "vertica", and `kerberos_host_name`, which defaults to the value of argument `host`. For example, ```python import vertica_python conn_info = {'host': '127.0.0.1', 'port': 5433, 'user': 'some_user', 'password': 'some_password', 'database': 'a_database', # The service name portion of the Vertica Kerberos principal 'kerberos_service_name': 'vertica_krb', # The instance or host name portion of the Vertica Kerberos principal 'kerberos_host_name': 'vcluster.example.com'} with vertica_python.connect(**conn_info) as conn: # do things ``` #### OAuth Authentication To authenticate via OAuth, provide an `oauth_access_token` that authorizes a user to the database. ```python import vertica_python conn_info = {'host': '127.0.0.1', 'port': 5433, 'database': 'a_database', # valid OAuth access token 'oauth_access_token': 'xxxxxx'} with vertica_python.connect(**conn_info) as conn: # do things ``` #### Logging Logging is disabled by default if neither ```log_level``` or ```log_path``` are set. Passing value to at least one of those options to enable logging. When logging is enabled, the default value of ```log_level``` is _logging.WARNING_. You can find all levels [here](https://docs.python.org/3/library/logging.html#logging-levels). And the default value of ```log_path``` is 'vertica_python.log', the log file will be in the current execution directory. If ```log_path``` is set to ```''``` (empty string) or ```None```, no file handler is set, logs will be processed by root handlers. 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 ## Example 4: use root handlers to process logs by setting 'log_path' to '' (empty string) conn_info = {'host': '127.0.0.1', 'port': 5433, 'user': 'some_user', 'password': 'some_password', 'database': 'a_database', 'log_level': logging.DEBUG, 'log_path': ''} 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 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 vertica_python.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 vertica_python.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 ``` #### Data Transfer Format There are two formats for transferring data from a server to a vertica-python client: text and binary. For example, a FLOAT type data is represented as a 8-byte IEEE-754 floating point number (fixed-width) in binary format, and a human-readable string (variable-width) in text format. The text format of values is whatever strings are produced and accepted by the input/output conversion functions for the particular data type. Depending on the data type, binary transfer is generally more efficient and requires less bandwidth than text transfer. However, when transferring a large number of small values, binary transfer may use more bandwidth. A connection is set to use text format by default. Set ```binary_transfer``` to True to use binary format. ```python import vertica_python conn_info = {'host': '127.0.0.1', 'port': 5433, 'user': 'some_user', 'password': 'some_password', 'database': 'vdb', 'binary_transfer': True # False by default } # Server enables binary transfer with vertica_python.connect(**conn_info) as conn: cur = conn.cursor() ... ``` Ideally, the [output data](#sql-data-conversion-to-python-objects) should be the same for these two formats. However, there are edge cases: - FLOAT data: binary format might offer slightly greater precision than text format. E.g. `select ATAN(12.345)` returns 1.48996835348642 (text) or 1.489968353486419 (binary) - TIMESTAMPTZ data: text format always use the session timezone, but binary format might fail to get session timezone and use local timezone. - NUMERIC data: In old server versions, the precision and scale is incorrect when querying a NUMERIC column that is not from a specific table with prepared statement in binary format. E.g. `select ?::NUMERIC` or `select node_id, ?/50 from nodes`. In newer server versions, binary transfer is forcibly disabled for NUMERIC data by the server, regardless of client-side values of ```binary_transfer``` and ```use_prepared_statements```. ### Send Queries and Retrieve Results The `Connection` class encapsulates a database session. It allows to: - create new `Cursor` instances using the `cursor()` method to execute database commands and queries. - [terminate transactions](#insert-and-commitrollback) using the methods `commit()` or `rollback()`. The class `Cursor` allows interaction with the database: - send commands to the database using methods such as `execute()`, `executemany()` and [copy](#using-copy-from). - retrieve data from the database, [iterating on the cursor](#stream-query-results) or using methods such as `fetchone()`, `fetchmany()`, `fetchall()`, `nextset()`. ```python import vertica_python conn_info = {'host': '127.0.0.1', 'port': 5433, 'user': 'some_user', 'password': 'some_password', 'database': 'vdb'} # Connect to a vertica database with vertica_python.connect(**conn_info) as conn: # Open a cursor to perform database operations # vertica-python only support one cursor per connection cur = conn.cursor() # Execute a command: create a table cur.execute("CREATE TABLE tbl (a INT, b VARCHAR)") # Insert a row cur.execute("INSERT INTO tbl VALUES (1, 'aa')") inserted = cur.fetchall() # [[1]] # Bulk Insert with executemany() # Pass data to fill a query placeholders and let vertica-python perform the correct conversion cur.executemany("INSERT INTO tbl(a, b) VALUES (?, ?)", [(2, 'bb'), (3, 'foo'), (4, 'xx'), (5, 'bar')], use_prepared_statements=True) # OR # cur.executemany("INSERT INTO tbl(a, b) VALUES (%s, %s)", [(6, 'bb'), (7, 'foo'), (8, 'xx'), (9, 'bar')], use_prepared_statements=False) # Query the database and obtain data as Python objects. cur.execute("SELECT * FROM tbl") datarow = cur.fetchone() # [1, 'aa'] remaining_rows = cur.fetchall() # [[2, 'bb'], [3, 'foo'], [4, 'xx'], [5, 'bar']] # Make the changes to the database persistent conn.commit() # Execute a query with MULTIPLE statements cur.execute("SELECT 1; SELECT 2; ...; SELECT N") while True: # Fetch the result set for each statement rows = cur.fetchall() print(rows) if not cur.nextset(): break # Output: # [[1]] # [[2]] # ... # [[N]] ``` #### Frequently Asked Questions :speech_balloon:
Why does my query return empty results? If you think Cursor.fetch*() should return something, check whether your query contains multiple statements. It is very likely that you miss to call Cursor.nextset().
Why does my query not throw an error? vertica-python tries to throw exceptions in the Cursor.execute() method, but depending on your query, there are some exceptions that can only be raised when you call fetchone() fetchmany() or fetchall(). In addition, if your query has multiple statements, errors that is not in the first statement cannot be thrown by execute(). It is recommended to always call fetchall() after execute() in order to capture any error. And for a query with multiple statements, call fetchall() and nextset() as the above example code shows.
Why is this client N times slower than another vertica client? You may find vertica-python performs much slower executing same query on same machine than another python client (e.g. pyodbc) or client in other programming language. This is because vertica-python is a pure Python program and CPython (the official implementation of Python, which is an interpreted, dynamic language) computation is often many times slower than compiled languages like C and Go, or JIT (Just-in-Time) compiled languages like Java and JavaScript. Therefore, if you want to get better performance, instead of using the official CPython interpreter, try other performance-oriented interpreters such as PyPy.
### Stream query results Streaming is recommended if you want to further process each row, save the results in a non-list/dict format (e.g. Pandas DataFrame), or save the results in a file. ```python cur = connection.cursor() cur.execute("SELECT * FROM a_table LIMIT 2") for row in cur.iterate(): print(row) # [ 1, 'some text', datetime.datetime(2014, 5, 18, 6, 47, 1, 928014) ] # [ 2, 'something else', None ] ``` ### In-memory results as list ```python cur = connection.cursor() cur.execute("SELECT * FROM a_table LIMIT 2") cur.fetchall() # [ [1, 'something'], [2, 'something_else'] ] ``` ### In-memory results as dictionary ```python cur = connection.cursor('dict') cur.execute("SELECT * FROM a_table LIMIT 2") cur.fetchall() # [ {'id': 1, 'value': 'something'}, {'id': 2, 'value': 'something_else'} ] connection.close() ``` ### Nextset If you execute multiple statements in a single call to execute(), you can use `Cursor.nextset()` to retrieve all of the data. ```python cur.execute('SELECT 1; SELECT 2;') cur.fetchone() # [1] cur.fetchone() # None cur.nextset() # True cur.fetchone() # [2] cur.fetchone() # None cur.nextset() # False ``` ### Passing parameters to SQL queries vertica-python provides two methods for passing parameters to a SQL query: 1. [Server-side binding](#server-side-binding-query-using-prepared-statements) 2. [Client-side binding](#client-side-binding-query-using-named-parameters-or-format-parameters) :warning: Prerequisites: Only SQL literals (i.e. query values) should be bound via these methods: they shouldn’t be used to merge table or field names to the query (_vertica-python_ will try quoting the table name as a string value, generating invalid SQL as it is actually a SQL Identifier). If you need to generate dynamically SQL queries (for instance choosing dynamically a table name) you have to construct the full query yourself. #### Server-side binding: Query using prepared statements Vertica server-side prepared statements let you define a statement once and then run it many times with different parameters. Internally, vertica-python sends the query and the parameters to the server separately. Placeholders in the statement are represented by question marks (?). Server-side prepared statements are useful for preventing SQL injection attacks. ```python import vertica_python # Enable using server-side prepared statements at connection level conn_info = {'host': '127.0.0.1', 'user': 'some_user', 'password': 'some_password', 'database': 'a_database', 'use_prepared_statements': True, } with vertica_python.connect(**conn_info) as connection: cur = connection.cursor() cur.execute("CREATE TABLE tbl (a INT, b VARCHAR)") cur.execute("INSERT INTO tbl VALUES (?, ?)", [1, 'aa']) cur.execute("INSERT INTO tbl VALUES (?, ?)", [2, 'bb']) cur.executemany("INSERT INTO tbl VALUES (?, ?)", [(3, 'foo'), (4, 'xx'), (5, 'bar')]) cur.execute("COMMIT") cur.execute("SELECT * FROM tbl WHERE a>=? AND a<=? ORDER BY a", (2,4)) cur.fetchall() # [[2, 'bb'], [3, 'foo'], [4, 'xx']] ``` :no_entry_sign: Vertica server-side prepared statements does not support executing a query string containing multiple statements. You can set ```use_prepared_statements``` option in ```cursor.execute*()``` functions to override the connection level setting. ```python import vertica_python # Enable using server-side prepared statements at connection level conn_info = {'host': '127.0.0.1', 'user': 'some_user', 'password': 'some_password', 'database': 'a_database', 'use_prepared_statements': True, } with vertica_python.connect(**conn_info) as connection: cur = connection.cursor() cur.execute("CREATE TABLE tbl (a INT, b VARCHAR)") # Executing compound statements cur.execute("INSERT INTO tbl VALUES (?, ?); COMMIT", [1, 'aa']) # Error message: Cannot insert multiple commands into a prepared statement # Disable prepared statements but forget to change placeholders (?) cur.execute("INSERT INTO tbl VALUES (?, ?); COMMIT;", [1, 'aa'], use_prepared_statements=False) # TypeError: not all arguments converted during string formatting cur.execute("INSERT INTO tbl VALUES (%s, %s); COMMIT;", [1, 'aa'], use_prepared_statements=False) cur.execute("INSERT INTO tbl VALUES (:a, :b); COMMIT;", {'a': 2, 'b': 'bb'}, use_prepared_statements=False) # Disable using server-side prepared statements at connection level conn_info['use_prepared_statements'] = False with vertica_python.connect(**conn_info) as connection: cur = connection.cursor() # Try using prepared statements cur.execute("INSERT INTO tbl VALUES (?, ?)", [3, 'foo']) # TypeError: not all arguments converted during string formatting cur.execute("INSERT INTO tbl VALUES (?, ?)", [3, 'foo'], use_prepared_statements=True) # Query using named parameters cur.execute("SELECT * FROM tbl WHERE a>=:n1 AND a<=:n2 ORDER BY a", {'n1': 2, 'n2': 4}) cur.fetchall() # [[2, 'bb'], [3, 'foo']] ``` Note: In other drivers, the batch insert is converted into a COPY statement by using prepared statements. vertica-python currently does not support that. [More details](#cursorexecutemany-server-side-binding-vs-client-side-binding) #### Client-side binding: Query using named parameters or format parameters vertica-python can automatically convert Python objects to SQL literals, merge the query and the parameters on the client side, and then send the query to the server: using this feature your code will be more robust and reliable to prevent SQL injection attacks. You need to set ```use_prepared_statements``` option to __False__ (at connection level or in cursor.execute*()) to use client-side binding. Variables can be specified with named (__:name__) placeholders. ```python cur = connection.cursor() data = {'propA': 1, 'propB': 'stringValue'} cur.execute("SELECT * FROM a_table WHERE a = :propA AND b = :propB", data, use_prepared_statements=False) # converted into a SQL command similar to: SELECT * FROM a_table WHERE a = 1 AND b = 'stringValue' cur.fetchall() # [ [1, 'stringValue'] ] ``` Variables can also be specified with positional format (__%s__) placeholders. The placeholder __must always be a %s__, even if a different placeholder (such as a `%d` for integers or `%f` for floats) may look more appropriate. __Never__ use Python string concatenation (+) or string parameters interpolation (%) to pass variables to a SQL query string. ```python cur = connection.cursor() data = (1, "O'Reilly") cur.execute("SELECT * FROM a_table WHERE a = %s AND b = %s" % data) # WRONG: % operator cur.execute("SELECT * FROM a_table WHERE a = %d AND b = %s", data) # WRONG: %d placeholder cur.execute("SELECT * FROM a_table WHERE a = %s AND b = %s", data) # correct # converted into a SQL command: SELECT * FROM a_table WHERE a = 1 AND b = 'O''Reilly' cur.fetchall() # [ [1, "O'Reilly"] ] ``` The placeholder must not be quoted. _vertica-python_ will add quotes where needed. ```python >>> cur.execute("INSERT INTO table VALUES (':propA')", {'propA': "someString"}) # WRONG >>> cur.execute("INSERT INTO table VALUES (:propA)", {'propA': "someString"}) # correct >>> cur.execute("INSERT INTO table VALUES ('%s')", ("someString",)) # WRONG >>> cur.execute("INSERT INTO table VALUES (%s)", ("someString",)) # correct ``` In order to merge the query (with placeholders) and the parameters on the client side, parameter values (python object) are converted to SQL literals (str). _vertica-python_ supports default mapping to SQL literals for many standard Python types (str, bytes, bool, int, float, decimal.Decimal, tuple, list, set, dict, datetime.datetime, datetime.date, datetime.time, uuid.UUID). For complex types, in some cases, you may need explicit typecasting for the placeholder (e.g. `%s::ARRAY[ARRAY[INT]]`, `%s::ROW(varchar,int,date)`): ```python from datetime import date cur.execute("CREATE TABLE table (a INT, b ARRAY[DATE])") value = [date(2021, 6, 10), date(2021, 6, 12), date(2021, 6, 30)] cur.execute("INSERT INTO table VALUES (%s, %s)", [100, value], use_prepared_statements=False) # WRONG # Error Message: Column "b" is of type array[date] but expression is of type array[varchar], Sqlstate: 42804, # Hint: You will need to rewrite or cast the expression cur.execute("INSERT INTO table VALUES (%s, %s::ARRAY[DATE])", [100, value], use_prepared_statements=False) # correct # converted into a SQL command: INSERT INTO vptest VALUES (100, ARRAY['2021-06-10','2021-06-12','2021-06-30']::ARRAY[DATE]) # Client-side binding of cursor.executemany is different from cursor.execute internally # But it also supports some of complex types mapping cur.executemany("INSERT INTO table (a, b) VALUES (%s, %s)", [[100, value]], use_prepared_statements=False) ``` ##### Register new SQL literal adapters It is possible to adapt new Python types to SQL literals via `Cursor.register_sql_literal_adapter(py_class_or_type, adapter_function)` function. Example: ```python class Point(object): def __init__(self, x, y): self.x = x self.y = y # Adapter should return a string value def adapt_point(point): return "STV_GeometryPoint({},{})".format(point.x, point.y) cur = conn.cursor() cur.register_sql_literal_adapter(Point, adapt_point) cur.execute("INSERT INTO geom_data (geom) VALUES (%s)", [Point(1.23, 4.56)]) cur.execute("select ST_asText(geom) from geom_data") cur.fetchall() # [['POINT (1.23 4.56)']] ``` To help you debug the binding process during Cursor.execute*(), `Cursor.object_to_sql_literal(py_object)` function can be used to inspect the SQL literal string converted from a Python object. ```python cur = conn.cursor cur.object_to_sql_literal("O'Reilly") # "'O''Reilly'" cur.object_to_sql_literal(None) # "NULL" cur.object_to_sql_literal(True) # "True" cur.object_to_sql_literal(Decimal("10.00000")) # "10.00000" cur.object_to_sql_literal(datetime.date(2018, 9, 7)) # "'2018-09-07'" cur.object_to_sql_literal(Point(-71.13, 42.36)) # "STV_GeometryPoint(-71.13,42.36)" if you registered in previous step ``` #### Cursor.executemany(): Server-side binding vs Client-side binding ``` Cursor.executemany(query, seq_of_parameters, use_prepared_statements=None) ``` Execute the same query or command with a sequence of input data. PARAMETERS - query (str or bytes) – The query to execute. - seq_of_parameters (a list/tuple of Sequences or Mappings) – The parameters to pass to the query. - use_prepared_statements (bool) – Use connection level setting by default. If set, execute the query using server-side prepared statements or not. When `use_prepared_statements=True` (Server-side binding), the query should contain only a single statement. Internally, vertica-python sends the query and each set of parameters to the server separately. When `use_prepared_statements=False` (Client-side binding), the query is limited to simple INSERT statements only. The batch insert is converted into a COPY FROM STDIN statement by the client. This is more efficient than performing separate queries (may even faster than Server-side binding), but in case of other statements you may consider using [copy](#using-copy-from). ```python # Note the query parameter placeholders difference! cur.executemany("INSERT INTO tbl(a, b) VALUES (?, ?)", [(2, 'bb'), (3, 'foo'), (4, 'xx'), (5, 'bar')], use_prepared_statements=True) cur.executemany("INSERT INTO tbl(a, b) VALUES (%s, %s)", [(6, 'bb'), (7, 'foo'), (8, 'xx'), (9, 'bar')], use_prepared_statements=False) cur.executemany("INSERT INTO tbl(a, b) VALUES (:a, :b)", [{'a': 2, 'b': 'bb'}, {'a': 3, 'b': 'foo'}], use_prepared_statements=False) ``` ### Insert and commit/rollback ```python cur = connection.cursor() # inline commit (when 'use_prepared_statements' is False) cur.execute("INSERT INTO a_table (a, b) VALUES (1, 'aa'); commit;") # commit in execution cur.execute("INSERT INTO a_table (a, b) VALUES (1, 'aa')") cur.execute("INSERT INTO a_table (a, b) VALUES (2, 'bb')") cur.execute("commit;") # connection.commit() cur.execute("INSERT INTO a_table (a, b) VALUES (1, 'aa')") connection.commit() # connection.rollback() cur.execute("INSERT INTO a_table (a, b) VALUES (0, 'bad')") connection.rollback() ``` ### Autocommit Session parameter AUTOCOMMIT can be configured by the connection option and the `Connection.autocommit` read/write attribute: ```python import vertica_python # Enable autocommit at startup conn_info = {'host': '127.0.0.1', 'user': 'some_user', 'password': 'some_password', 'database': 'a_database', # autocommit is off by default 'autocommit': True, } with vertica_python.connect(**conn_info) as connection: # Check current session autocommit setting print(connection.autocommit) # should be True # If autocommit is True, statements automatically commit their transactions when they complete. # Set autocommit setting with attribute connection.autocommit = False print(connection.autocommit) # should be False # If autocommit is False, the methods commit() or rollback() must be manually invoked to terminate the transaction. ``` To set AUTOCOMMIT to a new value, vertica-python uses `Cursor.execute()` to execute a command internally, and that would clear your previous query results, so be sure to call `Cursor.fetch*()` to save your results before you set autocommit. ### Using COPY FROM :warning: Prerequisites: If the data is in files not STDIN, only files on the client system should be loaded via these methods below. For files on the server system, run "COPY target-table FROM 'path‑to‑data'" with `Cursor.execute()`. There are 2 methods to do copy: #### Method 1: "COPY FROM STDIN" sql with Cursor.copy() ```python cur = connection.cursor() cur.copy("COPY test_copy (id, name) FROM stdin DELIMITER ',' ", csv) ``` Where `csv` is either a string or a file-like object (specifically, any object with a `read()` method). If using a file, the data is streamed (in chunks of `buffer_size` bytes, which defaults to 128 * 2 ** 10). ```python with open("/tmp/binary_file.csv", "rb") as fs: cursor.copy("COPY table(field1, field2) FROM STDIN DELIMITER ',' ENCLOSED BY '\"'", fs, buffer_size=65536) ``` #### Method 2: "COPY FROM LOCAL" sql with Cursor.execute() ```python import sys import vertica_python conn_info = {'host': '127.0.0.1', 'user': 'some_user', 'password': 'some_password', 'database': 'a_database', # False by default #'disable_copy_local': True, # Don't support executing COPY LOCAL operations with prepared statements 'use_prepared_statements': False } with vertica_python.connect(**conn_info) as connection: cur = connection.cursor() # Copy from local file cur.execute("COPY table(field1, field2) FROM LOCAL" " 'data_Jan_*.csv','data_Feb_01.csv' DELIMITER ','" " REJECTED DATA 'path/to/write/rejects.txt'" " EXCEPTIONS 'path/to/write/exceptions.txt'", buffer_size=65536 ) print("Rows loaded:", cur.fetchall()) # Copy from local stdin cur.execute("COPY table(field1, field2) FROM LOCAL STDIN DELIMITER ','", copy_stdin=sys.stdin) print("Rows loaded:", cur.fetchall()) # Copy from local stdin (compound statements) with open('f1.csv', 'r') as fs1, open('f2.csv', 'r') as fs2: cur.execute("COPY tlb1(field1, field2) FROM LOCAL STDIN DELIMITER ',';" "COPY tlb2(field1, field2) FROM LOCAL STDIN DELIMITER ',';", copy_stdin=[fs1, fs2], buffer_size=65536) print("Rows loaded 1:", cur.fetchall()) cur.nextset() print("Rows loaded 2:", cur.fetchall()) # Copy from local stdin (StringIO) from io import StringIO data = "Anna|123-456-789\nBrown|555-444-3333\nCindy|555-867-53093453453\nDodd|123-456-789\nEd|123-456-789" cur.execute("COPY customers (firstNames, phoneNumbers) FROM LOCAL STDIN ENFORCELENGTH RETURNREJECTED AUTO", copy_stdin=StringIO(data)) ``` When connection option `disable_copy_local` set to True, disables COPY LOCAL operations, including copying data from local files/stdin and using local files to store data and exceptions. You can use this property to prevent users from writing to and copying from files on a Vertica host, including an MC host. Note that this property doesn't apply to `Cursor.copy()`. The data for copying from/writing to local files is streamed in chunks of `buffer_size` bytes, which defaults to 128 * 2 ** 10. When executing "COPY FROM LOCAL STDIN", `copy_stdin` should be a file-like object or a list of file-like objects (specifically, any object with a `read()` method). ### Cancel the current database operation `Connection.cancel()` interrupts the processing of the current operation. Interrupting query execution will cause the cancelled method to raise a `vertica_python.errors.QueryCanceled`. If no query is being executed, it does nothing. You can call this function from a different thread/process than the one currently executing a database operation. ```python from multiprocessing import Process import time import vertica_python def cancel_query(connection, timeout=5): time.sleep(timeout) connection.cancel() # Example 1: Cancel the query before Cursor.execute() return. # The query stops executing in a shorter time after the cancel message is sent. with vertica_python.connect(**conn_info) as conn: cur = conn.cursor() # Call cancel() from a different process p1 = Process(target=cancel_query, args=(conn,)) p1.start() try: cur.execute("") except vertica_python.errors.QueryCanceled as e: pass p1.join() # Example 2: Cancel the query after Cursor.execute() return. # Less number of rows read after the cancel message is sent. with vertica_python.connect(**conn_info) as conn: cur = conn.cursor() cur.execute("SELECT id, time FROM large_table") nCount = 0 try: while cur.fetchone(): nCount += 1 if nCount == 100: conn.cancel() except vertica_python.errors.QueryCanceled as e: pass # nCount is less than the number of rows in large_table ``` ### SQL Data conversion to Python objects When a query is executed and `Cursor.fetch*()` is called, SQL data (bytes) are deserialized as Python objects. The following table shows the default mapping from SQL data types to Python objects: | SQL data type | Python object type | | -------------- | ------------------ | | NULL | None | | BOOLEAN | bool | | INTEGER | int | | FLOAT | float | | NUMERIC | [decimal.Decimal](https://docs.python.org/3/library/decimal.html#decimal.Decimal) | | CHAR | str | | VARCHAR | str | | LONG VARCHAR | str | | BINARY | bytes | | VARBINARY | bytes | | LONG VARBINARY | bytes | | UUID | [uuid.UUID](https://docs.python.org/3/library/uuid.html#uuid.UUID) | | DATE | datetime.date[1] | | TIME | datetime.time[2] | | TIMETZ | datetime.time[2] | | TIMESTAMP | datetime.datetime[1] | | TIMESTAMPTZ | datetime.datetime[1] | | INTERVAL | [dateutil.relativedelta.relativedelta](https://dateutil.readthedocs.io/en/stable/relativedelta.html#dateutil.relativedelta.relativedelta) | | ARRAY | list[3] | | SET | set[3] | | ROW | dict[3] | | MAP | dict[3] | [1]Python’s datetime.date and datetime.datetime only supports date ranges 0001-01-01 to 9999-12-31. Retrieving a value of BC date or future date (year>9999) results in an error. [2]Python’s datetime.time only supports times until 23:59:59. Retrieving a value of 24:00:00 results in an error. [3]If connection option 'request_complex_types' set to _False_, the server returns all complex types as VARCHAR/LONG VARCHAR Json strings, so the client will convert data to _str_ instead. Server before v12.0.2 cannot provide enough metadata for complex types, the behavior is equal to request_complex_types=False. #### Bypass data conversion to Python objects The `Cursor.disable_sqldata_converter` attribute can bypass the result data conversion to Python objects. ```python with vertica_python.connect(**conn_info) as conn: cur = conn.cursor() sql = "select 'foo'::VARCHAR, 100::INT, '2001-12-01 02:50:00'::TIMESTAMP" #### Convert SQL types to Python objects #### print(cur.disable_sqldata_converter) # Default is False # False cur.execute(sql) print(cur.fetchall()) # [['foo', 100, datetime.datetime(2001, 12, 1, 2, 50)]] #### No Conversion: return raw bytes data #### cur.disable_sqldata_converter = True # Set attribute to True cur.execute(sql) print(cur.fetchall()) # [[b'foo', b'100', b'2001-12-01 02:50:00']] ``` As a result, this can improve query performance when you call `fetchall()` but ignore/skip result data. #### Customize data conversion to Python objects The `Cursor.register_sqldata_converter(oid, converter_func)` method allows to customize how SQL data values are converted to Python objects when query results are returned. PARAMETERS: - oid – The Vertica type OID to manage. - converter_func – The converter function to register for oid. The function should have two arguments <`val`, `ctx`>. [Data Transfer Format](#data-transfer-format) matters for `val` (SQL data value). `ctx` is a dict managing resources that may be used by convertions. E.g. `ctx['column'].format_code` would be 0 (Text transfer format) or 1 (Binary transfer format). The `Cursor.unregister_sqldata_converter(oid)` method allows to cancel customization and use the default converter. Each Vertica type OID is an integer representing a SQL type, you can look up OIDs in `vertica_python.datatypes`: ``` $ python3 >>> from vertica_python.datatypes import VerticaType >>> {k:v for k,v in dict(VerticaType.__dict__).items() if not k.startswith('_')} {'UNKNOWN': 4, 'BOOL': 5, 'INT8': 6, 'FLOAT8': 7, 'CHAR': 8, 'VARCHAR': 9, 'DATE': 10, 'TIME': 11, 'TIMESTAMP': 12, 'TIMESTAMPTZ': 13, 'INTERVAL': 14, 'INTERVALYM': 114, 'TIMETZ': 15, 'NUMERIC': 16, 'VARBINARY': 17, 'UUID': 20, 'LONGVARCHAR': 115, 'LONGVARBINARY': 116, 'BINARY': 117, 'ROW': 300, 'ARRAY': 301, 'MAP': 302, 'ARRAY1D_BOOL': 1505, 'ARRAY1D_INT8': 1506, 'ARRAY1D_FLOAT8': 1507, 'ARRAY1D_CHAR': 1508, 'ARRAY1D_VARCHAR': 1509, 'ARRAY1D_DATE': 1510, 'ARRAY1D_TIME': 1511, 'ARRAY1D_TIMESTAMP': 1512, 'ARRAY1D_TIMESTAMPTZ': 1513, 'ARRAY1D_INTERVAL': 1514, 'ARRAY1D_INTERVALYM': 1521, 'ARRAY1D_TIMETZ': 1515, 'ARRAY1D_NUMERIC': 1516, 'ARRAY1D_VARBINARY': 1517, 'ARRAY1D_UUID': 1520, 'ARRAY1D_BINARY': 1522, 'ARRAY1D_LONGVARCHAR': 1519, 'ARRAY1D_LONGVARBINARY': 1518, 'SET_BOOL': 2705, 'SET_INT8': 2706, 'SET_FLOAT8': 2707, 'SET_CHAR': 2708, 'SET_VARCHAR': 2709, 'SET_DATE': 2710, 'SET_TIME': 2711, 'SET_TIMESTAMP': 2712, 'SET_TIMESTAMPTZ': 2713, 'SET_INTERVAL': 2714, 'SET_INTERVALYM': 2721, 'SET_TIMETZ': 2715, 'SET_NUMERIC': 2716, 'SET_VARBINARY': 2717, 'SET_UUID': 2720, 'SET_BINARY': 2722, 'SET_LONGVARCHAR': 2719, 'SET_LONGVARBINARY': 2718} >>> >>> # Use VerticaType.XXXX to refer to an OID >>> VerticaType.VARCHAR 9 >>> >>> from vertica_python.datatypes import TYPENAME >>> TYPENAME # mapping from OIDs to readable names {4: 'Unknown', 5: 'Boolean', 6: 'Integer', 7: 'Float', 8: 'Char', 9: 'Varchar', 115: 'Long Varchar', 10: 'Date', 11: 'Time', 15: 'TimeTz', 12: 'Timestamp', 13: 'TimestampTz', 117: 'Binary', 17: 'Varbinary', 116: 'Long Varbinary', 16: 'Numeric', 20: 'Uuid', 300: 'Row', 301: 'Array', 302: 'Map', 1505: 'Array[Boolean]', 1506: 'Array[Int8]', 1507: 'Array[Float8]', 1508: 'Array[Char]', 1509: 'Array[Varchar]', 1510: 'Array[Date]', 1511: 'Array[Time]', 1512: 'Array[Timestamp]', 1513: 'Array[TimestampTz]', 1515: 'Array[TimeTz]', 1516: 'Array[Numeric]', 1517: 'Array[Varbinary]', 1520: 'Array[Uuid]', 1522: 'Array[Binary]', 1519: 'Array[Long Varchar]', 1518: 'Array[Long Varbinary]', 2705: 'Set[Boolean]', 2706: 'Set[Int8]', 2707: 'Set[Float8]', 2708: 'Set[Char]', 2709: 'Set[Varchar]', 2710: 'Set[Date]', 2711: 'Set[Time]', 2712: 'Set[Timestamp]', 2713: 'Set[TimestampTz]', 2715: 'Set[TimeTz]', 2716: 'Set[Numeric]', 2717: 'Set[Varbinary]', 2720: 'Set[Uuid]', 2722: 'Set[Binary]', 2719: 'Set[Long Varchar]', 2718: 'Set[Long Varbinary]'} ``` **Example: Vertica numeric to Python float** Normally Vertica NUMERIC values are converted to Python decimal.Decimal instances, because both the types allow fixed-precision arithmetic and are not subject to rounding. Sometimes, however, you may want to perform floating-point math on NUMERIC values, and decimal.Decimal may get in the way. If you are fine with the potential loss of precision and you simply want to receive NUMERIC values as Python float, you can register a converter on NUMERIC. ```python import vertica_python from vertica_python.datatypes import VerticaType conn_info = {'host': '127.0.0.1', 'port': 5433, ... } conn = vertica_python.connect(**conn_info) cur = conn.cursor() cur.execute("SELECT 123.45::NUMERIC") print(cur.fetchone()[0]) # Decimal('123.45') def convert_numeric(val, ctx): # val: bytes - this is a text representation of NUMERIC value # ctx: dict - some info that may be useful to the converter return float(val) cur.register_sqldata_converter(VerticaType.NUMERIC, convert_numeric) cur.execute("SELECT 123.45::NUMERIC") print(cur.fetchone()[0]) # 123.45 cur.unregister_sqldata_converter(VerticaType.NUMERIC) # reset cur.execute("SELECT 123.45::NUMERIC") print(cur.fetchone()[0]) # Decimal('123.45') ``` **Example: Vertica complex types** The raw bytes data of complex types (ARRAY, MAP, ROW, SET) are in JSON format for both Text & Binary transfer format. ```python import json import numpy as np import vertica_python from vertica_python.datatypes import VerticaType conn_info = {'host': '127.0.0.1', 'port': 5433, 'binary_transfer': False/True, ... } conn = vertica_python.connect(**conn_info) cur = conn.cursor() #================================================== cur.execute("SELECT ARRAY[-1.234, 0, 1.66, null, 50]::ARRAY[FLOAT]") data = cur.fetchone()[0] print(type(data)) # print(data) # [-1.234, 0.0, 1.66, None, 50.0] numpy_data = np.array(data) # This is equal to the query value below #================================================== def convert_array(val, ctx): # val: b'[-1.234,0.0,1.66,null,50.0]' json_data = json.loads(val) return np.array(json_data) # VerticaType.ARRAY1D_FLOAT8 represents one-dimensional array of FLOAT type cur.register_sqldata_converter(VerticaType.ARRAY1D_FLOAT8, convert_array) cur.execute("SELECT ARRAY[-1.234, 0, 1.66, null, 50]::ARRAY[FLOAT]") data = cur.fetchone()[0] print(type(data)) # print(data) # [-1.234 0.0 1.66 None 50.0] #================================================== # VerticaType.ARRAY represents multidimensional array or contain ROWs cur.register_sqldata_converter(VerticaType.ARRAY, convert_array) cur.execute("SELECT ARRAY[ARRAY[-1, 234, 5],ARRAY[88, 0, 19]]::ARRAY[ARRAY[INT]]") data = cur.fetchone()[0] print(type(data)) # print(data) #[[ -1 234 5] # [ 88 0 19]] ``` ```python import pandas import vertica_python from io import BytesIO from vertica_python.datatypes import VerticaType conn_info = {'host': '127.0.0.1', 'port': 5433, 'binary_transfer': False/True, ... } conn = vertica_python.connect(**conn_info) cur = conn.cursor() cur.execute("SELECT ROW(ROW('a','b') as row1, ROW('c','d') as row2)") data = cur.fetchone()[0] print(type(data)) # print(data) # {'row1': {'f0': 'a', 'f1': 'b'}, 'row2': {'f0': 'c', 'f1': 'd'}} def convert_row(val, ctx): # val: b'{"row1":{"f0":"a","f1":"b"},"row2":{"f0":"c","f1":"d"}}' return pandas.read_json(BytesIO(val), orient='index') cur.register_sqldata_converter(VerticaType.ROW, convert_row) cur.execute("SELECT ROW(ROW('a','b') as row1, ROW('c','d') as row2)") data = cur.fetchone()[0] print(type(data)) # print(data) # f0 f1 # row1 a b # row2 c d ``` If you want to learn how default converters for each transfer format and oid works, look at the source code at `vertica_python/vertica/deserializer.py` ### Shortcuts The `Cursor.execute()` method returns `self`. This means that you can chain a fetch operation, such as `fetchone()`, to the `execute()` call: ```python row = cursor.execute(...).fetchone() for row in cur.execute(...).fetchall(): ... ``` ## Rowcount oddities vertica_python behaves a bit differently than dbapi when returning rowcounts. After a select execution, the rowcount will be -1, indicating that the row count is unknown. The rowcount value will be updated as data is streamed. ```python cur.execute('SELECT 10 things') cur.rowcount == -1 # indicates unknown rowcount cur.fetchone() cur.rowcount == 1 cur.fetchone() cur.rowcount == 2 cur.fetchall() cur.rowcount == 10 ``` After an insert/update/delete, the rowcount will be returned as a single element row: ```python cur.execute("DELETE 3 things") cur.rowcount == -1 # indicates unknown rowcount cur.fetchone()[0] == 3 ``` ## UTF-8 encoding issues While Vertica expects varchars stored to be UTF-8 encoded, sometimes invalid strings get into the database. You can specify how to handle reading these characters using the unicode_error connection option. This uses the same values as the unicode type (https://docs.python.org/3/library/codecs.html#error-handlers) ```python cur = vertica_python.Connection({..., 'unicode_error': 'strict'}).cursor() cur.execute(r"SELECT E'\xC2'") cur.fetchone() # caught 'utf8' codec can't decode byte 0xc2 in position 0: unexpected end of data cur = vertica_python.Connection({..., 'unicode_error': 'replace'}).cursor() cur.execute(r"SELECT E'\xC2'") cur.fetchone() # � cur = vertica_python.Connection({..., 'unicode_error': 'ignore'}).cursor() cur.execute(r"SELECT E'\xC2'") cur.fetchone() # ``` ## License Apache 2.0 License, please see `LICENSE` for details. ## Contributing guidelines Have a bug or an idea? Please see `CONTRIBUTING.md` for details. ## Acknowledgements We would like to thank the contributors to the Ruby Vertica gem (https://github.com/sprsquish/vertica), as this project gave us inspiration and help in understanding Vertica's wire protocol. These contributors are: * [Matt Bauer](http://github.com/mattbauer) * [Jeff Smick](http://github.com/sprsquish) * [Willem van Bergen](http://github.com/wvanbergen) * [Camilo Lopez](http://github.com/camilo) vertica-python-1.4.0/Vagrantfile000066400000000000000000000016261464740151100166650ustar00rootroot00000000000000# -*- mode: ruby -*- # vi: set ft=ruby : ENV["VAGRANT_DEFAULT_PROVIDER"] ||= "docker" VAGRANTFILE_API_VERSION = "2" ####################################################################### # This will set up a box with Vertica Community Edition 7.1.1 # running inside the box in a Docker container. # # The purpose is to have a Vertica instance that can be used by tests. # # Vertica's port 5433 is exposed to host machine. # Database 'docker' is available. # User is 'dbadmin' with no password. # # >>> # ! As is, any data stored inside Vertica will not live through # ! container or VM restart. # >>> ####################################################################### Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.vm.provider "docker" do |d| d.image = "sumitchawla/vertica:latest" d.ports = ["5433:5433"] end config.vm.synced_folder ".", "/vagrant", disabled: true end vertica-python-1.4.0/requirements-dev.txt000066400000000000000000000000701464740151100205300ustar00rootroot00000000000000pytest pytest-timeout python-dateutil six tox #kerberos vertica-python-1.4.0/setup.py000066400000000000000000000072001464740151100162040ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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. #!/usr/bin/env python import collections from setuptools import setup, find_packages ReqOpts = collections.namedtuple('ReqOpts', ['skip_requirements_regex', 'default_vcs']) opts = ReqOpts(None, 'git') # version should use the format 'x.x.x' (instead of 'vx.x.x') setup( name='vertica-python', version='1.4.0', description='Official native Python client for the Vertica database.', long_description="vertica-python is the official Vertica database client for the Python programming language. Please check the [project homepage](https://github.com/vertica/vertica-python) for the details.", long_description_content_type='text/markdown', author='Justin Berka, Alex Kim, Siting Ren', author_email='justin.berka@gmail.com, alex.kim@uber.com, sitingren@hotmail.com', url='https://github.com/vertica/vertica-python', keywords="database vertica", packages=find_packages(), license="Apache License 2.0", # 'pip install' will check this and refuse to install the project if the version does not match. python_requires=">=3.7", install_requires=[ 'python-dateutil>=1.5', 'six>=1.10.0' ], classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Database", "Topic :: Database :: Database Engines/Servers", "Topic :: Database :: Front-Ends", "Topic :: Software Development :: Libraries :: Python Modules", "Operating System :: OS Independent" ] ) vertica-python-1.4.0/tox.ini000066400000000000000000000002741464740151100160110ustar00rootroot00000000000000[tox] envlist = py37,py38,py39,py310,py311,py312 [testenv] passenv = * commands = pytest {posargs} deps = pytest pytest-timeout parameterized python-dateutil mock vertica-python-1.4.0/vertica_python/000077500000000000000000000000001464740151100175315ustar00rootroot00000000000000vertica-python-1.4.0/vertica_python/__init__.py000066400000000000000000000062131464740151100216440ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from .vertica.connection import Connection, connect, parse_dsn # Importing exceptions for compatibility with dbapi 2.0. # See: PEP 249 - Python Database API 2.0 # https://www.python.org/dev/peps/pep-0249/#exceptions from . import errors from .errors import ( Error, Warning, DataError, DatabaseError, IntegrityError, InterfaceError, InternalError, NotSupportedError, OperationalError, ProgrammingError) # Main module for this library. __author__ = 'Vertica' __copyright__ = 'Copyright (c) 2018-2024 Open Text.' __license__ = 'Apache 2.0' __all__ = ['Connection', 'PROTOCOL_VERSION', 'version_info', 'apilevel', 'threadsafety', 'paramstyle', 'connect', 'parse_dsn', 'Error', 'Warning', 'DataError', 'DatabaseError', 'IntegrityError', 'InterfaceError', 'InternalError', 'NotSupportedError', 'OperationalError', 'ProgrammingError'] # The version number of this library. version_info = (1, 4, 0) __version__ = '.'.join(map(str, version_info)) # The protocol version (3.16) implemented in this library. PROTOCOL_VERSION = 3 << 16 | 16 apilevel = 2.0 threadsafety = 1 # Threads may share the module, but not connections! # Accepted paramstyles are # 'qmark' = Question mark style, e.g. '...WHERE name=?' # 'named' = Named style, e.g. '...WHERE name=:name' # 'format' = ANSI C printf format codes, e.g. '...WHERE name=%s' paramstyle = 'named' vertica-python-1.4.0/vertica_python/compat.py000066400000000000000000000115231464740151100213700ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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. # Copyright 2015 The TensorFlow Authors. All Rights Reserved. # # 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. # ============================================================================== """Functions for Python 2 vs. 3 compatibility. ## Conversion routines In addition to the functions below, `as_str` converts an object to a `str`. @@as_bytes @@as_text @@as_str_any ## Types The compatibility module also provides the following types: * `bytes_or_text_types` * `complex_types` * `integral_types` * `real_types` """ from __future__ import absolute_import from __future__ import division from __future__ import print_function import six as _six def as_bytes(bytes_or_text, encoding='utf-8'): """Converts either bytes or unicode to `bytes`, using utf-8 encoding for text. Args: bytes_or_text: A `bytes`, `bytearray`, `str`, or `unicode` object. encoding: A string indicating the charset for encoding unicode. Returns: A `bytes` object. Raises: TypeError: If `bytes_or_text` is not a binary or unicode string. """ if isinstance(bytes_or_text, _six.text_type): return bytes_or_text.encode(encoding) elif isinstance(bytes_or_text, bytearray): return bytes(bytes_or_text) elif isinstance(bytes_or_text, bytes): return bytes_or_text else: raise TypeError('Expected binary or unicode string, got %r' % (bytes_or_text,)) def as_text(bytes_or_text, encoding='utf-8'): """Returns the given argument as a unicode string. Args: bytes_or_text: A `bytes`, `bytearray`, `str`, or `unicode` object. encoding: A string indicating the charset for decoding unicode. Returns: A `unicode` (Python 2) or `str` (Python 3) object. Raises: TypeError: If `bytes_or_text` is not a binary or unicode string. """ if isinstance(bytes_or_text, _six.text_type): return bytes_or_text elif isinstance(bytes_or_text, (bytes, bytearray)): return bytes_or_text.decode(encoding) else: raise TypeError('Expected binary or unicode string, got %r' % bytes_or_text) # Convert an object to a `str` in both Python 2 and 3. if _six.PY2: as_str = as_bytes else: as_str = as_text def as_str_any(value): """Converts to `str` as `str(value)`, but use `as_str` for `bytes`. Args: value: A object that can be converted to `str`. Returns: A `str` object. """ if isinstance(value, (bytes, bytearray)): return as_str(value) else: return str(value) # Either bytes or text. bytes_or_text_types = (bytes, bytearray, _six.text_type) _allowed_symbols = [ 'as_str', 'bytes_or_text_types', ] vertica-python-1.4.0/vertica_python/datatypes.py000066400000000000000000000527601464740151100221130ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from datetime import date, datetime, time, timezone from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Optional # noinspection PyPep8Naming def Date(year, month, day): return date(year, month, day) # noinspection PyPep8Naming def Time(hour, minute, second): return time(hour, minute, second) # noinspection PyPep8Naming def Timestamp(year, month, day, hour, minute, second): return datetime(year, month, day, hour, minute, second) # noinspection PyPep8Naming def DateFromTicks(ticks): d = datetime.fromtimestamp(ticks, timezone.utc) return d.date() # noinspection PyPep8Naming def TimeFromTicks(ticks): d = datetime.fromtimestamp(ticks, timezone.utc) return d.time() # noinspection PyPep8Naming def TimestampFromTicks(ticks): d = datetime.fromtimestamp(ticks, timezone.utc) return d.time() class Bytea(str): pass # noinspection PyPep8Naming def Binary(string): return Bytea(string) class VerticaType(object): UNKNOWN = 4 BOOL = 5 INT8 = 6 FLOAT8 = 7 CHAR = 8 VARCHAR = 9 DATE = 10 TIME = 11 TIMESTAMP = 12 TIMESTAMPTZ = 13 INTERVAL = 14 INTERVALYM = 114 TIMETZ = 15 NUMERIC = 16 VARBINARY = 17 UUID = 20 LONGVARCHAR = 115 LONGVARBINARY = 116 BINARY = 117 ROW = 300 ARRAY = 301 # multidimensional array or contain ROWs MAP = 302 # one-dimensional array of a primitive type ARRAY1D_BOOL = 1505 ARRAY1D_INT8 = 1506 ARRAY1D_FLOAT8 = 1507 ARRAY1D_CHAR = 1508 ARRAY1D_VARCHAR = 1509 ARRAY1D_DATE = 1510 ARRAY1D_TIME = 1511 ARRAY1D_TIMESTAMP = 1512 ARRAY1D_TIMESTAMPTZ = 1513 ARRAY1D_INTERVAL = 1514 ARRAY1D_INTERVALYM = 1521 ARRAY1D_TIMETZ = 1515 ARRAY1D_NUMERIC = 1516 ARRAY1D_VARBINARY = 1517 ARRAY1D_UUID = 1520 ARRAY1D_BINARY = 1522 ARRAY1D_LONGVARCHAR = 1519 ARRAY1D_LONGVARBINARY = 1518 # one-dimensional set of a primitive type SET_BOOL = 2705 SET_INT8 = 2706 SET_FLOAT8 = 2707 SET_CHAR = 2708 SET_VARCHAR = 2709 SET_DATE = 2710 SET_TIME = 2711 SET_TIMESTAMP = 2712 SET_TIMESTAMPTZ = 2713 SET_INTERVAL = 2714 SET_INTERVALYM = 2721 SET_TIMETZ = 2715 SET_NUMERIC = 2716 SET_VARBINARY = 2717 SET_UUID = 2720 SET_BINARY = 2722 SET_LONGVARCHAR = 2719 SET_LONGVARBINARY = 2718 def __init__(self, *values): self.values = values def __cmp__(self, other): if other in self.values: return 0 if other < self.values: return 1 else: return -1 def __eq__(self, other): return other in self.values def __ne__(self, other): return other not in self.values # dbapi: type object used to describe columns that are string-based STRING = VerticaType(VerticaType.CHAR, VerticaType.VARCHAR, VerticaType.BINARY, VerticaType.VARBINARY, VerticaType.UNKNOWN, VerticaType.LONGVARBINARY, VerticaType.LONGVARCHAR) # dbapi: type object used to describe (long) binary columns BINARY = VerticaType(VerticaType.BINARY, VerticaType.VARBINARY, VerticaType.LONGVARBINARY) # dbapi: type object used to describe numeric columns NUMBER = VerticaType(VerticaType.INT8, VerticaType.FLOAT8, VerticaType.NUMERIC) # dbapi: type object used to describe date/time columns DATETIME = VerticaType(VerticaType.DATE, VerticaType.TIME, VerticaType.TIMETZ, VerticaType.TIMESTAMP, VerticaType.TIMESTAMPTZ, VerticaType.INTERVAL, VerticaType.INTERVALYM) # dbapi: type object used to describe the "Row ID" column ROWID = VerticaType() # Vertica doesn't have row_id type # the max size of a CHAR/VARCHAR/BINARY/VARBINARY MAX_STRING_LEN = 65000 # the max size of a LONG VARCHAR/LONG VARBINARY MAX_LONG_STRING_LEN = 32000000 # interval mask constants # these masks determine the range of an interval for a given type_modifier # for example, an interval has range "Day to Hour" if: # (type_modifier & INTERVAL_MASK_DAY2HOUR) == INTERVAL_MASK_DAY2HOUR INTERVAL_MASK_MONTH = 1 << 17 INTERVAL_MASK_YEAR = 1 << 18 INTERVAL_MASK_DAY = 1 << 19 INTERVAL_MASK_HOUR = 1 << 26 INTERVAL_MASK_MINUTE = 1 << 27 INTERVAL_MASK_SECOND = 1 << 28 INTERVAL_MASK_YEAR2MONTH = INTERVAL_MASK_YEAR | INTERVAL_MASK_MONTH INTERVAL_MASK_DAY2HOUR = INTERVAL_MASK_DAY | INTERVAL_MASK_HOUR INTERVAL_MASK_DAY2MIN = INTERVAL_MASK_DAY | INTERVAL_MASK_HOUR | INTERVAL_MASK_MINUTE INTERVAL_MASK_DAY2SEC = INTERVAL_MASK_DAY | INTERVAL_MASK_HOUR | INTERVAL_MASK_MINUTE | INTERVAL_MASK_SECOND INTERVAL_MASK_HOUR2MIN = INTERVAL_MASK_HOUR | INTERVAL_MASK_MINUTE INTERVAL_MASK_HOUR2SEC = INTERVAL_MASK_HOUR | INTERVAL_MASK_MINUTE | INTERVAL_MASK_SECOND INTERVAL_MASK_MIN2SEC = INTERVAL_MASK_MINUTE | INTERVAL_MASK_SECOND TYPENAME = { VerticaType.UNKNOWN: "Unknown", VerticaType.BOOL: "Boolean", VerticaType.INT8: "Integer", VerticaType.FLOAT8: "Float", VerticaType.CHAR: "Char", VerticaType.VARCHAR: "Varchar", VerticaType.LONGVARCHAR: "Long Varchar", VerticaType.DATE: "Date", VerticaType.TIME: "Time", VerticaType.TIMETZ: "TimeTz", VerticaType.TIMESTAMP: "Timestamp", VerticaType.TIMESTAMPTZ: "TimestampTz", VerticaType.BINARY: "Binary", VerticaType.VARBINARY: "Varbinary", VerticaType.LONGVARBINARY: "Long Varbinary", VerticaType.NUMERIC: "Numeric", VerticaType.UUID: "Uuid", VerticaType.ROW: "Row", VerticaType.ARRAY: "Array", VerticaType.MAP: "Map", VerticaType.ARRAY1D_BOOL: "Array[Boolean]", VerticaType.ARRAY1D_INT8: "Array[Int8]", VerticaType.ARRAY1D_FLOAT8: "Array[Float8]", VerticaType.ARRAY1D_CHAR: "Array[Char]", VerticaType.ARRAY1D_VARCHAR: "Array[Varchar]", VerticaType.ARRAY1D_DATE: "Array[Date]", VerticaType.ARRAY1D_TIME: "Array[Time]", VerticaType.ARRAY1D_TIMESTAMP: "Array[Timestamp]", VerticaType.ARRAY1D_TIMESTAMPTZ: "Array[TimestampTz]", VerticaType.ARRAY1D_TIMETZ: "Array[TimeTz]", VerticaType.ARRAY1D_NUMERIC: "Array[Numeric]", VerticaType.ARRAY1D_VARBINARY: "Array[Varbinary]", VerticaType.ARRAY1D_UUID: "Array[Uuid]", VerticaType.ARRAY1D_BINARY: "Array[Binary]", VerticaType.ARRAY1D_LONGVARCHAR: "Array[Long Varchar]", VerticaType.ARRAY1D_LONGVARBINARY: "Array[Long Varbinary]", VerticaType.SET_BOOL: "Set[Boolean]", VerticaType.SET_INT8: "Set[Int8]", VerticaType.SET_FLOAT8: "Set[Float8]", VerticaType.SET_CHAR: "Set[Char]", VerticaType.SET_VARCHAR: "Set[Varchar]", VerticaType.SET_DATE: "Set[Date]", VerticaType.SET_TIME: "Set[Time]", VerticaType.SET_TIMESTAMP: "Set[Timestamp]", VerticaType.SET_TIMESTAMPTZ: "Set[TimestampTz]", VerticaType.SET_TIMETZ: "Set[TimeTz]", VerticaType.SET_NUMERIC: "Set[Numeric]", VerticaType.SET_VARBINARY: "Set[Varbinary]", VerticaType.SET_UUID: "Set[Uuid]", VerticaType.SET_BINARY: "Set[Binary]", VerticaType.SET_LONGVARCHAR: "Set[Long Varchar]", VerticaType.SET_LONGVARBINARY: "Set[Long Varbinary]", } COMPLEX_ELEMENT_TYPE = { VerticaType.ARRAY1D_BOOL: VerticaType.BOOL, VerticaType.ARRAY1D_INT8: VerticaType.INT8, VerticaType.ARRAY1D_FLOAT8: VerticaType.FLOAT8, VerticaType.ARRAY1D_CHAR: VerticaType.CHAR, VerticaType.ARRAY1D_VARCHAR: VerticaType.VARCHAR, VerticaType.ARRAY1D_DATE: VerticaType.DATE, VerticaType.ARRAY1D_TIME: VerticaType.TIME, VerticaType.ARRAY1D_TIMESTAMP: VerticaType.TIMESTAMP, VerticaType.ARRAY1D_TIMESTAMPTZ: VerticaType.TIMESTAMPTZ, VerticaType.ARRAY1D_TIMETZ: VerticaType.TIMETZ, VerticaType.ARRAY1D_INTERVAL: VerticaType.INTERVAL, VerticaType.ARRAY1D_INTERVALYM: VerticaType.INTERVALYM, VerticaType.ARRAY1D_NUMERIC: VerticaType.NUMERIC, VerticaType.ARRAY1D_VARBINARY: VerticaType.VARBINARY, VerticaType.ARRAY1D_UUID: VerticaType.UUID, VerticaType.ARRAY1D_BINARY: VerticaType.BINARY, VerticaType.ARRAY1D_LONGVARCHAR: VerticaType.LONGVARCHAR, VerticaType.ARRAY1D_LONGVARBINARY: VerticaType.LONGVARBINARY, VerticaType.SET_BOOL: VerticaType.BOOL, VerticaType.SET_INT8: VerticaType.INT8, VerticaType.SET_FLOAT8: VerticaType.FLOAT8, VerticaType.SET_CHAR: VerticaType.CHAR, VerticaType.SET_VARCHAR: VerticaType.VARCHAR, VerticaType.SET_DATE: VerticaType.DATE, VerticaType.SET_TIME: VerticaType.TIME, VerticaType.SET_TIMESTAMP: VerticaType.TIMESTAMP, VerticaType.SET_TIMESTAMPTZ: VerticaType.TIMESTAMPTZ, VerticaType.SET_TIMETZ: VerticaType.TIMETZ, VerticaType.SET_INTERVAL: VerticaType.INTERVAL, VerticaType.SET_INTERVALYM: VerticaType.INTERVALYM, VerticaType.SET_NUMERIC: VerticaType.NUMERIC, VerticaType.SET_VARBINARY: VerticaType.VARBINARY, VerticaType.SET_UUID: VerticaType.UUID, VerticaType.SET_BINARY: VerticaType.BINARY, VerticaType.SET_LONGVARCHAR: VerticaType.LONGVARCHAR, VerticaType.SET_LONGVARBINARY: VerticaType.LONGVARBINARY, } def getTypeName(data_type_oid: int, type_modifier: int) -> str: """Returns the base type name according to data_type_oid and type_modifier.""" if data_type_oid in TYPENAME: return TYPENAME[data_type_oid] elif data_type_oid in (VerticaType.INTERVAL, VerticaType.INTERVALYM): return "Interval " + getIntervalRange(data_type_oid, type_modifier) elif data_type_oid in (VerticaType.ARRAY1D_INTERVAL, VerticaType.ARRAY1D_INTERVALYM): return "Array[Interval {}]".format(getIntervalRange(COMPLEX_ELEMENT_TYPE[data_type_oid], type_modifier)) elif data_type_oid in (VerticaType.SET_INTERVAL, VerticaType.SET_INTERVALYM): return "Set[Interval {}]".format(getIntervalRange(COMPLEX_ELEMENT_TYPE[data_type_oid], type_modifier)) else: return "Unknown" def getComplexElementType(data_type_oid: int) -> Optional[int]: """For 1D ARRAY or SET, returns the type of its elements.""" return COMPLEX_ELEMENT_TYPE.get(data_type_oid) def getIntervalRange(data_type_oid: int, type_modifier: int) -> str: """Extracts an interval's range from the bits set in its type_modifier.""" if data_type_oid not in (VerticaType.INTERVAL, VerticaType.INTERVALYM): raise ValueError("Invalid data type OID: {}".format(data_type_oid)) if type_modifier == -1: # assume the default if data_type_oid == VerticaType.INTERVALYM: return "Year to Month" elif data_type_oid == VerticaType.INTERVAL: return "Day to Second" if data_type_oid == VerticaType.INTERVALYM: # Year/Month intervals if (type_modifier & INTERVAL_MASK_YEAR2MONTH) == INTERVAL_MASK_YEAR2MONTH: return "Year to Month" elif (type_modifier & INTERVAL_MASK_YEAR) == INTERVAL_MASK_YEAR: return "Year" elif (type_modifier & INTERVAL_MASK_MONTH) == INTERVAL_MASK_MONTH: return "Month" else: return "Year to Month" if data_type_oid == VerticaType.INTERVAL: # Day/Time intervals if (type_modifier & INTERVAL_MASK_DAY2SEC) == INTERVAL_MASK_DAY2SEC: return "Day to Second" elif (type_modifier & INTERVAL_MASK_DAY2MIN) == INTERVAL_MASK_DAY2MIN: return "Day to Minute" elif (type_modifier & INTERVAL_MASK_DAY2HOUR) == INTERVAL_MASK_DAY2HOUR: return "Day to Hour" elif (type_modifier & INTERVAL_MASK_DAY) == INTERVAL_MASK_DAY: return "Day" elif (type_modifier & INTERVAL_MASK_HOUR2SEC) == INTERVAL_MASK_HOUR2SEC: return "Hour to Second" elif (type_modifier & INTERVAL_MASK_HOUR2MIN) == INTERVAL_MASK_HOUR2MIN: return "Hour to Minute" elif (type_modifier & INTERVAL_MASK_HOUR) == INTERVAL_MASK_HOUR: return "Hour" elif (type_modifier & INTERVAL_MASK_MIN2SEC) == INTERVAL_MASK_MIN2SEC: return "Minute to Second" elif (type_modifier & INTERVAL_MASK_MINUTE) == INTERVAL_MASK_MINUTE: return "Minute" elif (type_modifier & INTERVAL_MASK_SECOND) == INTERVAL_MASK_SECOND: return "Second" else: return "Day to Second" def getIntervalLeadingPrecision(data_type_oid: int, type_modifier: int) -> int: """ Returns the leading precision for an interval, which is the largest number of digits that can fit in the leading field of the interval. All Year/Month intervals are defined in terms of months, even if the type_modifier forbids months to be specified (i.e. INTERVAL YEAR). Similarly, all Day/Time intervals are defined as a number of microseconds. Because of this, interval types with shorter ranges will have a larger leading precision. For example, an INTERVAL DAY's leading precision is ((2^63)-1)/MICROSECS_PER_DAY, while an INTERVAL HOUR's leading precision is ((2^63)-1)/MICROSECS_PER_HOUR """ interval_range = getIntervalRange(data_type_oid, type_modifier) if interval_range in ("Year", "Year to Month"): return 18 elif interval_range == "Month": return 19 elif interval_range in ("Day", "Day to Hour", "Day to Minute", "Day to Second"): return 9 elif interval_range in ("Hour", "Hour to Minute", "Hour to Second"): return 10 elif interval_range in ("Minute", "Minute to Second"): return 12 elif interval_range == "Second": return 13 else: raise ValueError("Invalid interval range: {}".format(interval_range)) def getPrecision(data_type_oid: int, type_modifier: int) -> Optional[int]: """ Returns the precision for the given Vertica type with consideration of the type modifier. For numerics, precision is the total number of digits (in base 10) that can fit in the type. For intervals, time and timestamps, precision is the number of digits to the right of the decimal point in the seconds portion of the time. The type modifier of -1 is used when the size of a type is unknown. In those cases we assume the maximum possible size. """ if data_type_oid == VerticaType.NUMERIC: if type_modifier == -1: return 1024 return ((type_modifier - 4) >> 16) & 0xFFFF elif data_type_oid in (VerticaType.TIME, VerticaType.TIMETZ, VerticaType.TIMESTAMP, VerticaType.TIMESTAMPTZ, VerticaType.INTERVAL, VerticaType.INTERVALYM): if type_modifier == -1: return 6 return type_modifier & 0xF else: return None # None if no meaningful values can be provided def getScale(data_type_oid: int, type_modifier: int) -> Optional[int]: """ Returns the scale for the given Vertica type with consideration of the type modifier. Returns None if no meaningful values can be provided. """ if data_type_oid == VerticaType.NUMERIC: return 15 if type_modifier == -1 else (type_modifier - 4) & 0xFF else: return None def getDisplaySize(data_type_oid: int, type_modifier: int) -> Optional[int]: """ Returns the column display size for the given Vertica type with consideration of the type modifier. Returns None if no meaningful values can be provided. The display size of a column is the maximum number of characters needed to display data in character form. """ if data_type_oid == VerticaType.BOOL: # T or F return 1 elif data_type_oid == VerticaType.INT8: # a sign and 19 digits if signed or 20 digits if unsigned return 20 elif data_type_oid == VerticaType.FLOAT8: # a sign, 15 digits, a decimal point, the letter E, a sign, and 3 digits return 22 elif data_type_oid == VerticaType.NUMERIC: # a sign, precision digits, and a decimal point return getPrecision(data_type_oid, type_modifier) + 2 elif data_type_oid == VerticaType.DATE: # yyyy-mm-dd, a space, and the calendar era (BC) return 13 elif data_type_oid == VerticaType.TIME: seconds_precision = getPrecision(data_type_oid, type_modifier) if seconds_precision == 0: # hh:mm:ss return 8 else: # hh:mm:ss.[fff...] return 9 + seconds_precision elif data_type_oid == VerticaType.TIMETZ: seconds_precision = getPrecision(data_type_oid, type_modifier) if seconds_precision == 0: # hh:mm:ss, a sign, hh:mm return 14 else: # hh:mm:ss.[fff...], a sign, hh:mm return 15 + seconds_precision elif data_type_oid == VerticaType.TIMESTAMP: seconds_precision = getPrecision(data_type_oid, type_modifier) if seconds_precision == 0: # yyyy-mm-dd hh:mm:ss, a space, and the calendar era (BC) return 22 else: # yyyy-mm-dd hh:mm:ss[.fff...], a space, and the calendar era (BC) return 23 + seconds_precision elif data_type_oid == VerticaType.TIMESTAMPTZ: seconds_precision = getPrecision(data_type_oid, type_modifier) if seconds_precision == 0: # yyyy-mm-dd hh:mm:ss, a sign, hh:mm, a space, and the calendar era (BC) return 28 else: # yyyy-mm-dd hh:mm:ss.[fff...], a sign, hh:mm, a space, and the calendar era (BC) return 29 + seconds_precision elif data_type_oid in (VerticaType.INTERVAL, VerticaType.INTERVALYM): leading_precision = getIntervalLeadingPrecision(data_type_oid, type_modifier) seconds_precision = getPrecision(data_type_oid, type_modifier) interval_range = getIntervalRange(data_type_oid, type_modifier) if interval_range in ("Year", "Month", "Day", "Hour", "Minute"): # a sign, [range...] return 1 + leading_precision elif interval_range in ("Day to Hour", "Year to Month", "Hour to Minute"): # a sign, [dd...] hh; a sign, [yy...]-mm; a sign, [hh...]:mm return 1 + leading_precision + 3 elif interval_range == "Day to Minute": # a sign, [dd...] hh:mm return 1 + leading_precision + 6 elif interval_range == "Second": if seconds_precision == 0: # a sign, [ss...] return 1 + leading_precision else: # a sign, [ss...].[fff...] return 1 + leading_precision + 1 + seconds_precision elif interval_range == "Day to Second": if seconds_precision == 0: # a sign, [dd...] hh:mm:ss return 1 + leading_precision + 9 else: # a sign, [dd...] hh:mm:ss.[fff...] return 1 + leading_precision + 10 + seconds_precision elif interval_range == "Hour to Second": if seconds_precision == 0: # a sign, [hh...]:mm:ss return 1 + leading_precision + 6 else: # a sign, [hh...]:mm:ss.[fff...] return 1 + leading_precision + 7 + seconds_precision elif interval_range == "Minute to Second": if seconds_precision == 0: # a sign, [mm...]:ss return 1 + leading_precision + 3 else: # a sign, [mm...]:ss.[fff...] return 1 + leading_precision + 4 + seconds_precision elif data_type_oid == VerticaType.UUID: # aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee return 36 elif data_type_oid in (VerticaType.CHAR, VerticaType.VARCHAR, VerticaType.BINARY, VerticaType.VARBINARY, VerticaType.UNKNOWN): # the defined maximum octet length of the column return MAX_STRING_LEN if type_modifier <= -1 else (type_modifier - 4) elif data_type_oid in (VerticaType.LONGVARCHAR, VerticaType.LONGVARBINARY): return MAX_LONG_STRING_LEN if type_modifier <= -1 else (type_modifier - 4) else: return None # None if no meaningful values can be provided vertica-python-1.4.0/vertica_python/errors.py000066400000000000000000000130171464740151100214210ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations import re from .vertica.mixins.notice_response_attr import _NoticeResponseAttrMixin ############################################# # dbapi errors ############################################# class Error(Exception): pass # noinspection PyShadowingBuiltins class Warning(Exception): pass class InterfaceError(Error): pass class DatabaseError(Error): pass class InternalError(DatabaseError): pass class OperationalError(DatabaseError): pass class ProgrammingError(DatabaseError): pass class IntegrityError(DatabaseError): pass class DataError(DatabaseError): pass class NotSupportedError(DatabaseError): pass # # Other Errors # class TimedOutError(OperationalError): pass class ConnectionError(DatabaseError): pass class KerberosError(ConnectionError): pass class SSLNotSupported(ConnectionError): pass class MessageError(InternalError): pass class EmptyQueryError(ProgrammingError): pass class QueryError(_NoticeResponseAttrMixin, ProgrammingError): """ An error occurred associated with a query had been issued. This error is perhaps the most commonly encountered error type, associated with failures during query execution, invalid SQL statements, and more. Much of the details of the error are available as properties, including the SQL statement that had been issued (`sql`). """ def __init__(self, error_response, sql): self.error_response = error_response self.sql = sql ProgrammingError.__init__(self, "{0}, SQL: {1}".format(error_response.error_message(), repr(self.one_line_sql()))) def one_line_sql(self): if self.sql: return re.sub(r"[\r\n]+", ' ', self.sql) else: return '' def __reduce__(self): # Workaround to make these exception instances (un)picklable. This must # be redefined by any subclass that changes the signature of __init__ # # This workaround applies to python3. https://bugs.python.org/issue37489 return ( type(self), (self.error_response, self.sql), self.__dict__, ) @property def _notice_attrs(self): # provided for _NoticeResponseAttrMixin if self.error_response is None: return {} return self.error_response._notice_attrs or {} @classmethod def from_error_response(cls, error_response, sql): klass = QUERY_ERROR_CLASSES.get(error_response.sqlstate, None) if klass is None: klass = cls return klass(error_response, sql) class LockFailure(QueryError): pass class InsufficientResources(QueryError): pass class OutOfMemory(QueryError): pass class VerticaSyntaxError(QueryError): pass class MissingSchema(QueryError): pass class MissingRelation(QueryError): pass class MissingColumn(QueryError): pass class CopyRejected(QueryError): pass class PermissionDenied(QueryError): pass class InvalidDatetimeFormat(QueryError): pass class DuplicateObject(QueryError): pass class QueryCanceled(QueryError): pass class ConnectionFailure(QueryError): pass class LostConnectivityFailure(QueryError): pass QUERY_ERROR_CLASSES = { '55V03': LockFailure, '53000': InsufficientResources, '53200': OutOfMemory, '42601': VerticaSyntaxError, '3F000': MissingSchema, '42V01': MissingRelation, '42703': MissingColumn, '22V04': CopyRejected, '42501': PermissionDenied, '22007': InvalidDatetimeFormat, '42710': DuplicateObject, '57014': QueryCanceled, '08006': ConnectionFailure, 'V1001': LostConnectivityFailure } vertica-python-1.4.0/vertica_python/os_utils.py000066400000000000000000000044021464740151100217440ustar00rootroot00000000000000# Copyright (c) 2020-2024 Open Text. # # 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. from __future__ import print_function, division, absolute_import, annotations import errno import os def ensure_dir_exists(filepath: str) -> None: """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 def check_file_readable(filename: str) -> None: """Ensure this is a readable file""" if not os.path.exists(filename): raise OSError('{} does not exist'.format(filename)) elif not os.path.isfile(filename): raise OSError('{} is not a file'.format(filename)) elif not os.access(filename, os.R_OK): raise OSError('{} is not readable'.format(filename)) def check_file_writable(filename: str) -> None: """Ensure this is a writable file. If the file doesn't exist, ensure its directory is writable. """ if os.path.exists(filename): if not os.path.isfile(filename): raise OSError('{} is not a file'.format(filename)) if not os.access(filename, os.W_OK): raise OSError('{} is not writable'.format(filename)) # If target does not exist, check permission on parent dir ensure_dir_exists(filename) pdir = os.path.dirname(filename) if not pdir: pdir = '.' if not os.path.isdir(pdir): raise OSError('{} is not a directory'.format(pdir)) if not os.access(pdir, os.W_OK): raise OSError('Directory {} is not writable'.format(pdir)) vertica-python-1.4.0/vertica_python/tests/000077500000000000000000000000001464740151100206735ustar00rootroot00000000000000vertica-python-1.4.0/vertica_python/tests/__init__.py000066400000000000000000000033061464740151100230060ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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.vertica-python-1.4.0/vertica_python/tests/common/000077500000000000000000000000001464740151100221635ustar00rootroot00000000000000vertica-python-1.4.0/vertica_python/tests/common/__init__.py000066400000000000000000000033061464740151100242760ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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.vertica-python-1.4.0/vertica_python/tests/common/base.py000066400000000000000000000142651464740151100234570ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations import os import sys import logging import unittest import inspect import getpass 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': '', 'database': '', 'oauth_access_token': '', 'oauth_user': '', } 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: 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 '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 python3.7, then # the log would write to $VP_TEST_LOG_DIR/py37/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_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.__class__.__name__+"."+self._testMethodName+'\n'+'-'*50) def tearDown(self): self.logger.info('\n'+'-'*10+' End '+self.__class__.__name__+"."+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, str): 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) vertica-python-1.4.0/vertica_python/tests/common/vp_test.conf.example000066400000000000000000000011711464740151100261500ustar00rootroot00000000000000# 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 # OAuth authentication information #VP_TEST_OAUTH_USER= #VP_TEST_OAUTH_ACCESS_TOKEN=****** vertica-python-1.4.0/vertica_python/tests/conftest.py000066400000000000000000000014651464740151100231000ustar00rootroot00000000000000# Copyright (c) 2019-2024 Open Text. # # 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. def pytest_configure(config): config.addinivalue_line( "markers", "integration_tests: mark test to be an integration test" ) config.addinivalue_line( "markers", "unit_tests: mark test to be an unit test" ) vertica-python-1.4.0/vertica_python/tests/integration_tests/000077500000000000000000000000001464740151100244405ustar00rootroot00000000000000vertica-python-1.4.0/vertica_python/tests/integration_tests/__init__.py000066400000000000000000000033061464740151100265530ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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.vertica-python-1.4.0/vertica_python/tests/integration_tests/base.py000066400000000000000000000171601464740151100257310ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations import pytest from ... import errors, connect from ..common.base import VerticaPythonTestCase @pytest.mark.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', 'oauth_access_token', 'oauth_user',] 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 # Note: The server-side prepared statements is disabled here. Please # see cls.createPrepStmtClass() below. 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._oauth_info = { 'access_token': cls.test_config['oauth_access_token'], 'user': cls.test_config['oauth_user'], } 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] @classmethod def createPrepStmtClass(cls): """Generates the code of a new subclass that has the same tests as this class but turns on the server-side prepared statements. To ensure test coverage, this method should be used if tests are not sensitive to paramstyles (or query protocols). Usage: "exec(xxxTestCase.createPrepStmtClass())" :return: a string acceptable by exec() to define the class """ base_cls_name = cls.__name__ cls_name = 'PrepStmt' + base_cls_name code = ('class ' + cls_name + '(' + base_cls_name + '):\n' ' @classmethod\n' ' def setUpClass(cls):\n' ' super(' + cls_name + ', cls).setUpClass()\n' " cls._conn_info['use_prepared_statements'] = True") return code 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_type=errors.ConnectionError, err_msg='Failed to establish a connection to the primary server or any backup address.'): with pytest.raises(err_type, match=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) def require_protocol_at_least(self, min_protocol_version): with self._connect() as conn: effective_protocol = conn.parameters['protocol_version'] if effective_protocol < min_protocol_version: msg = ("The test requires the effective protocol version to be at " "least {}.{}, but the current version is {}.{}.").format( min_protocol_version >> 16, min_protocol_version & 0x0000ffff, effective_protocol >> 16, effective_protocol & 0x0000ffff) self.skipTest(msg) vertica-python-1.4.0/vertica_python/tests/integration_tests/test_authentication.py000066400000000000000000000147571464740151100311060ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # # 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. from __future__ import print_function, division, absolute_import, annotations from .base import VerticaPythonIntegrationTestCase class AuthenticationTestCase(VerticaPythonIntegrationTestCase): def setUp(self): super(AuthenticationTestCase, self).setUp() self._user = self._conn_info['user'] self._password = self._conn_info['password'] def tearDown(self): self._conn_info['user'] = self._user self._conn_info['password'] = self._password if 'oauth_access_token' in self._conn_info: del self._conn_info['oauth_access_token'] super(AuthenticationTestCase, self).tearDown() def test_SHA512(self): with self._connect() as conn: cur = conn.cursor() cur.execute("DROP USER IF EXISTS sha512_user") cur.execute("DROP AUTHENTICATION IF EXISTS testIPv4hostHash CASCADE") cur.execute("DROP AUTHENTICATION IF EXISTS testIPv6hostHash CASCADE") cur.execute("DROP AUTHENTICATION IF EXISTS testlocalHash CASCADE") try: cur.execute("CREATE USER sha512_user IDENTIFIED BY 'password'") cur.execute("ALTER USER sha512_user SECURITY_ALGORITHM 'SHA512'") cur.execute("CREATE AUTHENTICATION testIPv4hostHash METHOD 'hash' HOST '0.0.0.0/0'") cur.execute("GRANT AUTHENTICATION testIPv4hostHash TO sha512_user") cur.execute("CREATE AUTHENTICATION testIPv6hostHash METHOD 'hash' HOST '::/0'") cur.execute("GRANT AUTHENTICATION testIPv6hostHash TO sha512_user") cur.execute("CREATE AUTHENTICATION testlocalHash METHOD 'hash' LOCAL") cur.execute("GRANT AUTHENTICATION testlocalHash TO sha512_user") self._conn_info['user'] = 'sha512_user' self._conn_info['password'] = 'password' expire_msg = 'The password for user sha512_user has expired' self.assertConnectionFail(err_msg=expire_msg) # Test SHA512 connection cur.execute("ALTER USER sha512_user IDENTIFIED BY 'password'") self.assertConnectionSuccess() # Switch to MD5 for hashing and storing the user password cur.execute("ALTER USER sha512_user SECURITY_ALGORITHM 'MD5'") self.assertConnectionFail(err_msg=expire_msg) # Test MD5 connection cur.execute("ALTER USER sha512_user IDENTIFIED BY 'password'") self.assertConnectionSuccess() finally: # Must clean up authentication methods within this session, no # matter whether an exception has occurred or not, otherwise # those authentication methods may affect existing users cur.execute("DROP USER IF EXISTS sha512_user") cur.execute("DROP AUTHENTICATION IF EXISTS testIPv4hostHash CASCADE") cur.execute("DROP AUTHENTICATION IF EXISTS testIPv6hostHash CASCADE") cur.execute("DROP AUTHENTICATION IF EXISTS testlocalHash CASCADE") def test_password_expire(self): with self._connect() as conn: cur = conn.cursor() cur.execute("DROP USER IF EXISTS pw_expire_user") cur.execute("DROP AUTHENTICATION IF EXISTS testIPv4hostHash CASCADE") cur.execute("DROP AUTHENTICATION IF EXISTS testIPv6hostHash CASCADE") cur.execute("DROP AUTHENTICATION IF EXISTS testlocalHash CASCADE") try: cur.execute("CREATE USER pw_expire_user IDENTIFIED BY 'password'") cur.execute("CREATE AUTHENTICATION testIPv4hostHash METHOD 'hash' HOST '0.0.0.0/0'") cur.execute("GRANT AUTHENTICATION testIPv4hostHash TO pw_expire_user") cur.execute("CREATE AUTHENTICATION testIPv6hostHash METHOD 'hash' HOST '::/0'") cur.execute("GRANT AUTHENTICATION testIPv6hostHash TO pw_expire_user") cur.execute("CREATE AUTHENTICATION testlocalHash METHOD 'hash' LOCAL") cur.execute("GRANT AUTHENTICATION testlocalHash TO pw_expire_user") # Test connection self._conn_info['user'] = 'pw_expire_user' self._conn_info['password'] = 'password' self.assertConnectionSuccess() # Expire the user's password immediately cur.execute("ALTER USER pw_expire_user PASSWORD EXPIRE") # Expect an error message self.assertConnectionFail( err_msg='The password for user pw_expire_user has expired') finally: # Must clean up authentication methods within this session, no # matter whether an exception has occurred or not, otherwise # those authentication methods may affect existing users cur.execute("DROP USER IF EXISTS pw_expire_user") cur.execute("DROP AUTHENTICATION IF EXISTS testIPv4hostHash CASCADE") cur.execute("DROP AUTHENTICATION IF EXISTS testIPv6hostHash CASCADE") cur.execute("DROP AUTHENTICATION IF EXISTS testlocalHash CASCADE") def test_oauth_access_token(self): self.require_protocol_at_least(3 << 16 | 11) if not self._oauth_info['access_token']: self.skipTest('OAuth access token not set') if not self._oauth_info['user'] and not self._conn_info['database']: self.skipTest('Both database and oauth_user are not set') self._conn_info['user'] = self._oauth_info['user'] self._conn_info['oauth_access_token'] = self._oauth_info['access_token'] with self._connect() as conn: cur = conn.cursor() cur.execute("SELECT authentication_method FROM sessions WHERE session_id=(SELECT current_session())") res = cur.fetchone() self.assertEqual(res[0], 'OAuth') exec(AuthenticationTestCase.createPrepStmtClass()) vertica-python-1.4.0/vertica_python/tests/integration_tests/test_cancel.py000066400000000000000000000102721464740151100273000ustar00rootroot00000000000000# Copyright (c) 2019-2024 Open Text. # # 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. from __future__ import print_function, division, absolute_import, annotations from multiprocessing import Process import pytest import time from .base import VerticaPythonIntegrationTestCase from ... import errors class CancelTestCase(VerticaPythonIntegrationTestCase): def test_cursor_cancel(self): # Cursor.cancel() should be not supported any more with self._connect() as conn: cursor = conn.cursor() with self.assertRaises(errors.NotSupportedError): cursor.cancel() def test_connection_cancel_no_query(self): with self._connect() as conn: cur = conn.cursor() # No query is being executed, cancel does nothing conn.cancel() @pytest.mark.timeout(30) def test_connection_cancel_running_query(self): def cancel_query(conn, delay=5): time.sleep(delay) conn.cancel() with self._connect() as conn: cur = conn.cursor() p1 = Process(target=cancel_query, args=(conn,)) p1.start() with self.assertRaises(errors.QueryCanceled): long_running_query = ('select count(*) from ' '(select node_name from CONFIGURATION_PARAMETERS) as a cross join ' '(select node_name from CONFIGURATION_PARAMETERS) as b cross join ' '(select node_name from CONFIGURATION_PARAMETERS) as c') cur.execute(long_running_query) p1.join() # Must be able to successfully run next query cur.execute("SELECT 1") res = cur.fetchall() self.assertListOfListsEqual(res, [[1]]) def test_connection_cancel_returned_query(self): with self._connect() as conn: cur = conn.cursor() cur.execute("DROP TABLE IF EXISTS vptest") try: # Creating and loading table cur.execute("CREATE TABLE vptest(id INTEGER, time TIMESTAMP)") cur.execute("INSERT INTO vptest" " SELECT row_number() OVER(), slice_time" " FROM(" " SELECT slice_time FROM(" " SELECT '2021-01-01'::timestamp s UNION ALL SELECT '2022-01-01'::timestamp s" " ) sq TIMESERIES slice_time AS '1 second' OVER(ORDER BY s)" " ) sq2") # This query returns over 30,000,000 rows. We cancel the command after # reading 100 of them, and then continue reading results. This quickly # results in an exception being thrown due to the cancel having taken effect. cur.execute("SELECT id, time FROM vptest") nCount = 0 with self.assertRaises(errors.QueryCanceled): while cur.fetchone(): nCount += 1 if nCount == 100: conn.cancel() # The number of rows read after the cancel message is sent to the server can vary. # 1,000,000 seems to leave a safe margin while still falling well short of # the 30,000,000+ rows we'd have read if the cancel didn't work. self.assertTrue(100 <= nCount < 1000000) # Must be able to successfully run next query cur.execute("SELECT 1") res = cur.fetchall() self.assertListOfListsEqual(res, [[1]]) finally: cur.execute("DROP TABLE IF EXISTS vptest") exec(CancelTestCase.createPrepStmtClass()) vertica-python-1.4.0/vertica_python/tests/integration_tests/test_column.py000066400000000000000000000110601464740151100273440ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations 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) def test_column_description(self): type_descriptions = [ ['boolVal', 5, 1, 1, None, None, True], ['intVal', 6, 20, 8, None, None, True], ['floatVal', 7, 22, 8, None, None, True], ['charVal', 8, 1, -1, None, None, True], ['varCharVal', 9, 128, -1, None, None, True], ['dateVal', 10, 13, 8, None, None, True], ['timestampVal', 12, 29, 8, 6, None, True], ['timestampTZVal', 13, 35, 8, 6, None, True], ['intervalVal', 14, 24, 8, 4, None, True], ['intervalYMVal', 114, 22, 8, 0, None, True], ['timeVal', 11, 15, 8, 6, None, True], ['timeTZVal', 15, 21, 8, 6, None, True], ['varBinVal', 17, 80, -1, None, None, True], ['uuidVal', 20, 36, 16, None, None, True], ['lVarCharVal', 115, 65536, -1, None, None, True], ['lVarBinaryVal', 116, 65536, -1, None, None, True], ['binaryVal', 117, 1, -1, None, None, True], ['numericVal', 16, 1002, -1, 1000, 18, True]] with self._connect() as conn: cur = conn.cursor() cur.execute("DROP TABLE IF EXISTS full_type_tbl") cur.execute("""CREATE TABLE full_type_tbl( boolVal BOOLEAN, intVal INT, floatVal FLOAT, charVal CHAR, varCharVal VARCHAR(128), dateVal DATE, timestampVal TIMESTAMP, timestampTZVal TIMESTAMPTZ, intervalVal INTERVAL DAY TO SECOND(4), intervalYMVal INTERVAL YEAR TO MONTH, timeVal TIME, timeTZVal TIMETZ, varBinVal VARBINARY, uuidVal UUID, lVarCharVal LONG VARCHAR(65536), lVarBinaryVal LONG VARBINARY(65536), binaryVal BINARY, numericVal NUMERIC(1000,18))""") cur.execute("SELECT * FROM full_type_tbl") self.assertListOfListsEqual([list(i) for i in cur.description], type_descriptions) cur.execute("DROP TABLE IF EXISTS full_type_tbl") exec(ColumnTestCase.createPrepStmtClass()) vertica-python-1.4.0/vertica_python/tests/integration_tests/test_connection.py000066400000000000000000000156741464740151100302250ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations import getpass import socket import uuid from .base import VerticaPythonIntegrationTestCase class ConnectionTestCase(VerticaPythonIntegrationTestCase): def tearDown(self): super(ConnectionTestCase, self).tearDown() if 'session_label' in self._conn_info: del self._conn_info['session_label'] if 'autocommit' in self._conn_info: del self._conn_info['autocommit'] if 'workload' in self._conn_info: del self._conn_info['workload'] def test_client_os_user_name_metadata(self): try: value = getpass.getuser() except Exception as e: value = '' # 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) def test_client_os_hostname_metadata(self): self.require_protocol_at_least(3 << 16 | 14) try: value = socket.gethostname() except Exception as e: value = '' # Metadata client_os_hostname sent from client should be captured into system tables query = 'SELECT client_os_hostname FROM v_monitor.current_session' res = self._query_and_fetchone(query) self.assertEqual(res[0], value) query = 'SELECT client_os_hostname 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_hostname 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_hostname FROM v_internal.dc_session_starts WHERE session_id=(SELECT current_session())' res = self._query_and_fetchone(query) self.assertEqual(res[0], value) def test_session_label(self): label = str(uuid.uuid1()) self._conn_info['session_label'] = label query = 'SELECT client_label FROM v_monitor.current_session' res = self._query_and_fetchone(query) self.assertEqual(res[0], label) query = 'SELECT client_label FROM v_monitor.sessions WHERE session_id=(SELECT current_session())' res = self._query_and_fetchone(query) self.assertEqual(res[0], label) query = 'SELECT client_label FROM v_monitor.user_sessions WHERE session_id=(SELECT current_session())' res = self._query_and_fetchone(query) self.assertEqual(res[0], label) query = 'SELECT client_label FROM v_internal.dc_session_starts WHERE session_id=(SELECT current_session())' res = self._query_and_fetchone(query) self.assertEqual(res[0], label) def test_autocommit_on(self): # Set with connection option self._conn_info['autocommit'] = True with self._connect() as conn: self.assertTrue(conn.autocommit) # Set with attribute setter conn.autocommit = False self.assertFalse(conn.autocommit) def test_autocommit_off(self): # Set with connection option self._conn_info['autocommit'] = False with self._connect() as conn: self.assertFalse(conn.autocommit) # Set with attribute setter conn.autocommit = True self.assertTrue(conn.autocommit) def test_workload_default(self): self.require_protocol_at_least(3 << 16 | 15) with self._connect() as conn: query = "SHOW WORKLOAD" res = self._query_and_fetchone(query) self.assertEqual(res[1], '') def test_workload_set_property(self): self.require_protocol_at_least(3 << 16 | 15) self._conn_info['workload'] = 'python_test_workload' with self._connect() as conn: # we use dc_client_server_messages to test that the client is working properly. # We do not regularly test on a multi subcluster database and the server will reject this # workload from the startup packet, returning a parameter status message with an empty string. query = ("SELECT contents FROM dc_client_server_messages" " WHERE session_id = current_session()" " AND message_type = '^+'" " AND contents LIKE '%workload%'") res = self._query_and_fetchone(query) self.assertEqual(res[0], 'workload: python_test_workload') exec(ConnectionTestCase.createPrepStmtClass()) vertica-python-1.4.0/vertica_python/tests/integration_tests/test_cursor.py000066400000000000000000001751621464740151100274020ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from datetime import date, datetime, time from dateutil.relativedelta import relativedelta from dateutil.tz import tzoffset from decimal import Decimal from io import open from uuid import UUID import logging import os import pytest import re import shutil import sys import tempfile from parameterized import parameterized from .base import VerticaPythonIntegrationTestCase from ... import errors """ There are a couple of testcases in this file, they are 1. CursorTestCase: general cursor tests, not sensitive to query protocols. 2. SimpleQueryTestCase: simple query protocol tests in execute(). 3. SimpleQueryExecutemanyTestCase: simple query protocol tests in executemany(). 4. PreparedStatementTestCase: prepared statements tests in both execute() and executemany(). Different query protocols use different paramstyles: - simple query protocol: 'named' and 'format' paramstyles - extended query protocol (prepared statements): 'qmark' paramstyle """ 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_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']]) # integration test for #345 def test_copy_with_file_like_object(self): class FileWrapper: read = property(lambda self: self.file.read) seek = property(lambda self: self.file.seek) write = property(lambda self: self.file.write) def __init__(self, file): self.file = file with tempfile.TemporaryFile() as f, self._connect() as conn: wrapped_f = FileWrapper(f) # object with a read() method wrapped_f.write(b"1,foo\n2,bar") # move rw pointer to top of file wrapped_f.seek(0) cur = conn.cursor() cur.copy( "COPY {0} (a, b) FROM STDIN DELIMITER ','".format(self._table), wrapped_f ) cur.execute("SELECT * FROM {0} ORDER BY a ASC".format(self._table)) res = cur.fetchall() self.assertListOfListsEqual(res, [[1, 'foo'], [2, 'bar']]) # integration test for #325 @parameterized.expand([ (tempfile.NamedTemporaryFile,), (tempfile.SpooledTemporaryFile,), (tempfile.TemporaryFile,), ]) def test_copy_with_temporary_file(self, temp_file_type): with temp_file_type() as f, self._connect() as conn: f.write(b"1,foo\n2,bar") f.seek(0) cur = conn.cursor() cur.copy( "COPY {0} (a, b) FROM STDIN DELIMITER ','".format(self._table), f, ) cur.execute("SELECT * FROM {0} ORDER BY a ASC".format(self._table)) res = cur.fetchall() self.assertListOfListsEqual(res, [[1, 'foo'], [2, 'bar']]) def test_copy_with_file(self): with tempfile.TemporaryFile() as f, self._connect() as conn1, self._connect() as conn2: 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']]) def test_copy_with_closed_file(self): with tempfile.TemporaryFile() as f, self._connect() as conn: f.write(b"1,foo\n2,bar") # move rw pointer to top of file f.seek(0) cur = conn.cursor() f.close() with pytest.raises(errors.DataError, match='closed file'): cur.copy("COPY {0} (a, b) FROM STDIN DELIMITER ','".format(self._table), f) # Must not close the cursor object and able to successfully run queries cur.execute("SELECT 1;") res = cur.fetchall() self.assertListOfListsEqual(res, [[1]]) # 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.MissingRelation 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.CopyRejected as e: cur.execute("SELECT 1;") res = cur.fetchall() self.assertListOfListsEqual(res, [[1]]) def test_copy_multiple_statements(self): with self._connect() as conn: cur = conn.cursor() cur.copy("COPY {0} (a, b) FROM STDIN DELIMITER ','; SELECT 5".format(self._table), "1,foo\n2,bar") self.assertListOfListsEqual(cur.fetchall(), []) self.assertTrue(cur.nextset()) self.assertListOfListsEqual(cur.fetchall(), [[5]]) self.assertFalse(cur.nextset()) def test_with_conn(self): with self._connect() as conn: cur = conn.cursor() cur.execute("INSERT INTO {0} (a, b) VALUES (1, 'aa');".format(self._table)) conn.commit() 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 {} (a, b) VALUES ({}, '{}')" .format(self._table, n, 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 {} (a, b) VALUES ({}, '{}')" .format(self._table, n, 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');".format(self._table)) cur.execute("COMMIT;") with self.assertRaises(errors.QueryError): cur.execute("SELECT * FROM {0}_fail".format(self._table)) # generate a user-defined error message err_msg = 'USER GENERATED ERROR: test error' with pytest.raises(errors.QueryError, match=err_msg): cur.execute("SELECT THROW_ERROR('test error')") # 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');".format(self._table)) cur.execute("COMMIT;") # (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() with pytest.raises(errors.InterfaceError, match='Cursor is closed'): cur.execute("SELECT 1;") cur = conn.cursor() def test_udtype(self): poly = "POLYGON ((1 2, 2 3, 3 1, 1 2))" line = "LINESTRING (42.1 71, 41.4 70, 41.3 72.9, 42.99 71.46, 44.47 73.21)" with self._connect() as conn: cur = conn.cursor() cur.execute("DROP TABLE IF EXISTS {0}".format(self._table)) cur.execute("CREATE TABLE {} (c1 GEOMETRY(10000), c2 GEOGRAPHY(1000))" .format(self._table)) cur.execute("INSERT INTO {} VALUES (ST_GeomFromText('{}'), ST_GeographyFromText('{}'))" .format(self._table, poly, line)) conn.commit() cur.execute("SELECT c1, c2, ST_AsText(c1), ST_AsText(c2) FROM {}".format(self._table)) res = cur.fetchall() self.assertEqual(res[0][2], poly) self.assertEqual(res[0][3], line) datatype_names = [col.type_name for col in cur.description] expected = ['geometry', 'geography', 'Long Varchar', 'Long Varchar'] self.assertListEqual(datatype_names, expected) self.assertEqual(cur.description[0].display_size, 10000) self.assertEqual(cur.description[1].display_size, 1000) def test_disable_sqldata_converter(self): with self._connect() as conn: cur = conn.cursor() # Default is False self.assertFalse(cur.disable_sqldata_converter) # Set with attribute setter cur.disable_sqldata_converter = True self.assertTrue(cur.disable_sqldata_converter) 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 a, b FROM {0} ORDER BY a ASC".format(self._table)) res = cur.fetchall() if conn.options['binary_transfer']: self.assertListOfListsEqual(res, [[b'\x00\x00\x00\x00\x00\x00\x00\x01', b'aa'], [b'\x00\x00\x00\x00\x00\x00\x00\x02', b'bb']]) else: self.assertListOfListsEqual(res, [[b'1', b'aa'], [b'2', b'bb']]) def test_custom_sqldata_converter(self): with self._connect() as conn: cur = conn.cursor() cur.register_sqldata_converter(5, lambda val, ctx: 'yes' if val == b't' else 'no') cur.execute("SELECT 't'::BOOL, NULL::BOOL, 'f'::BOOL") self.assertListOfListsEqual(cur.fetchall(), [['yes', None, 'no']]) cur.unregister_sqldata_converter(5) cur.execute("SELECT 't'::BOOL, NULL::BOOL, 'f'::BOOL") self.assertListOfListsEqual(cur.fetchall(), [[True, None, False]]) self._conn_info['binary_transfer'] = True with self._connect() as conn: cur = conn.cursor() cur.execute("SELECT 't'::BOOL, NULL::BOOL, 'f'::BOOL") self.assertListOfListsEqual(cur.fetchall(), [[True, None, False]]) cur.register_sqldata_converter(5, lambda val, ctx: 'on' if val == b'\x01' else 'off') cur.execute("SELECT 't'::BOOL, NULL::BOOL, 'f'::BOOL") self.assertListOfListsEqual(cur.fetchall(), [['on', None, 'off']]) cur.unregister_sqldata_converter(5) cur.execute("SELECT 't'::BOOL, NULL::BOOL, 'f'::BOOL") self.assertListOfListsEqual(cur.fetchall(), [[True, None, False]]) def test_custom_sqldata_converter_errors(self): with self._connect() as conn: cur = conn.cursor() with self.assertRaises(TypeError): # first arg is not an integer cur.register_sqldata_converter("string", lambda val, ctx: val) with self.assertRaises(TypeError): # second arg is not a function cur.register_sqldata_converter(6, "string") exec(CursorTestCase.createPrepStmtClass()) class SimpleQueryTestCase(VerticaPythonIntegrationTestCase): @classmethod def setUpClass(cls): super(SimpleQueryTestCase, cls).setUpClass() cls._conn_info['use_prepared_statements'] = False # Create data files for COPY LOCAL tests with tempfile.NamedTemporaryFile(delete=False) as cls._f1: cls._f1.write(b"1,foo\n2,bar\nx\xc3\xb1,bla") with tempfile.NamedTemporaryFile(delete=False) as cls._f2: cls._f2.write(b"4,\n5," + b'a'*10 + b"\n,baz") with tempfile.NamedTemporaryFile(delete=False) as cls._f3: cls._f3.write(b"10," + b'k'*12 + b"\n11,qux\nxx,corge") with tempfile.NamedTemporaryFile(delete=False) as cls._f4: cls._f4.write(b"13,flob\nf,quux\n15,xyz") @classmethod def tearDownClass(cls): for f in (cls._f1, cls._f2, cls._f3, cls._f4): os.remove(f.name) def setUp(self): super(SimpleQueryTestCase, self).setUp() self._table = 'simplequery_test' with self._connect() as conn: cur = conn.cursor() cur.execute("DROP TABLE IF EXISTS {0}".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(SimpleQueryTestCase, self).tearDown() def test_inline_commit(self): with self._connect() as conn: cur = conn.cursor() cur.execute("CREATE TABLE {0} (a INT, b VARCHAR)".format(self._table)) 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']]) # 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, []) cur.execute("--select 1") res = cur.fetchall() self.assertListOfListsEqual(res, []) cur.execute(""" /* Test block comment */ """) res = cur.fetchall() self.assertListOfListsEqual(res, []) # unit test for #175 def test_datetime_types(self): with self._connect() as conn: cur = conn.cursor() # 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} VALUES (:n, :s, :dt, :d, :t)".format(self._table), {'n': 10, 's': 'aa', 'dt': datetime(2018, 9, 7, 15, 38, 19, 769000), 'd': date(2018, 9, 7), 't': 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(2018, 9, 7, 15, 38, 19, 769000), date(2018, 9, 7), time(13, 50, 9)]]) def test_binary_types(self): with self._connect() as conn: cur = conn.cursor() # create test table cur.execute("""CREATE TABLE {0} ( a binary(1), b binary(3), c varbinary ) """.format(self._table)) cur.execute("INSERT INTO {0} VALUES (:b, :s1, :s2)".format(self._table), {'b': b'x', 's1': b'xyz', 's2': 'abcde'}) conn.commit() cur.execute("SELECT a, b, c FROM {0}".format(self._table)) res = cur.fetchall() self.assertListOfListsEqual(res, [[b'x', b'xyz', b'abcde']]) def test_uuid_type(self): with self._connect() as conn: cur = conn.cursor() x = UUID('{00010203-0405-0607-0809-0a0b0c0d0e0f}') y = UUID('00000020-0000-0000-0000-100000000000') z = UUID('00100010000000000000000000000000') # create test table cur.execute("CREATE TABLE {0} ( a uuid, b uuid, c uuid )".format(self._table)) cur.execute("INSERT INTO {0} VALUES (:u1, :u2, :u3)".format(self._table), {'u1': x, 'u2': y, 'u3': z}) conn.commit() cur.execute("SELECT a, b, c FROM {0}".format(self._table)) res = cur.fetchall()[0] self.assertListEqual([str(i) for i in res], [str(x), str(y), str(z)]) # unit test for #74 def test_nextset(self): with self._connect() as conn: cur = conn.cursor() cur.execute("SELECT 1; SELECT 'foo';") res1 = cur.fetchall() self.assertEqual(cur.rowcount, 1) self.assertListOfListsEqual(res1, [[1]]) self.assertIsNone(cur.fetchone()) self.assertTrue(cur.nextset()) res2 = cur.fetchall() self.assertEqual(cur.rowcount, 1) self.assertListOfListsEqual(res2, [['foo']]) 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() cur.execute("CREATE TABLE {0} (a INT, b VARCHAR(32))".format(self._table)) # 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() # test for #526 def test_nextset_with_error_2(self): with self._connect() as conn: cur = conn.cursor() cur.execute("CREATE TABLE {0} (a INT, b INT)".format(self._table)) # insert data cur.execute("INSERT INTO {0} (a, b) VALUES (8, 2)".format(self._table)) cur.execute("INSERT INTO {0} (a, b) VALUES (2, 0)".format(self._table)) conn.commit() cur.execute("SELECT 1; SELECT a/b FROM {}; SELECT 2".format(self._table)) # verify data from first query res1 = cur.fetchall() self.assertListOfListsEqual(res1, [[1]]) self.assertIsNone(cur.fetchone()) self.assertTrue(cur.nextset()) self.assertEqual(cur.fetchone()[0], Decimal('4')) # Division by zero error at the second row, should be skipped by next nextset() self.assertFalse(cur.nextset()) self.assertIsNone(cur.fetchone()) def test_qmark_paramstyle(self): with self._connect() as conn: cur = conn.cursor() cur.execute("CREATE TABLE {0} (a INT, b VARCHAR)".format(self._table)) err_msg = 'Invalid SQL' values = [1, 'aa'] with pytest.raises(ValueError, match=err_msg): cur.execute("INSERT INTO {} VALUES (?, ?)".format(self._table), values) cur.execute("INSERT INTO {} VALUES (?, ?)".format(self._table), values, use_prepared_statements=True) cur.execute("SELECT * FROM {}".format(self._table)) res = cur.fetchall() self.assertListOfListsEqual(res, [values]) def test_execute_parameters(self): with self._connect() as conn: cur = conn.cursor() all_chars = u"".join(chr(i) for i in range(1, 128)) backslash_data = u"\\backslash\\ \\data\\\\" cur.execute("SELECT :a, :b", parameters={"a": all_chars, "b": backslash_data}) self.assertEqual([all_chars, backslash_data], cur.fetchone()) def test_execute_percent_parameters(self): with self._connect() as conn: cur = conn.cursor() all_chars = u"".join(chr(i) for i in range(1, 128)) backslash_data = u"\\backslash\\ \\data\\\\" cur.execute("SELECT %s, %s", parameters=[all_chars, backslash_data]) self.assertEqual([all_chars, backslash_data], cur.fetchone()) def test_disabled_copy_local(self): self._conn_info['disable_copy_local'] = True with self._connect() as conn: cur = conn.cursor() cur.execute("CREATE TABLE {0} (a INT, b VARCHAR(9))".format(self._table)) with pytest.raises(errors.InterfaceError, match='disabled'): cur.execute( "COPY {} FROM LOCAL '{}','{}' DELIMITER ',' ENFORCELENGTH;" "SELECT 100;" .format(self._table, self._f1.name, self._f2.name) ) self.assertListOfListsEqual(cur.fetchall(), []) self.assertFalse(cur.nextset()) # Must not close the cursor object and able to successfully run queries cur.execute("SELECT 1;") self.assertListOfListsEqual(cur.fetchall(), [[1]]) def test_copy_local_stdin_input_options(self): with self._connect() as conn: cur = conn.cursor() cur.execute("CREATE TABLE {0} (a INT, b VARCHAR(9))".format(self._table)) # No STDIN input with pytest.raises(ValueError, match='No STDIN source'): cur.execute( "COPY {} FROM LOCAL STDIN DELIMITER ',' ENFORCELENGTH" .format(self._table)) with pytest.raises(ValueError, match='No STDIN source'): cur.execute( "COPY {} FROM LOCAL STDIN DELIMITER ',' ENFORCELENGTH" .format(self._table), copy_stdin=[]) # Invalid STDIN input with pytest.raises(TypeError, match='file-like object'): cur.execute( "COPY {} FROM LOCAL STDIN DELIMITER ',' ENFORCELENGTH" .format(self._table), copy_stdin='Not file-like') with pytest.raises(TypeError, match='file-like object'): cur.execute( "COPY {} FROM LOCAL STDIN DELIMITER ',' ENFORCELENGTH" .format(self._table), copy_stdin=['Not file-like']) # A file-like object as STDIN input fs = open(self._f1.name) cur.execute( "COPY {} FROM LOCAL STDIN DELIMITER ',' ENFORCELENGTH" .format(self._table), copy_stdin=fs) res1 = cur.fetchall() self.assertListOfListsEqual(res1, [[2]]) fs.close() def test_copy_local_stdin_multistat(self): # Define paths to rejected files tmpdir = os.path.dirname(self._f1.name) rejdir = os.path.join(tmpdir, 'copylocal1') rej1 = os.path.join(rejdir, 'copy_rej.txt') except1 = os.path.join(rejdir, 'copy_exception.txt') if os.path.isdir(rejdir): shutil.rmtree(rejdir) rej2 = os.path.join(tmpdir, 'copy_rej2.txt') except2 = os.path.join(tmpdir, 'copy_exception2.txt') for f in (rej2, except2): if os.path.isfile(f): os.remove(f) # Execute the COPY LOCAL statement as the first and later statement # within a query containing multiple statements with self._connect() as conn: cur = conn.cursor() cur.execute("CREATE TABLE {0} (a INT, b VARCHAR(9))".format(self._table)) # Feed STDIN f1 = open(self._f1.name) f2 = open(self._f3.name) cur.execute( "COPY {} FROM LOCAL STDIN DELIMITER ',' ENFORCELENGTH" " REJECTED DATA '{}' EXCEPTIONS '{}';" "SELECT 100;" "COPY {} FROM LOCAL STDIN DELIMITER ',' ENFORCELENGTH" " REJECTED DATA '{}' EXCEPTIONS '{}';" "SELECT 200;" .format(self._table, rej1, except1, self._table, rej2, except2), copy_stdin=[f1, f2] ) res1 = cur.fetchall() self.assertListOfListsEqual(res1, [[2]]) self.assertTrue(cur.nextset()) f1.close() res2 = cur.fetchall() self.assertListOfListsEqual(res2, [[100]]) self.assertTrue(cur.nextset()) res3 = cur.fetchall() self.assertListOfListsEqual(res3, [[1]]) self.assertTrue(cur.nextset()) f2.close() res4 = cur.fetchall() self.assertListOfListsEqual(res4, [[200]]) self.assertFalse(cur.nextset()) # There should be no hang/error in the next execution call cur.execute("SELECT * FROM {0} ORDER BY a ASC".format(self._table)) res = cur.fetchall() self.assertListOfListsEqual(res, [[1, 'foo'], [2, 'bar'], [11, 'qux']]) # Check rejected files with open(rej1, 'r', encoding='utf-8') as f: self.assertEqual(f.read(), u'x\u00f1,bla\n') with open(except1, 'r', encoding='utf-8') as f: content = f.read() self.assertTrue(u"Invalid integer format 'x\u00f1' for column 1 (a)" in content) with open(rej2, 'r', encoding='utf-8') as f: self.assertEqual(f.read(), u'10,kkkkkkkkkkkk\nxx,corge\n') with open(except2, 'r', encoding='utf-8') as f: content = f.read() self.assertTrue(u"The 12-byte value is too long for type Varchar(9), column 2 (b)" in content) self.assertTrue(u"Invalid integer format 'xx' for column 1 (a)" in content) # Delete data files try: for f in (rej2, except2): os.remove(f) shutil.rmtree(rejdir) except Exception: pass def test_copy_local_file_not_exist(self): with self._connect() as conn: cur = conn.cursor() cur.execute("CREATE TABLE {0} (a INT, b VARCHAR(9))".format(self._table)) with pytest.raises(OSError, match='not_exist.file does not exist'): cur.execute( "COPY {} FROM LOCAL '{}','not_exist.file' DELIMITER ',' ENFORCELENGTH" .format(self._table, self._f1.name)) self.assertListOfListsEqual(cur.fetchall(), []) self.assertFalse(cur.nextset()) # Must not close the cursor object and able to successfully run queries cur.execute("SELECT 1;") self.assertListOfListsEqual(cur.fetchall(), [[1]]) def test_copy_local_glob(self): suffix = ".copy_glob_test" files = (self._f1.name, self._f2.name, self._f3.name, self._f4.name) fdir = os.path.dirname(self._f1.name) for f in files: shutil.copy(f, f + suffix) with self._connect() as conn: cur = conn.cursor() cur.execute("CREATE TABLE {0} (a INT, b VARCHAR(9))".format(self._table)) cur.execute( "COPY {} FROM LOCAL '{}' DELIMITER ',' ENFORCELENGTH" .format(self._table, os.path.join(fdir, '*' + suffix))) self.assertListOfListsEqual(cur.fetchall(), [[7]]) for f in files: os.remove(f + suffix) @parameterized.expand([(True,), (False,)]) def test_copy_local_file_multistat(self, fetch_results): # Define paths to rejected files tmpdir = os.path.dirname(self._f1.name) rejdir = os.path.join(tmpdir, 'copylocal1') rej1 = os.path.join(rejdir, 'copy_rej.txt') except1 = os.path.join(rejdir, 'copy_exception.txt') if os.path.isdir(rejdir): shutil.rmtree(rejdir) rej2 = os.path.join(tmpdir, 'copy_rej2.txt') except2 = os.path.join(tmpdir, 'copy_exception2.txt') for f in (rej2, except2): if os.path.isfile(f): os.remove(f) # Execute the COPY LOCAL statement as the first and later statement # within a query containing multiple statements with self._connect() as conn: cur = conn.cursor() cur.execute("CREATE TABLE {0} (a INT, b VARCHAR(9))".format(self._table)) cur.execute( "COPY {} FROM LOCAL '{}','{}' DELIMITER ',' ENFORCELENGTH" " REJECTED DATA '{}' EXCEPTIONS '{}';" "SELECT 100;" "COPY {} FROM LOCAL '{}','{}' DELIMITER ',' ENFORCELENGTH" " REJECTED DATA '{}' EXCEPTIONS '{}';" "SELECT 200;" .format(self._table, self._f1.name, self._f2.name, rej1, except1, self._table, self._f3.name, self._f4.name, rej2, except2) ) if fetch_results: res1 = cur.fetchall() self.assertListOfListsEqual(res1, [[4]]) self.assertTrue(cur.nextset()) res2 = cur.fetchall() self.assertListOfListsEqual(res2, [[100]]) self.assertTrue(cur.nextset()) res3 = cur.fetchall() self.assertListOfListsEqual(res3, [[3]]) self.assertTrue(cur.nextset()) res4 = cur.fetchall() self.assertListOfListsEqual(res4, [[200]]) self.assertFalse(cur.nextset()) # There should be no hang/error in the next execution call cur.execute("SELECT * FROM {0} ORDER BY a ASC".format(self._table)) res = cur.fetchall() self.assertListOfListsEqual(res, [[None, 'baz'], [1, 'foo'], [2, 'bar'], [4, None], [11, 'qux'], [13, 'flob'], [15, 'xyz']]) # Check rejected files with open(rej1, 'r', encoding='utf-8') as f: self.assertEqual(f.read(), u'x\u00f1,bla\n5,aaaaaaaaaa\n') with open(except1, 'r', encoding='utf-8') as f: content = f.read() self.assertTrue(u"Invalid integer format 'x\u00f1' for column 1 (a)" in content) self.assertTrue(u"The 10-byte value is too long for type Varchar(9), column 2 (b)" in content) with open(rej2, 'r', encoding='utf-8') as f: self.assertEqual(f.read(), u'10,kkkkkkkkkkkk\nxx,corge\nf,quux\n') with open(except2, 'r', encoding='utf-8') as f: content = f.read() self.assertTrue(u"The 12-byte value is too long for type Varchar(9), column 2 (b)" in content) self.assertTrue(u"Invalid integer format 'xx' for column 1 (a)" in content) self.assertTrue(u"Invalid integer format 'f' for column 1 (a)" in content) # Delete files try: for f in (rej2, except2): os.remove(f) shutil.rmtree(rejdir) except Exception: pass def test_copy_local_returnrejected(self): with self._connect() as conn: cur = conn.cursor() cur.execute("CREATE TABLE {0} (a INT, b VARCHAR(9))".format(self._table)) msg = "COPY LOCAL does not support rejected row numbers with exceptions or rejected data options" with pytest.raises(errors.QueryError, match=msg): cur.execute( "COPY {} FROM LOCAL '{}','{}' DELIMITER ',' ENFORCELENGTH" " RETURNREJECTED" " REJECTED DATA 'copy_rej.txt' EXCEPTIONS 'copy_exp.txt'" .format(self._table, self._f1.name, self._f2.name)) # Rejected row numbers write to log file cur.execute( "COPY {} FROM LOCAL '{}','{}' DELIMITER ',' ENFORCELENGTH" " RETURNREJECTED" .format(self._table, self._f1.name, self._f2.name)) self.assertListOfListsEqual(cur.fetchall(), [[4]]) cur.execute("SELECT * FROM {0} ORDER BY a ASC".format(self._table)) self.assertListOfListsEqual(cur.fetchall(), [[None, 'baz'], [1, 'foo'], [2, 'bar'], [4, None]]) def test_copy_local_rejected_as_table(self): with self._connect() as conn: cur = conn.cursor() cur.execute("CREATE TABLE {0} (a INT, b VARCHAR(9))".format(self._table)) cur.execute("DROP TABLE IF EXISTS test_loader_rejects CASCADE") cur.execute( "COPY {} FROM LOCAL '{}','{}' DELIMITER ',' ENFORCELENGTH" " REJECTED DATA AS TABLE test_loader_rejects" .format(self._table, self._f1.name, self._f2.name)) self.assertListOfListsEqual(cur.fetchall(), [[4]]) cur.execute("SELECT rejected_data, rejected_reason FROM test_loader_rejects ORDER BY row_number ASC") self.assertListOfListsEqual(cur.fetchall(), [['5,aaaaaaaaaa', 'The 10-byte value is too long for type Varchar(9), column 2 (b)'], [u'x\u00f1,bla', u"Invalid integer format 'x\u00f1' for column 1 (a)"]]) cur.execute("SELECT * FROM {0} ORDER BY a ASC".format(self._table)) self.assertListOfListsEqual(cur.fetchall(), [[None, 'baz'], [1, 'foo'], [2, 'bar'], [4, None]]) cur.execute("DROP TABLE IF EXISTS test_loader_rejects CASCADE") def test_copy_local_abort_on_error(self): with self._connect() as conn: cur = conn.cursor() cur.execute("CREATE TABLE {0} (a INT, b VARCHAR(9))".format(self._table)) msg = r"The 12-byte value is too long for type Varchar\(9\), column 2 \(b\)" # COPY LOCAL FILE with pytest.raises(errors.CopyRejected, match=msg): cur.execute( "COPY {} FROM LOCAL '{}' DELIMITER ',' ENFORCELENGTH ABORT ON ERROR" .format(self._table, self._f3.name)) # Must not close the cursor object and able to successfully run queries cur.execute("SELECT 1;") self.assertListOfListsEqual(cur.fetchall(), [[1]]) # COPY LOCAL STDIN with pytest.raises(errors.CopyRejected, match=msg): cur.execute( "COPY {} FROM LOCAL STDIN DELIMITER ',' ENFORCELENGTH ABORT ON ERROR" .format(self._table), copy_stdin=[open(self._f3.name)]) # Must not close the cursor object and able to successfully run queries cur.execute("SELECT 1;") self.assertListOfListsEqual(cur.fetchall(), [[1]]) class SimpleQueryExecutemanyTestCase(VerticaPythonIntegrationTestCase): def setUp(self): super(SimpleQueryExecutemanyTestCase, 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(SimpleQueryExecutemanyTestCase, 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, [(i, chr(i)) for i in range(0, 128)]) 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')]) # test for #292 def test_executemany_autocommit(self): with self._connect() as conn: cur = conn.cursor() conn.autocommit = False cur.execute('BEGIN') cur.executemany("INSERT INTO {0} (a, b) VALUES (%s, %s)".format(self._table), ((None, 'foo'), [2, None], [3, 'bar'])) cur.execute('ROLLBACK') cur.execute("SELECT count(*) FROM {0}".format(self._table)) res = cur.fetchone()[0] self.assertEqual(res, 0) 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()) def test_executemany_empty(self): err_msg = "executemany is implemented for simple INSERT statements only" with self._connect() as conn: cur = conn.cursor() with pytest.raises(NotImplementedError, match=err_msg): cur.executemany("", []) class PreparedStatementTestCase(VerticaPythonIntegrationTestCase): @classmethod def setUpClass(cls): super(PreparedStatementTestCase, cls).setUpClass() cls._conn_info['use_prepared_statements'] = True def setUp(self): super(PreparedStatementTestCase, self).setUp() self._table = 'preparedstmt_test' with self._connect() as conn: cur = conn.cursor() cur.execute("DROP TABLE IF EXISTS {0}".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(PreparedStatementTestCase, self).tearDown() def test_empty_statement(self): with self._connect() as conn: cur = conn.cursor() err_msg = 'The statement being prepared is empty' with pytest.raises(errors.EmptyQueryError, match=err_msg): cur.execute("") with pytest.raises(errors.EmptyQueryError, match=err_msg): cur.executemany("", [()]) with pytest.raises(errors.EmptyQueryError, match=err_msg): cur.execute("--select 1") with pytest.raises(errors.EmptyQueryError, match=err_msg): cur.execute(""" /* Test block comment */ """) def test_compound_statement(self): with self._connect() as conn: cur = conn.cursor() query = "select 1; select 2; select 3;" err_msg = 'Cannot insert multiple commands into a prepared statement' with pytest.raises(errors.VerticaSyntaxError, match=err_msg): cur.execute(query) def test_num_of_parameters(self): with self._connect() as conn: cur = conn.cursor() cur.execute("CREATE TABLE {} (a int, b varchar)".format(self._table)) err_msg = 'Invalid number of parameters' # The number of parameters is less than columns of table with pytest.raises(ValueError, match=err_msg): cur.execute("INSERT INTO {} VALUES (?,?)".format(self._table)) # The number of parameters is greater than columns of table with pytest.raises(ValueError, match=err_msg): cur.execute("INSERT INTO {} VALUES (?,?)".format(self._table), [1, 'foo', 2]) # The number of parameters is equal to columns of table values = [1, 'varchar'] cur.execute("INSERT INTO {} VALUES (?,?)".format(self._table), values) conn.commit() cur.execute("SELECT * FROM {}".format(self._table)) res = cur.fetchall() self.assertListOfListsEqual(res, [values]) def test_format_paramstyle(self): with self._connect() as conn: cur = conn.cursor() cur.execute("CREATE TABLE {} (a int, b varchar)".format(self._table)) err_msg = 'Invalid SQL' values = [1, 'varchar'] with pytest.raises(ValueError, match=err_msg): cur.execute("INSERT INTO {} VALUES (%s, %s)".format(self._table), values) cur.execute("INSERT INTO {} VALUES (%s, %s)".format(self._table), values, use_prepared_statements=False) conn.commit() cur.execute("SELECT * FROM {}".format(self._table)) res = cur.fetchall() self.assertListOfListsEqual(res, [values]) def test_named_paramstyle(self): with self._connect() as conn: cur = conn.cursor() cur.execute("CREATE TABLE {} (a int, b varchar)".format(self._table)) err_msg = 'Execute parameters should be a list/tuple' values = {'a': 1, 'b': 'varchar'} with pytest.raises(TypeError, match=err_msg): cur.execute("INSERT INTO {} VALUES (:a, :b)".format(self._table), values) cur.execute("INSERT INTO {} VALUES (:a, :b)".format(self._table), values, use_prepared_statements=False) conn.commit() cur.execute("SELECT * FROM {}".format(self._table)) res = cur.fetchall() self.assertListOfListsEqual(res, [[1, 'varchar']]) def test_executemany(self): values = ((None, 'foo'), [1, 'aa'], (2, None), [2, u'a\xfc']) expected = [[None, 'foo'], [1, 'aa'], [2, None], [2, u'a\xfc']] with self._connect() as conn: cur = conn.cursor() cur.execute("CREATE TABLE {} (a int, b varchar)".format(self._table)) cur.executemany("INSERT INTO {} VALUES (?, ?)".format(self._table), values) self.assertListOfListsEqual(cur.fetchall(), [[1]]) self.assertIsNone(cur.fetchone()) self.assertTrue(cur.nextset()) self.assertListOfListsEqual(cur.fetchall(), [[1]]) self.assertIsNone(cur.fetchone()) self.assertTrue(cur.nextset()) self.assertListOfListsEqual(cur.fetchall(), [[1]]) self.assertIsNone(cur.fetchone()) self.assertTrue(cur.nextset()) self.assertListOfListsEqual(cur.fetchall(), [[1]]) self.assertIsNone(cur.fetchone()) self.assertFalse(cur.nextset()) cur.executemany("SELECT * FROM {} WHERE a=? ORDER BY b" .format(self._table), [[1], [2], [3]]) self.assertListOfListsEqual(cur.fetchall(), [[1, 'aa']]) self.assertIsNone(cur.fetchone()) self.assertTrue(cur.nextset()) self.assertListOfListsEqual(cur.fetchall(), [[2, u'a\xfc'], [2, None]]) self.assertIsNone(cur.fetchone()) self.assertTrue(cur.nextset()) self.assertListOfListsEqual(cur.fetchall(), []) self.assertIsNone(cur.fetchone()) self.assertFalse(cur.nextset()) def test_bind_boolean(self): values = (True, 't', 'true', '1', 1, 'Yes', 'y', None, False, 'f', 'false', '0', 0, 'No', 'n') expected = [[True] * 7 + [None] + [False] * 7] with self._connect() as conn: cur = conn.cursor() cur.execute("""CREATE TABLE {} ( col_1 BOOL, col_2 BOOL, col_3 BOOL, col_4 BOOL, col_5 BOOL, col_6 BOOL, col_7 BOOL, col_8 BOOL, col_9 BOOL, col_10 BOOL, col_11 BOOL, col_12 BOOL, col_13 BOOL, col_14 BOOL, col_15 BOOL )""".format(self._table)) cur.execute("INSERT INTO {} VALUES ({})".format( self._table, ','.join(['?']*15)), values) conn.commit() cur.execute("SELECT * FROM {}".format(self._table)) res = cur.fetchall() self.assertListOfListsEqual(res, expected) def test_bind_datetime(self): with self._connect() as conn: cur = conn.cursor() cur.execute("CREATE TABLE {} (a TIMESTAMP, b DATE, c TIME, d TIMETZ," "e TIMESTAMP, f DATE, g TIME, h TIMETZ)".format(self._table)) values = [datetime(2018, 9, 7, 15, 38, 19, 769000), date(2018, 9, 7), time(13, 50, 9), time(22, 36, 33, 123400, tzinfo=tzoffset(None, -19800)), None, None, None, None] cur.execute("INSERT INTO {} VALUES ({})".format(self._table, ','.join(['?']*8)), values) conn.commit() cur.execute("SELECT * FROM {}".format(self._table)) res = cur.fetchall() self.assertListOfListsEqual(res, [values]) def test_bind_binary(self): values = [b'binary data', b'\\backslash data\\', u'\\backslash data\\', u'\u00f1 encoding', 'raw data', 'long varbinary data', None] expected = [[b'binary data\x00\x00\x00', b'\\backslash data\\', b'\\backslash data\\', b'\xc3\xb1 encoding', b'raw data', b'long varbinary data', None]] with self._connect() as conn: cur = conn.cursor() cur.execute("""CREATE TABLE {} ( col_1 BINARY(14), col_2 VARBINARY, col_3 VARBINARY, col_4 BYTEA, col_5 RAW, col_6 LONG VARBINARY, col_7 VARBINARY)""".format(self._table)) cur.execute("INSERT INTO {} VALUES ({})".format( self._table, ','.join(['?']*7)), values) conn.commit() cur.execute("SELECT * FROM {}".format(self._table)) res = cur.fetchall() self.assertListOfListsEqual(res, expected) def test_bind_numeric(self): values = [Decimal('123456789.98765'), Decimal('123456789.98765'), Decimal('123456789.98765'), Decimal('123456789.98765'), 10, 11, 1234567890, 1296554905964, 123, 123, 123.45, 123.45, 123.45, 123.45, 123.45, None, None, None] expected = [[Decimal('123456789.9877'), Decimal('123456789.987650000000000'), Decimal('123456790'), Decimal('123456789.987650000000000'), 10, 11, 1234567890, 1296554905964, 123, 123, 123.45, 123.45, 123.45, 123.45, 123.45, None, None, None]] with self._connect() as conn: cur = conn.cursor() cur.execute("""CREATE TABLE {} ( col_1 MONEY, col_2 NUMERIC, col_3 NUMBER, col_4 DECIMAL, col_5 TINYINT, col_6 SMALLINT, col_7 INT8, col_8 BIGINT, col_9 INT, col_10 INTEGER, col_11 DOUBLE PRECISION, col_12 REAL, col_13 FLOAT8, col_14 FLOAT(10), col_15 FLOAT, col_16 NUMERIC, col_17 INT, col_18 FLOAT )""".format(self._table)) cur.execute("INSERT INTO {} VALUES ({})".format( self._table, ','.join(['?']*18)), values) conn.commit() cur.execute("SELECT * FROM {}".format(self._table)) res = cur.fetchall() self.assertListOfListsEqual(res, expected) def test_bind_character(self): values = ['char data', b'raw varchar data', u'long varbinary data \u00f1', None, None, None] expected = [['char data ', 'raw varchar data', u'long varbinary data \u00f1', None, None, None]] with self._connect() as conn: cur = conn.cursor() cur.execute("""CREATE TABLE {} ( col_1 CHAR(10), col_2 VARCHAR, col_3 LONG VARCHAR, col_4 CHAR(10), col_5 VARCHAR, col_6 LONG VARCHAR )""".format(self._table)) cur.execute("INSERT INTO {} VALUES ({})".format( self._table, ','.join(['?']*6)), values) conn.commit() cur.execute("SELECT * FROM {}".format(self._table)) res = cur.fetchall() self.assertListOfListsEqual(res, expected) def test_bind_interval(self): values = ['1y 10m', '1 2', '1y 10m', '1y 10m', '17910y 1h 3m 6s 5msecs 57us ago', '3 days 2 hours', '1 3', '1y 15 mins 20 sec', '15 mins 20 sec', '1y 5 mins 20 sec', '2 days 12 hours 15 mins 1235 milliseconds', '2 days 12 hours 15 mins ago', '2 days 12 hours 15 mins ago', None, None] expected = [[relativedelta(years=+1), relativedelta(years=+1, months=+2), relativedelta(years=+1, months=+10), relativedelta(days=+365), relativedelta(days=-6537150, hours=-1, minutes=-3, seconds=-6, microseconds=-5100), relativedelta(days=+3, hours=+2), relativedelta(hours=+1, minutes=+3), relativedelta(days=+365, minutes=+15, seconds=+20), relativedelta(minutes=+15), relativedelta(days=+365, minutes=+5, seconds=+20), relativedelta(days=+2, hours=+12, minutes=+15, seconds=+1, microseconds=+240000), relativedelta(days=-2, hours=-12), relativedelta(days=-2, hours=-12, minutes=-15), None, None]] with self._connect() as conn: cur = conn.cursor() cur.execute("""CREATE TABLE {} ( col_1 INTERVAL YEAR, col_2 INTERVAL YEAR TO MONTH, col_3 INTERVAL MONTH, col_4 INTERVAL DAY, col_5 INTERVAL DAY TO SECOND(4), col_6 INTERVAL HOUR, col_7 INTERVAL HOUR to MINUTE, col_8 INTERVAL HOUR to SECOND, col_9 INTERVAL MINUTE, col_10 INTERVAL MINUTE TO SECOND, col_11 INTERVAL SECOND(2), col_12 INTERVAL DAY TO HOUR, col_13 INTERVAL DAY TO MINUTE, col_14 INTERVAL YEAR TO MONTH, col_15 INTERVAL DAY TO SECOND )""".format(self._table)) cur.execute("INSERT INTO {} VALUES ({})".format( self._table, ','.join(['?']*15)), values) conn.commit() cur.execute("SELECT * FROM {}".format(self._table)) res = cur.fetchall() self.assertListOfListsEqual(res, expected) def test_bind_udtype(self): poly = "POLYGON ((1 2, 2 3, 3 1, 1 2))" line = "LINESTRING (42.1 71, 41.4 70, 41.3 72.9, 42.99 71.46, 44.47 73.21)" with self._connect() as conn: cur = conn.cursor() cur.execute("CREATE TABLE {} (c1 GEOMETRY(10000), c2 GEOGRAPHY(1000))" .format(self._table)) cur.execute("INSERT INTO {} VALUES (ST_GeomFromText(?), ST_GeographyFromText(?))" .format(self._table), [poly, line]) conn.commit() cur.execute("SELECT c1, c2, ST_AsText(c1), ST_AsText(c2) FROM {}".format(self._table)) res = cur.fetchall() self.assertEqual(res[0][2], poly) self.assertEqual(res[0][3], line) datatype_names = [col.type_name for col in cur.description] expected = ['geometry', 'geography', 'Long Varchar', 'Long Varchar'] self.assertListEqual(datatype_names, expected) self.assertEqual(cur.description[0].display_size, 10000) self.assertEqual(cur.description[1].display_size, 1000) vertica-python-1.4.0/vertica_python/tests/integration_tests/test_datatypes.py000066400000000000000000001237471464740151100300650ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from datetime import date, datetime, time from dateutil.relativedelta import relativedelta from dateutil.tz import tzoffset from decimal import Decimal from math import isnan from uuid import UUID 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) def test_uuid_query(self): self.require_protocol_at_least(3 << 16 | 8) value = UUID('00010203-0405-0607-0809-0a0b0c0d0e0f') query = "SELECT '{0}'::uuid".format(value) res = self._query_and_fetchone(query) self.assertEqual(res[0], value) exec(TypeTestCase.createPrepStmtClass()) class InsertComplexTypeTestCase(VerticaPythonIntegrationTestCase): """ Python objects (list, set, dict) convert to SQL literal (ARRAY, SET, ROW) Only for client-side bindings (use_prepared_statements=False) """ def setUp(self): super(InsertComplexTypeTestCase, self).setUp() self.require_protocol_at_least(3 << 16 | 12) self._table = 'insert_complex_types_test' def tearDown(self): with self._connect() as conn: cur = conn.cursor() cur.execute(f"DROP TABLE IF EXISTS {self._table}") super(InsertComplexTypeTestCase, self).tearDown() def _test_insert_complex_type(self, col_type, values, expected=None, test_executemany=False): if expected is None: expected = values with self._connect() as conn: cur = conn.cursor() cur.execute(f"DROP TABLE IF EXISTS {self._table}") cur.execute(f"CREATE TABLE {self._table} (a INT, b {col_type})") seq_of_values = [(i, values[i]) for i in range(len(values))] for value in seq_of_values: # Some cases need explicit typecasting cur.execute(f"INSERT INTO {self._table} (a, b) VALUES (%s, %s::{col_type})", value, use_prepared_statements=False) rows = cur.execute(f"SELECT b FROM {self._table} ORDER BY a").fetchall() results = [row[0] for row in rows] self.assertEqual(results, expected) if not test_executemany: return # test cursor.executemany cur.execute(f"TRUNCATE TABLE {self._table}") cur.executemany(f"INSERT INTO {self._table} (a, b) VALUES (%s, %s)", seq_of_values, use_prepared_statements=False) rows = cur.execute(f"SELECT b FROM {self._table} ORDER BY a").fetchall() results = [row[0] for row in rows] self.assertEqual(results, expected) ####################### # tests for ARRAY type ####################### def test_Array_boolean_type(self): self._test_insert_complex_type('ARRAY[BOOL]', [[True, False, None], None, [], [None]], test_executemany=True) def test_Array_integer_type(self): self._test_insert_complex_type('ARRAY[INT]', [[1,-2,3], [4,None,5], None, [], [None]], test_executemany=True) self._test_insert_complex_type('ARRAY[ARRAY[INT]]', [[[1,2], [3,4], None, [5,None], []], None, [], [None]]) self._test_insert_complex_type('ARRAY[ARRAY[ARRAY[ARRAY[INT]]]]', [[[[None,[1,2,3],None,[1,None,3],[None,None,None],[4,5],[],None]]], None, [], [None]]) def test_Array_float_type(self): self._test_insert_complex_type('ARRAY[FLOAT]', [[1.23456e-18,float('Inf'),float('-Inf'),None,-1.234,0.0], None, [], [None]], test_executemany=True) def test_Array_numeric_type(self): self._test_insert_complex_type('ARRAY[NUMERIC]', [[Decimal('-1.1200000000'), Decimal('0E-10'), None, Decimal('1234567890123456789.0123456789')], None, [], [None]], test_executemany=True) def test_Array_char_type(self): self._test_insert_complex_type('ARRAY[CHAR(3)]', [['a', u'\u16b1', None, 'foo'], None, [], [None]], [['a ', u'\u16b1', None, 'foo'], None, [], [None]], test_executemany=True) def test_Array_varchar_type(self): self._test_insert_complex_type('ARRAY[VARCHAR(10)]', [['', u'\u16b1\nb', None, 'foo'], None, [], [None]], test_executemany=True) self._test_insert_complex_type('ARRAY[VARCHAR]', [[chr(i)] for i in range(1, 128)], test_executemany=True) def test_Array_date_type(self): self._test_insert_complex_type('ARRAY[DATE]', [[date(2021, 6, 10),None,date(221, 5, 2)], None, [], [None]], test_executemany=True) def test_Array_time_type(self): self._test_insert_complex_type('ARRAY[TIME(3)]', [[time(0, 0, 0),None,time(22, 36, 33, 124000)], None, [], [None]], test_executemany=True) def test_Array_timetz_type(self): self._test_insert_complex_type('ARRAY[TIMETZ(3)]', [[time(22, 36, 33, 123000, tzinfo=tzoffset(None, 23400)),None, time(22, 36, 33, 123000, tzinfo=tzoffset(None, -10800))], None, [], [None]], test_executemany=True) def test_Array_timestamp_type(self): self._test_insert_complex_type('ARRAY[TIMESTAMP]', [[datetime(276, 12, 1, 11, 22, 33),None,datetime(2001, 12, 1, 0, 30, 45, 87000)], None, [], [None]], test_executemany=True) def test_Array_timestamptz_type(self): self._test_insert_complex_type('ARRAY[TIMESTAMPTZ]', [[datetime(276, 11, 30, 23, 32, 57, tzinfo=tzoffset(None, 3600)),None,datetime(2001, 12, 1, 0, 30, 45, 87000, tzinfo=tzoffset(None, -18000))], None, [], [None]], test_executemany=True) def test_Array_UUID_type(self): self._test_insert_complex_type('ARRAY[UUID]', [[UUID('00010203-0405-0607-0809-0a0b0c0d0e0f'),None,UUID('123e4567-e89b-12d3-a456-426655440a00')], None, [], [None]], test_executemany=True) ##################### # tests for SET type ##################### def test_1DSet_boolean_type(self): self._test_insert_complex_type('SET[BOOL]', [{True, False, None}, None, set(), {None}], test_executemany=True) def test_1DSet_integer_type(self): self._test_insert_complex_type('SET[INT]', [{0, 1, -2, 3, None}, None, set(), {None}], test_executemany=True) def test_1DSet_float_type(self): self._test_insert_complex_type('SET[FLOAT]', [{float('Inf'), float('-Inf'), None, -1.234, 0.0, 1.23456e-18}, None, set(), {None}], test_executemany=True) def test_1DSet_numeric_type(self): self._test_insert_complex_type('SET[NUMERIC]', [{Decimal('-1.12'), Decimal('0E-15'), None, Decimal('1234567890123456789.0123456789')}, None, set(), {None}], test_executemany=True) def test_1DSet_char_type(self): self._test_insert_complex_type('SET[CHAR(3)]', [{'a ', u'\u16b1', None, 'foo'}, None, set(), {None}], test_executemany=True) def test_1DSet_varchar_type(self): self._test_insert_complex_type('SET[VARCHAR(10)]', [{'', u'\u16b1\nb', None, 'foo'}, None, set(), {None}], test_executemany=True) self._test_insert_complex_type('SET[VARCHAR]', [{chr(i)} for i in range(1, 128)], test_executemany=True) def test_1DSet_date_type(self): self._test_insert_complex_type('SET[DATE]', [{date(2021, 6, 10), None, date(221, 5, 2)}, None, set(), {None}], test_executemany=True) def test_1DSet_time_type(self): self._test_insert_complex_type('SET[TIME(3)]', [{time(0, 0, 0), None, time(22, 36, 33, 124000)}, None, set(), {None}], test_executemany=True) def test_1DSet_timetz_type(self): self._test_insert_complex_type('SET[TIMETZ(3)]', [{time(22, 36, 33, 123000, tzinfo=tzoffset(None, 23400)),None, time(22, 36, 33, 123000, tzinfo=tzoffset(None, -10800))}, None, set(), {None}], test_executemany=True) def test_1DSet_timestamp_type(self): self._test_insert_complex_type('SET[TIMESTAMP]', [{datetime(276, 12, 1, 11, 22, 33),None,datetime(2001, 12, 1, 0, 30, 45, 87000)}, None, set(), {None}], test_executemany=True) def test_1DSet_timestamptz_type(self): self._test_insert_complex_type('SET[TIMESTAMPTZ]', [{datetime(276, 11, 30, 23, 32, 57, tzinfo=tzoffset(None, 3600)),None, datetime(2001, 12, 1, 0, 30, 45, 87000, tzinfo=tzoffset(None, -18000))}, None, set(), {None}], test_executemany=True) def test_1DSet_UUID_type(self): self._test_insert_complex_type('SET[UUID]', [{UUID('00010203-0405-0607-0809-0a0b0c0d0e0f'),None,UUID('123e4567-e89b-12d3-a456-426655440a00')}, None, set(), {None}], test_executemany=True) ##################### # tests for ROW type ##################### def test_row_boolean_type(self): self._test_insert_complex_type('ROW(BOOL, ARRAY[BOOL], ROW(BOOL, ARRAY[BOOL]))', [ {'f0': True, 'f1': [None, False], 'f2': {'f0': False, 'f1': [True, None]}}, {'f0': None, 'f1': [None], 'f2': {'f0': None, 'f1': []}}, ]) def test_row_integer_type(self): self._test_insert_complex_type('ROW(INT, ARRAY[INT], ROW(INT, ARRAY[INT]))', [ {'f0': -10, 'f1': [None, 1, 2], 'f2': {'f0': 90, 'f1': [0, None]}}, {'f0': None, 'f1': [None], 'f2': {'f0': None, 'f1': []}}, {'f0': 5, 'f1': [], 'f2': None}, ]) def test_row_float_type(self): self._test_insert_complex_type('ROW(FLOAT, ARRAY[FLOAT], ROW(FLOAT, ARRAY[FLOAT]))', [ {'f0': 0.0, 'f1': [None, 1.23456e-18], 'f2': {'f0': float('-Inf'), 'f1': [-1.2, None]}}, {'f0': None, 'f1': [None], 'f2': {'f0': None, 'f1': []}}, ]) def test_row_numeric_type(self): self._test_insert_complex_type('ROW(NUMERIC, ARRAY[NUMERIC], ROW(NUMERIC, ARRAY[NUMERIC]))', [ {'f0': Decimal('-1.12'), 'f1': [None, Decimal('0E-15')], 'f2': {'f0': Decimal('1234567890123456789.0123456789'), 'f1': [Decimal(10), None]}}, {'f0': None, 'f1': [None], 'f2': {'f0': None, 'f1': []}}, ]) def test_row_char_type(self): self._test_insert_complex_type('ROW(CHAR(3), ARRAY[CHAR(3)], ROW(CHAR(3), ARRAY[CHAR(3)]))', [ {'f0': 'a ', 'f1': [None, 'foo'], 'f2': {'f0': '\u16b1', 'f1': [' b ', None]}}, {'f0': None, 'f1': [None], 'f2': {'f0': None, 'f1': []}}, ]) def test_row_varchar_type(self): self._test_insert_complex_type('ROW(VARCHAR, ARRAY[VARCHAR], ROW(VARCHAR, ARRAY[VARCHAR]))', [ {'f0': 'a', 'f1': [None, 'foo'], 'f2': {'f0': '\u16b1\nb', 'f1': ['', None]}}, {'f0': None, 'f1': [None], 'f2': {'f0': None, 'f1': []}}, ]) def test_row_date_type(self): self._test_insert_complex_type('ROW(DATE, ARRAY[DATE], ROW(DATE, ARRAY[DATE]))', [ {'f0': date(2021, 6, 10), 'f1': [None, date(2021, 6, 11)], 'f2': {'f0': date(221, 5, 2), 'f1': [date(2023, 1, 1), None]}}, {'f0': None, 'f1': [None], 'f2': {'f0': None, 'f1': []}}, ]) def test_row_time_type(self): self._test_insert_complex_type('ROW(TIME(3), ARRAY[TIME(3)], ROW(TIME(3), ARRAY[TIME(3)]))', [ {'f0': time(0, 0, 0), 'f1': [None, time(8, 30, 10)], 'f2': {'f0': time(22, 36, 33, 124000), 'f1': [time(0, 0, 0, 500000), None]}}, {'f0': None, 'f1': [None], 'f2': {'f0': None, 'f1': []}}, ]) def test_row_timetz_type(self): self._test_insert_complex_type('ROW(TIMETZ(3), ARRAY[TIMETZ], ROW(TIMETZ, ARRAY[TIMETZ(3)]))', [ {'f0': time(22, 36, 33, 123000, tzinfo=tzoffset(None, 23400)), 'f1': [None, time(8, 30, 10, tzinfo=tzoffset(None, -23400))], 'f2': {'f0': time(22, 36, 33, 123000, tzinfo=tzoffset(None, -10800)), 'f1': [time(0, 0, tzinfo=tzoffset(None, 10800)), None]}}, {'f0': None, 'f1': [None], 'f2': {'f0': None, 'f1': []}}, ]) def test_row_timestamp_type(self): self._test_insert_complex_type('ROW(TIMESTAMP, ARRAY[TIMESTAMP], ROW(TIMESTAMP, ARRAY[TIMESTAMP]))', [ {'f0': datetime(276, 12, 1, 11, 22, 33), 'f1': [None, datetime(2001, 12, 1, 0, 30, 45, 87000)], 'f2': {'f0': datetime(2023, 12, 1, 11, 30, 45, 87000), 'f1': [datetime(1998, 12, 1, 11, 22, 33), None]}}, {'f0': None, 'f1': [None], 'f2': {'f0': None, 'f1': []}}, ]) def test_row_timestamptz_type(self): self._test_insert_complex_type('ROW(TIMESTAMPTZ, ARRAY[TIMESTAMPTZ], ROW(TIMESTAMPTZ, ARRAY[TIMESTAMPTZ]))', [ {'f0': datetime(276, 11, 30, 23, 32, 57, tzinfo=tzoffset(None, 23400)), 'f1': [None, datetime(2001, 12, 1, 0, 30, 45, 87000, tzinfo=tzoffset(None, -18000))], 'f2': {'f0': datetime(2011, 12, 1, 0, 30, 45, 57000, tzinfo=tzoffset(None, 18000)), 'f1': [datetime(2023, 12, 1, 11, 30, 45, tzinfo=tzoffset(None, -23400)), None]}}, {'f0': None, 'f1': [None], 'f2': {'f0': None, 'f1': []}}, ]) def test_row_UUID_type(self): self._test_insert_complex_type('ROW(UUID, ARRAY[UUID], ROW(UUID, ARRAY[UUID]))', [ {'f0': UUID('00010203-0405-0607-0809-0a0b0c0d0e0f'), 'f1': [None, UUID('123e4567-e89b-12d3-a456-426655440a00')], 'f2': {'f0': UUID('123e4567-e89b-12d3-a456-426655440a00'), 'f1': [UUID('00010203-0405-0607-0809-0a0b0c0d0e0f'), None]}}, {'f0': None, 'f1': [None], 'f2': {'f0': None, 'f1': []}}, ]) class ComplexTypeTestCase(VerticaPythonIntegrationTestCase): """ SQL data types (ARRAY, SET, ROW) convert to Python objects (list, set, dict) """ def setUp(self): super(ComplexTypeTestCase, self).setUp() self.require_protocol_at_least(3 << 16 | 12) ###################################################### # tests for connection option 'request_complex_types' ###################################################### def test_connection_option(self): self._conn_info['request_complex_types'] = False with self._connect() as conn: cur = conn.cursor() res = self._query_and_fetchone("SELECT ARRAY[-500, 0, null, 500]::ARRAY[INT]") self.assertEqual(res[0], '[-500,0,null,500]') ####################### # tests for ARRAY type ####################### def test_1DArray_boolean_type(self): query = "SELECT ARRAY['t', 'f', null]::ARRAY[BOOL], ARRAY[]::ARRAY[BOOL], null::ARRAY[BOOL]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [True, False, None]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) def test_1DArray_integer_type(self): query = "SELECT ARRAY[-500, 0, null, 500]::ARRAY[INT], ARRAY[]::ARRAY[INT], null::ARRAY[INT]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [-500, 0, None, 500]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) def test_1DArray_float_type(self): query = ("SELECT ARRAY['Infinity'::float, '-Infinity'::float, null, -1.234, 0, 1.23456e-18]::ARRAY[FLOAT]," " ARRAY[]::ARRAY[FLOAT], null::ARRAY[FLOAT], ARRAY['NaN'::float]::ARRAY[FLOAT]") res = self._query_and_fetchone(query) self.assertEqual(res[0], [float('Inf'), float('-Inf'), None, -1.234, 0.0, 1.23456e-18]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) self.assertTrue(isnan(res[3][0])) def test_1DArray_numeric_type(self): query = "SELECT ARRAY[-1.12, 0, null, 1234567890123456789.0123456789]::ARRAY[NUMERIC], ARRAY[]::ARRAY[DECIMAL], null::ARRAY[NUMERIC]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [Decimal('-1.1200000000'), Decimal('0E-10'), None, Decimal('1234567890123456789.0123456789')]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) def test_1DArray_char_type(self): query = u"SELECT ARRAY['a', '\u16b1b', null, 'foo']::ARRAY[CHAR(3)], ARRAY[]::ARRAY[CHAR(4)], null::ARRAY[CHAR(5)]" res = self._query_and_fetchone(query) self.assertEqual(res[0], ['a ', u'\u16b1', None, 'foo']) self.assertEqual(res[1], []) self.assertEqual(res[2], None) def test_1DArray_varchar_type(self): query = u"SELECT ARRAY['', '\u16b1\nb', null, 'foo']::ARRAY[VARCHAR(10),4], ARRAY[]::ARRAY[VARCHAR(4)], null::ARRAY[VARCHAR]" res = self._query_and_fetchone(query) self.assertEqual(res[0], ['', u'\u16b1\nb', None, 'foo']) self.assertEqual(res[1], []) self.assertEqual(res[2], None) def test_1DArray_date_type(self): query = "SELECT ARRAY['2021-06-10', null, '0221-05-02']::ARRAY[DATE], ARRAY[]::ARRAY[DATE], null::ARRAY[DATE]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [date(2021, 6, 10), None, date(221, 5, 2)]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) def test_1DArray_time_type(self): query = "SELECT ARRAY['00:00:00.00', null, '22:36:33.123956']::ARRAY[TIME(3)], ARRAY[]::ARRAY[TIME(4)], null::ARRAY[TIME]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [time(0, 0, 0), None, time(22, 36, 33, 124000)]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) def test_1DArray_timetz_type(self): query = "SELECT ARRAY['22:36:33.12345+0630', null, '800-02-03 22:36:33.123456 America/Cayman']::ARRAY[TIMETZ(3)], ARRAY[]::ARRAY[TIMETZ(4)], null::ARRAY[TIMETZ]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [time(22, 36, 33, 123000, tzinfo=tzoffset(None, 23400)), None, time(22, 36, 33, 123000, tzinfo=tzoffset(None, -19176))]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) def test_1DArray_timestamp_type(self): query = "SELECT ARRAY['276-12-1 11:22:33', '2001-12-01 00:30:45.087', null]::ARRAY[TIMESTAMP], ARRAY[]::ARRAY[TIMESTAMP(4)], null::ARRAY[TIMESTAMP]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [datetime(276, 12, 1, 11, 22, 33), datetime(2001, 12, 1, 0, 30, 45, 87000), None]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) def test_1DArray_timestamptz_type(self): with self._connect() as conn: cur = conn.cursor() cur.execute("SET TIMEZONE 'America/Cayman'") # set session's time zone cur.fetchall() query = "SELECT ARRAY['276-12-1 11:22:33+0630', '2001-12-01 00:30:45.087 America/Cayman', null]::ARRAY[TIMESTAMPTZ], ARRAY[]::ARRAY[TIMESTAMPTZ(4)], null::ARRAY[TIMESTAMPTZ]" cur.execute(query) res = cur.fetchone() self.assertEqual(res[0], [datetime(276, 11, 30, 23, 32, 57, tzinfo=tzoffset(None, -19176)), datetime(2001, 12, 1, 0, 30, 45, 87000, tzinfo=tzoffset(None, -18000)), None]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) def test_1DArray_interval_type(self): query = "SELECT ARRAY['1 02:03:04.0005', '1 02:03:04', '02:03:04.0005', '02:03', null]::ARRAY[INTERVAL DAY TO SECOND], ARRAY[]::ARRAY[INTERVAL DAY TO SECOND], null::ARRAY[INTERVAL DAY TO SECOND]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [relativedelta(days=+1, hours=+2, minutes=+3, seconds=+4, microseconds=+500), relativedelta(days=+1, hours=+2, minutes=+3, seconds=+4), relativedelta(hours=+2, minutes=+3, seconds=+4, microseconds=+500), relativedelta(hours=+2, minutes=+3), None]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) query = "SELECT ARRAY['1 02:03', '02:03', null]::ARRAY[INTERVAL DAY TO MINUTE], ARRAY[]::ARRAY[INTERVAL DAY TO MINUTE], null::ARRAY[INTERVAL DAY TO MINUTE]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [relativedelta(days=+1, hours=+2, minutes=+3), relativedelta(hours=+2, minutes=+3), None]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) query = "SELECT ARRAY['1 02:03', '6', '02:03', null]::ARRAY[INTERVAL DAY TO HOUR], ARRAY[]::ARRAY[INTERVAL DAY TO HOUR], null::ARRAY[INTERVAL DAY TO HOUR]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [relativedelta(days=+1, hours=+2), relativedelta(days=+6), relativedelta(hours=+2), None]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) query = "SELECT ARRAY['123', '-6', null]::ARRAY[INTERVAL DAY], ARRAY[]::ARRAY[INTERVAL DAY], null::ARRAY[INTERVAL DAY]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [relativedelta(days=+123), relativedelta(days=-6), None]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) query = "SELECT ARRAY['02:03:04', '02:03:04.0005', '02:03', null]::ARRAY[INTERVAL HOUR TO SECOND], ARRAY[]::ARRAY[INTERVAL HOUR TO SECOND], null::ARRAY[INTERVAL HOUR TO SECOND]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [relativedelta(hours=+2, minutes=+3, seconds=+4), relativedelta(hours=+2, minutes=+3, seconds=+4, microseconds=+500), relativedelta(hours=+2, minutes=+3), None]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) query = "SELECT ARRAY['02:03:04', '-02:03', null]::ARRAY[INTERVAL HOUR TO MINUTE], ARRAY[]::ARRAY[INTERVAL HOUR TO MINUTE], null::ARRAY[INTERVAL HOUR TO MINUTE]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [relativedelta(hours=+2, minutes=+3), relativedelta(hours=-2, minutes=-3), None]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) query = "SELECT ARRAY['32', '-03', null]::ARRAY[INTERVAL HOUR], ARRAY[]::ARRAY[INTERVAL HOUR], null::ARRAY[INTERVAL HOUR]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [relativedelta(days=+1, hours=+8), relativedelta(hours=-3), None]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) query = "SELECT ARRAY['00:04.0005', '03:04', null]::ARRAY[INTERVAL MINUTE TO SECOND], ARRAY[]::ARRAY[INTERVAL MINUTE TO SECOND], null::ARRAY[INTERVAL MINUTE TO SECOND]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [relativedelta(seconds=+4, microseconds=+500), relativedelta(minutes=+3, seconds=+4), None]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) query = "SELECT ARRAY['03', '-34', null]::ARRAY[INTERVAL MINUTE], ARRAY[]::ARRAY[INTERVAL MINUTE], null::ARRAY[INTERVAL MINUTE]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [relativedelta(minutes=+3), relativedelta(minutes=-34), None]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) query = "SELECT ARRAY['216901.024', '216901', null]::ARRAY[INTERVAL SECOND], ARRAY[]::ARRAY[INTERVAL SECOND], null::ARRAY[INTERVAL SECOND]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [relativedelta(days=+2, hours=+12, minutes=+15, seconds=+1, microseconds=+24000), relativedelta(days=+2, hours=+12, minutes=+15, seconds=+1), None]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) def test_1DArray_intervalYM_type(self): query = "SELECT ARRAY['1y 10m', '1y', '10m ago', null]::ARRAY[INTERVAL YEAR TO MONTH], ARRAY[]::ARRAY[INTERVAL YEAR TO MONTH], null::ARRAY[INTERVAL YEAR TO MONTH]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [relativedelta(years=+1, months=+10), relativedelta(years=+1), relativedelta(months=-10), None]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) query = "SELECT ARRAY['1y ago', '2y', null]::ARRAY[INTERVAL YEAR], ARRAY[]::ARRAY[INTERVAL YEAR], null::ARRAY[INTERVAL YEAR]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [relativedelta(years=-1), relativedelta(years=+2), None]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) query = "SELECT ARRAY['1y 10m', '1y', '10m ago', null]::ARRAY[INTERVAL MONTH], ARRAY[]::ARRAY[INTERVAL MONTH], null::ARRAY[INTERVAL MONTH]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [relativedelta(years=+1, months=+10), relativedelta(years=+1), relativedelta(months=-10), None]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) def test_1DArray_UUID_type(self): query = "SELECT ARRAY['00010203-0405-0607-0809-0a0b0c0d0e0f', null]::ARRAY[UUID], ARRAY[]::ARRAY[UUID], null::ARRAY[UUID]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [UUID('00010203-0405-0607-0809-0a0b0c0d0e0f'), None]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) def test_1DArray_binary_type(self): query = "SELECT ARRAY[hex_to_binary('0x41'), hex_to_binary('0x4243'), null]::ARRAY[BINARY(2)], ARRAY[]::ARRAY[BINARY(4)], null::ARRAY[BINARY]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [b'A\x00', b'BC', None]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) def test_1DArray_varbinary_type(self): query = "SELECT ARRAY[hex_to_binary('0x41'), hex_to_binary('0x4210'), null]::ARRAY[VARBINARY(2)], ARRAY[]::ARRAY[VARBINARY(4)], null::ARRAY[VARBINARY]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [b'A', b'B\x10', None]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) def test_Array_dummy_type(self): query = "SELECT ARRAY[]" res = self._query_and_fetchone(query) self.assertEqual(res[0], []) query = "SELECT ARRAY[ARRAY[]]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [[]]) def test_NDArray_type(self): query = "SELECT ARRAY[ARRAY[1,2],ARRAY[3,4],null,ARRAY[5,null],ARRAY[]]::ARRAY[ARRAY[INT]], ARRAY[]::ARRAY[ARRAY[INT]], null::ARRAY[ARRAY[INT]]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [[1,2], [3,4], None, [5,None], []]) self.assertEqual(res[1], []) self.assertEqual(res[2], None) query = "SELECT ARRAY[ARRAY[ARRAY[null,ARRAY[1,2,3],null,ARRAY[1,null,3],ARRAY[null,null,null]::ARRAY[INT],ARRAY[4,5],ARRAY[],null]]]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [[[None,[1,2,3],None,[1,None,3],[None,None,None],[4,5],[],None]]]) query = "SELECT ARRAY[ARRAY[0.0,0.1,0.2],ARRAY[-1.0,-1.1,-1.2],ARRAY['Infinity'::float, '-Infinity'::float, null, 1.23456e-18]]::ARRAY[ARRAY[FLOAT]]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [[0.0,0.1,0.2], [-1.0,-1.1,-1.2], [float('Inf'), float('-Inf'), None, 1.23456e-18]]) def test_Array_with_Row_type(self): query = "SELECT ARRAY[ROW('Amy' AS name, 2 AS id, '2021-06-10'::DATE AS date),ROW('Fred' AS first_name, 4 AS id, '1998-02-24'::DATE)]" res = self._query_and_fetchone(query) self.assertEqual(res[0], [{"first_name":"Amy","id":2,"date":date(2021, 6, 10)},{"first_name":"Fred","id":4,"date":date(1998, 2, 24)}]) ##################### # tests for SET type ##################### def test_1DSet_boolean_type(self): query = "SELECT SET['t', 'f', null]::SET[BOOL], SET[]::SET[BOOL], null::SET[BOOL]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {True, False, None}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) def test_1DSet_integer_type(self): query = "SELECT SET[0,1,-2,3,null]::SET[INT], SET[]::SET[INT], null::SET[INT]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {0, 1, -2, 3, None}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) def test_1DSet_float_type(self): query = ("SELECT SET['Infinity'::float, '-Infinity'::float, null, -1.234, 0, 1.23456e-18]::SET[FLOAT]," " SET[]::SET[FLOAT], null::SET[FLOAT]") res = self._query_and_fetchone(query) self.assertEqual(res[0], {float('Inf'), float('-Inf'), None, -1.234, 0.0, 1.23456e-18}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) def test_1DSet_numeric_type(self): query = "SELECT SET[-1.12, 0, null, 1234567890123456789.0123456789]::SET[NUMERIC], SET[]::SET[DECIMAL], null::SET[NUMERIC]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {Decimal('-1.1200000000'), Decimal('0E-10'), None, Decimal('1234567890123456789.0123456789')}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) def test_1DSet_char_type(self): query = u"SELECT SET['a', '\u16b1b', null, 'foo']::SET[CHAR(3)], SET[]::SET[CHAR(4)], null::SET[CHAR(5)]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {'a ', u'\u16b1', None, 'foo'}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) def test_1DSet_varchar_type(self): query = u"SELECT SET['', '\u16b1\nb', null, 'foo']::SET[VARCHAR(10),4], SET[]::SET[VARCHAR(4)], null::SET[VARCHAR]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {'', u'\u16b1\nb', None, 'foo'}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) def test_1DSet_date_type(self): query = "SELECT SET['2021-06-10', null, '0221-05-02']::SET[DATE], SET[]::SET[DATE], null::SET[DATE]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {date(2021, 6, 10), None, date(221, 5, 2)}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) def test_1DSet_time_type(self): query = "SELECT SET['00:00:00.00', null, '22:36:33.123956']::SET[TIME(3)], SET[]::SET[TIME(4)], null::SET[TIME]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {time(0, 0, 0), None, time(22, 36, 33, 124000)}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) def test_1DSet_timetz_type(self): query = "SELECT SET['22:36:33.12345+0630', null, '800-02-03 22:36:33.123456 America/Cayman']::SET[TIMETZ(3)], SET[]::SET[TIMETZ(4)], null::SET[TIMETZ]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {time(22, 36, 33, 123000, tzinfo=tzoffset(None, 23400)), None, time(22, 36, 33, 123000, tzinfo=tzoffset(None, -19176))}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) def test_1DSet_timestamp_type(self): query = "SELECT SET['276-12-1 11:22:33', '2001-12-01 00:30:45.087', null]::SET[TIMESTAMP], SET[]::SET[TIMESTAMP(4)], null::SET[TIMESTAMP]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {datetime(276, 12, 1, 11, 22, 33), datetime(2001, 12, 1, 0, 30, 45, 87000), None}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) def test_1DSet_timestamptz_type(self): with self._connect() as conn: cur = conn.cursor() cur.execute("SET TIMEZONE 'America/Cayman'") # set session's time zone cur.fetchall() query = "SELECT SET['276-12-1 11:22:33+0630', '2001-12-01 00:30:45.087 America/Cayman', null]::SET[TIMESTAMPTZ], SET[]::SET[TIMESTAMPTZ(4)], null::SET[TIMESTAMPTZ]" cur.execute(query) res = cur.fetchone() self.assertEqual(res[0], {datetime(276, 11, 30, 23, 32, 57, tzinfo=tzoffset(None, -19176)), datetime(2001, 12, 1, 0, 30, 45, 87000, tzinfo=tzoffset(None, -18000)), None}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) def test_1DSet_interval_type(self): query = "SELECT SET['1 02:03:04.0005', '1 02:03:04', '02:03:04.0005', '02:03', null]::SET[INTERVAL DAY TO SECOND], SET[]::SET[INTERVAL DAY TO SECOND], null::SET[INTERVAL DAY TO SECOND]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {relativedelta(days=+1, hours=+2, minutes=+3, seconds=+4, microseconds=+500), relativedelta(days=+1, hours=+2, minutes=+3, seconds=+4), relativedelta(hours=+2, minutes=+3, seconds=+4, microseconds=+500), relativedelta(hours=+2, minutes=+3), None}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) query = "SELECT SET['1 02:03', '02:03', null]::SET[INTERVAL DAY TO MINUTE], SET[]::SET[INTERVAL DAY TO MINUTE], null::SET[INTERVAL DAY TO MINUTE]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {relativedelta(days=+1, hours=+2, minutes=+3), relativedelta(hours=+2, minutes=+3), None}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) query = "SELECT SET['1 02:03', '6', '02:03', null]::SET[INTERVAL DAY TO HOUR], SET[]::SET[INTERVAL DAY TO HOUR], null::SET[INTERVAL DAY TO HOUR]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {relativedelta(days=+1, hours=+2), relativedelta(days=+6), relativedelta(hours=+2), None}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) query = "SELECT SET['123', '-6', null]::SET[INTERVAL DAY], SET[]::SET[INTERVAL DAY], null::SET[INTERVAL DAY]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {relativedelta(days=+123), relativedelta(days=-6), None}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) query = "SELECT SET['02:03:04', '02:03:04.0005', '02:03', null]::SET[INTERVAL HOUR TO SECOND], SET[]::SET[INTERVAL HOUR TO SECOND], null::SET[INTERVAL HOUR TO SECOND]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {relativedelta(hours=+2, minutes=+3, seconds=+4), relativedelta(hours=+2, minutes=+3, seconds=+4, microseconds=+500), relativedelta(hours=+2, minutes=+3), None}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) query = "SELECT SET['02:03:04', '-02:03', null]::SET[INTERVAL HOUR TO MINUTE], SET[]::SET[INTERVAL HOUR TO MINUTE], null::SET[INTERVAL HOUR TO MINUTE]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {relativedelta(hours=+2, minutes=+3), relativedelta(hours=-2, minutes=-3), None}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) query = "SELECT SET['32', '-03', null]::SET[INTERVAL HOUR], SET[]::SET[INTERVAL HOUR], null::SET[INTERVAL HOUR]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {relativedelta(days=+1, hours=+8), relativedelta(hours=-3), None}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) query = "SELECT SET['00:04.0005', '03:04', null]::SET[INTERVAL MINUTE TO SECOND], SET[]::SET[INTERVAL MINUTE TO SECOND], null::SET[INTERVAL MINUTE TO SECOND]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {relativedelta(seconds=+4, microseconds=+500), relativedelta(minutes=+3, seconds=+4), None}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) query = "SELECT SET['03', '-34', null]::SET[INTERVAL MINUTE], SET[]::SET[INTERVAL MINUTE], null::SET[INTERVAL MINUTE]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {relativedelta(minutes=+3), relativedelta(minutes=-34), None}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) query = "SELECT SET['216901.024', '216901', null]::SET[INTERVAL SECOND], SET[]::SET[INTERVAL SECOND], null::SET[INTERVAL SECOND]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {relativedelta(days=+2, hours=+12, minutes=+15, seconds=+1, microseconds=+24000), relativedelta(days=+2, hours=+12, minutes=+15, seconds=+1), None}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) def test_1DSet_intervalYM_type(self): query = "SELECT SET['1y 10m', '1y', '10m ago', null]::SET[INTERVAL YEAR TO MONTH], SET[]::SET[INTERVAL YEAR TO MONTH], null::SET[INTERVAL YEAR TO MONTH]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {relativedelta(years=+1, months=+10), relativedelta(years=+1), relativedelta(months=-10), None}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) query = "SELECT SET['1y ago', '2y', null]::SET[INTERVAL YEAR], SET[]::SET[INTERVAL YEAR], null::SET[INTERVAL YEAR]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {relativedelta(years=-1), relativedelta(years=+2), None}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) query = "SELECT SET['1y 10m', '1y', '10m ago', null]::SET[INTERVAL MONTH], SET[]::SET[INTERVAL MONTH], null::SET[INTERVAL MONTH]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {relativedelta(years=+1, months=+10), relativedelta(years=+1), relativedelta(months=-10), None}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) def test_1DSet_UUID_type(self): query = "SELECT SET['00010203-0405-0607-0809-0a0b0c0d0e0f', null]::SET[UUID], SET[]::SET[UUID], null::SET[UUID]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {UUID('00010203-0405-0607-0809-0a0b0c0d0e0f'), None}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) def test_1DSet_binary_type(self): query = "SELECT SET[hex_to_binary('0x41'), hex_to_binary('0x4243'), null]::SET[BINARY(2)], SET[]::SET[BINARY(4)], null::SET[BINARY]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {b'A\x00', b'BC', None}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) def test_1DSet_varbinary_type(self): query = "SELECT SET[hex_to_binary('0x41'), hex_to_binary('0x4210'), null]::SET[VARBINARY(2)], SET[]::SET[VARBINARY(4)], null::SET[VARBINARY]" res = self._query_and_fetchone(query) self.assertEqual(res[0], {b'A', b'B\x10', None}) self.assertEqual(res[1], set()) self.assertEqual(res[2], None) def test_Set_dummy_type(self): query = "SELECT SET[]" res = self._query_and_fetchone(query) self.assertEqual(res[0], set()) ##################### # tests for ROW type ##################### def test_1DRow_type(self): query = "SELECT ROW(null, 'Amy', -3::int, '-Infinity'::float, 2.5::numeric, '2021-10-23'::DATE, false::bool, hex_to_binary('0x4210')::VARBINARY), null::ROW(a VARCHAR)" res = self._query_and_fetchone(query) self.assertEqual(res[0], {"f0":None,"f1":"Amy","f2":-3,"f3":float('-Inf'),"f4":Decimal('2.5'),"f5":date(2021, 10, 23),"f6":False, "f7":b'B\x10'}) self.assertEqual(res[1], None) def test_NDRow_type(self): query = "SELECT ROW('Amy',ARRAY[1.5,-2,3.75],ARRAY[ARRAY[false::bool,null,true::bool]])::ROW(name varchar, b ARRAY[NUMERIC], c ARRAY[ARRAY[BOOL]])" res = self._query_and_fetchone(query) self.assertEqual(res[0], {"name":"Amy","b":[Decimal('1.5'),Decimal('-2'),Decimal('3.75')],"c":[[False,None,True]]}) query = "SELECT ROW(ROW(ARRAY[ROW(ARRAY[1,2,3]),ROW(ARRAY[4,5,6]),ROW(ARRAY[7,8,9])]::ARRAY[ROW(d3 ARRAY[INTERVAL DAY])] AS d2) AS d1)" res = self._query_and_fetchone(query) self.assertEqual(res[0], {"d1":{"d2":[ {"d3":[relativedelta(days=+1),relativedelta(days=+2),relativedelta(days=+3)]}, {"d3":[relativedelta(days=+4),relativedelta(days=+5),relativedelta(days=+6)]}, {"d3":[relativedelta(days=+7),relativedelta(days=+8),relativedelta(days=+9)]}]}}) def test_Row_dummy_type(self): query = "SELECT ROW()" res = self._query_and_fetchone(query) self.assertEqual(res[0], {}) query = "SELECT ROW(ROW()), ROW(ARRAY[])" res = self._query_and_fetchone(query) self.assertEqual(res[0], {"f0":{}}) self.assertEqual(res[1], {"f0":[]}) exec(ComplexTypeTestCase.createPrepStmtClass()) vertica-python-1.4.0/vertica_python/tests/integration_tests/test_dates.py000066400000000000000000000165611464740151100271620ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations 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') exec(DateParsingTestCase.createPrepStmtClass()) vertica-python-1.4.0/vertica_python/tests/integration_tests/test_errors.py000066400000000000000000000054701464740151100273730ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations 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") exec(ErrorTestCase.createPrepStmtClass()) vertica-python-1.4.0/vertica_python/tests/integration_tests/test_loadbalance.py000066400000000000000000000316341464740151100303050ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from .base import VerticaPythonIntegrationTestCase class LoadBalanceTestCase(VerticaPythonIntegrationTestCase): def setUp(self): super(LoadBalanceTestCase, self).setUp() self._host = self._conn_info['host'] self._port = self._conn_info['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()))") conn1.commit() 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()))") conn1.commit() 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()))") conn1.commit() 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' self._conn_info['backup_server_node'] = (self._host, self._port) self.assertConnectionFail(TypeError, err_msg) err_msg = ('Each item of connection option "backup_server_node"' r' must be a host string or a \(host, port\) tuple') self._conn_info['backup_server_node'] = [9999] self.assertConnectionFail(TypeError, err_msg) self._conn_info['backup_server_node'] = [(self._host, self._port, 'foo', 9999)] self.assertConnectionFail(TypeError, err_msg) err_msg = 'Host must be a string: invalid value: .*' self._conn_info['backup_server_node'] = [(9999, self._port)] self.assertConnectionFail(TypeError, err_msg) self._conn_info['backup_server_node'] = [(9999, 'port_num')] self.assertConnectionFail(TypeError, err_msg) err_msg = 'Port must be an integer or a string: invalid value: .*' self._conn_info['backup_server_node'] = [(self._host, 5433.0022)] self.assertConnectionFail(TypeError, err_msg) err_msg = r'Port .* is not a valid string: invalid literal for int\(\) with base 10: .*' self._conn_info['backup_server_node'] = [(self._host, 'port_num')] self.assertConnectionFail(ValueError, err_msg) self._conn_info['backup_server_node'] = [(self._host, '5433.0022')] self.assertConnectionFail(ValueError, err_msg) err_msg = 'Invalid port number: .*' self._conn_info['backup_server_node'] = [(self._host, -1000)] self.assertConnectionFail(ValueError, err_msg) self._conn_info['backup_server_node'] = [(self._host, 66000)] self.assertConnectionFail(ValueError, err_msg) self._conn_info['backup_server_node'] = [(self._host, '-1000')] self.assertConnectionFail(ValueError, err_msg) self._conn_info['backup_server_node'] = [(self._host, '66000')] self.assertConnectionFail(ValueError, err_msg) 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()))") conn1.commit() 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]) exec(LoadBalanceTestCase.createPrepStmtClass()) vertica-python-1.4.0/vertica_python/tests/integration_tests/test_timezones.py000066400000000000000000000074201464740151100300710ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from collections import namedtuple from datetime import datetime from dateutil import tz 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=tz.tzutc()) ), ] 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=tz.tzutc()) ), ] self._test_ts(test_cases=test_cases) exec(TimeZoneTestCase.createPrepStmtClass()) vertica-python-1.4.0/vertica_python/tests/integration_tests/test_tls.py000066400000000000000000000343321464740151100266600ustar00rootroot00000000000000# Copyright (c) 2023-2024 Open Text. # # 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. from __future__ import print_function, division, absolute_import, annotations import os import socket import ssl from tempfile import NamedTemporaryFile from ... import errors from .base import VerticaPythonIntegrationTestCase class TlsTestCase(VerticaPythonIntegrationTestCase): SSL_STATE_SQL = 'SELECT ssl_state FROM sessions WHERE session_id=current_session()' def tearDown(self): # Use a non-TLS connection here so cleanup can happen # even if mutual mode is configured self._conn_info['tlsmode'] = 'disable' with self._connect() as conn: cur = conn.cursor() cur.execute("ALTER TLS CONFIGURATION server CERTIFICATE NULL TLSMODE 'DISABLE'") if hasattr(self, 'client_cert'): os.remove(self.client_cert.name) cur.execute("ALTER TLS CONFIGURATION server REMOVE CA CERTIFICATES vp_CA_cert") if hasattr(self, 'client_key'): os.remove(self.client_key.name) cur.execute("DROP KEY IF EXISTS vp_client_key CASCADE") if hasattr(self, 'CA_cert'): os.remove(self.CA_cert.name) cur.execute("DROP KEY IF EXISTS vp_server_key CASCADE") cur.execute("DROP KEY IF EXISTS vp_CA_key CASCADE") for key in ('tlsmode', 'ssl', 'tls_cafile', 'tls_certfile', 'tls_keyfile'): if key in self._conn_info: del self._conn_info[key] super(TlsTestCase, self).tearDown() def _generate_and_set_certificates(self, mutual_mode=False): with self._connect() as conn: cur = conn.cursor() # Generate a root CA private key cur.execute("CREATE KEY vp_CA_key TYPE 'RSA' LENGTH 4096") # Generate a root CA certificate cur.execute("CREATE CA CERTIFICATE vp_CA_cert " "SUBJECT '/C=US/ST=Massachusetts/L=Burlington/O=OpenText/OU=Vertica/CN=Vertica Root CA' " "VALID FOR 3650 EXTENSIONS 'nsComment' = 'Self-signed root CA cert' KEY vp_CA_key") cur.execute("SELECT certificate_text FROM CERTIFICATES WHERE name='vp_CA_cert'") vp_CA_cert = cur.fetchone()[0] with NamedTemporaryFile(delete=False) as self.CA_cert: self.CA_cert.write(vp_CA_cert.encode()) # Generate a server private key cur.execute("CREATE KEY vp_server_key TYPE 'RSA' LENGTH 4096") # Generate a server certificate host = self._conn_info['host'] hostname_for_verify = ('IP:' if host.count('.') == 3 else 'DNS:') + host cur.execute("CREATE CERTIFICATE vp_server_cert " "SUBJECT '/C=US/ST=MA/L=Cambridge/O=Foo/OU=Vertica/CN=Vertica server/emailAddress=abc@example.com' " "SIGNED BY vp_CA_cert EXTENSIONS 'nsComment' = 'Vertica server cert', 'extendedKeyUsage' = 'serverAuth', " f"'subjectAltName' = '{hostname_for_verify}' KEY vp_server_key") if mutual_mode: # Generate a client private key cur.execute("CREATE KEY vp_client_key TYPE 'RSA' LENGTH 4096") cur.execute("SELECT key FROM CRYPTOGRAPHIC_KEYS WHERE name='vp_client_key'") vp_client_key = cur.fetchone()[0] with NamedTemporaryFile(delete=False) as self.client_key: self.client_key.write(vp_client_key.encode()) # Generate a client certificate cur.execute("CREATE CERTIFICATE vp_client_cert " "SUBJECT '/C=US/ST=MA/L=Boston/O=Bar/OU=Vertica/CN=Vertica client/emailAddress=def@example.com' " "SIGNED BY vp_CA_cert EXTENSIONS 'nsComment' = 'Vertica client cert', 'extendedKeyUsage' = 'clientAuth' " "KEY vp_client_key") cur.execute("SELECT certificate_text FROM CERTIFICATES WHERE name='vp_client_cert'") vp_client_cert = cur.fetchone()[0] with NamedTemporaryFile(delete=False) as self.client_cert: self.client_cert.write(vp_client_cert.encode()) # In order to use Mutual Mode, set a server and CA certificate. # This CA certificate is used to verify client certificates cur.execute('ALTER TLS CONFIGURATION server CERTIFICATE vp_server_cert ADD CA CERTIFICATES vp_CA_cert') # Enable TLS. Connection succeeds if Vertica verifies that the client certificate is from a trusted CA. # If the client does not present a client certificate, the connection is rejected. cur.execute("ALTER TLS CONFIGURATION server TLSMODE 'VERIFY_CA'") else: # In order to use Server Mode, set the server certificate for the server's TLS Configuration cur.execute('ALTER TLS CONFIGURATION server CERTIFICATE vp_server_cert') # Enable TLS. Server does not check client certificates. cur.execute("ALTER TLS CONFIGURATION server TLSMODE 'ENABLE'") # For debug # SELECT * FROM tls_configurations WHERE name='server'; # SELECT * FROM CRYPTOGRAPHIC_KEYS; # SELECT * FROM CERTIFICATES; return vp_CA_cert ###################################################### #### Test 'ssl' and 'tlsmode' options are not set #### ###################################################### def test_option_default_server_disable(self): # TLS is disabled on the server with self._connect() as conn: cur = conn.cursor() res = self._query_and_fetchone(self.SSL_STATE_SQL) self.assertEqual(res[0], 'None') def test_option_default_server_enable(self): # Setting certificates with TLS configuration self._generate_and_set_certificates() # TLS is enabled on the server with self._connect() as conn: cur = conn.cursor() res = self._query_and_fetchone(self.SSL_STATE_SQL) self.assertEqual(res[0], 'Server') ####################################################### #### Test 'ssl' and 'tlsmode' options are both set #### ####################################################### def test_tlsmode_over_ssl(self): self._conn_info['tlsmode'] = 'disable' self._conn_info['ssl'] = True with self._connect() as conn: cur = conn.cursor() res = self._query_and_fetchone(self.SSL_STATE_SQL) self.assertEqual(res[0], 'None') ############################################### #### Test 'ssl' option with boolean values #### ############################################### def test_ssl_false(self): self._conn_info['ssl'] = False with self._connect() as conn: cur = conn.cursor() res = self._query_and_fetchone(self.SSL_STATE_SQL) self.assertEqual(res[0], 'None') def test_ssl_true_server_disable(self): # Requires that the server use TLS. If the TLS connection attempt fails, the client rejects the connection. self._conn_info['ssl'] = True self.assertConnectionFail(err_type=errors.SSLNotSupported, err_msg='SSL requested but disabled on the server') def test_ssl_true_server_enable(self): # Setting certificates with TLS configuration self._generate_and_set_certificates() self._conn_info['ssl'] = True with self._connect() as conn: cur = conn.cursor() res = self._query_and_fetchone(self.SSL_STATE_SQL) self.assertEqual(res[0], 'Server') ############################### #### Test 'tlsmode' option #### ############################### def test_TLSMode_disable(self): self._conn_info['tlsmode'] = 'disable' with self._connect() as conn: cur = conn.cursor() res = self._query_and_fetchone(self.SSL_STATE_SQL) self.assertEqual(res[0], 'None') def test_TLSMode_prefer_server_disable(self): # TLS is disabled on the server self._conn_info['tlsmode'] = 'prefer' with self._connect() as conn: cur = conn.cursor() res = self._query_and_fetchone(self.SSL_STATE_SQL) self.assertEqual(res[0], 'None') def test_TLSMode_prefer_server_enable(self): # Setting certificates with TLS configuration self._generate_and_set_certificates() self._conn_info['tlsmode'] = 'prefer' with self._connect() as conn: cur = conn.cursor() res = self._query_and_fetchone(self.SSL_STATE_SQL) self.assertEqual(res[0], 'Server') def test_TLSMode_require_server_disable(self): # Requires that the server use TLS. If the TLS connection attempt fails, the client rejects the connection. self._conn_info['tlsmode'] = 'require' self.assertConnectionFail(err_type=errors.SSLNotSupported, err_msg='SSL requested but disabled on the server') def test_TLSMode_require_server_enable(self): # Setting certificates with TLS configuration self._generate_and_set_certificates() self._conn_info['tlsmode'] = 'require' with self._connect() as conn: cur = conn.cursor() res = self._query_and_fetchone(self.SSL_STATE_SQL) self.assertEqual(res[0], 'Server') def test_TLSMode_verify_ca(self): # Setting certificates with TLS configuration CA_cert = self._generate_and_set_certificates() self._conn_info['tlsmode'] = 'verify-ca' self._conn_info['tls_cafile'] = self.CA_cert.name with self._connect() as conn: cur = conn.cursor() res = self._query_and_fetchone(self.SSL_STATE_SQL) self.assertEqual(res[0], 'Server') def test_TLSMode_verify_full(self): # Setting certificates with TLS configuration CA_cert = self._generate_and_set_certificates() self._conn_info['tlsmode'] = 'verify-full' self._conn_info['tls_cafile'] = self.CA_cert.name with self._connect() as conn: cur = conn.cursor() res = self._query_and_fetchone(self.SSL_STATE_SQL) self.assertEqual(res[0], 'Server') def test_TLSMode_mutual_TLS(self): # Setting certificates with TLS configuration CA_cert = self._generate_and_set_certificates(mutual_mode=True) self._conn_info['tlsmode'] = 'verify-full' self._conn_info['tls_cafile'] = self.CA_cert.name # CA certificate used to verify server certificate self._conn_info['tls_certfile'] = self.client_cert.name # client certificate self._conn_info['tls_keyfile'] = self.client_key.name # private key used for the client certificate with self._connect() as conn: cur = conn.cursor() res = self._query_and_fetchone(self.SSL_STATE_SQL) self.assertEqual(res[0], 'Mutual') ###################################################### #### Test 'ssl' option with ssl.SSLContext object #### ###################################################### def test_sslcontext_require(self): # Setting certificates with TLS configuration self._generate_and_set_certificates() ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE self._conn_info['ssl'] = ssl_context with self._connect() as conn: cur = conn.cursor() res = self._query_and_fetchone(self.SSL_STATE_SQL) self.assertEqual(res[0], 'Server') def test_sslcontext_verify_ca(self): # Setting certificates with TLS configuration CA_cert = self._generate_and_set_certificates() ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ssl_context.verify_mode = ssl.CERT_REQUIRED ssl_context.check_hostname = False ssl_context.load_verify_locations(cadata=CA_cert) # CA certificate used to verify server certificate self._conn_info['ssl'] = ssl_context with self._connect() as conn: cur = conn.cursor() res = self._query_and_fetchone(self.SSL_STATE_SQL) self.assertEqual(res[0], 'Server') def test_sslcontext_verify_full(self): # Setting certificates with TLS configuration CA_cert = self._generate_and_set_certificates() ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ssl_context.verify_mode = ssl.CERT_REQUIRED ssl_context.check_hostname = True # hostname in server cert's subjectAltName ssl_context.load_verify_locations(cadata=CA_cert) # CA certificate used to verify server certificate self._conn_info['ssl'] = ssl_context with self._connect() as conn: cur = conn.cursor() res = self._query_and_fetchone(self.SSL_STATE_SQL) self.assertEqual(res[0], 'Server') def test_sslcontext_mutual_TLS(self): # Setting certificates with TLS configuration CA_cert = self._generate_and_set_certificates(mutual_mode=True) ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ssl_context.verify_mode = ssl.CERT_REQUIRED ssl_context.check_hostname = True # hostname in server cert's subjectAltName ssl_context.load_verify_locations(cadata=CA_cert) # CA certificate used to verify server certificate ssl_context.load_cert_chain(certfile=self.client_cert.name, keyfile=self.client_key.name) self._conn_info['ssl'] = ssl_context with self._connect() as conn: cur = conn.cursor() res = self._query_and_fetchone(self.SSL_STATE_SQL) self.assertEqual(res[0], 'Mutual') exec(TlsTestCase.createPrepStmtClass()) vertica-python-1.4.0/vertica_python/tests/integration_tests/test_transfer_format.py000066400000000000000000000124601464740151100312500ustar00rootroot00000000000000# Copyright (c) 2022-2024 Open Text. # # 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. from __future__ import print_function, division, absolute_import, annotations from .base import VerticaPythonIntegrationTestCase """Check the consistency of query results btw text transfer and binary transfer""" class DataTransferFormatTestCase(VerticaPythonIntegrationTestCase): @classmethod def setUpClass(cls): super(DataTransferFormatTestCase, cls).setUpClass() cls._conn_info['binary_transfer'] = False cls.text_conn = cls._connect() cls._conn_info['binary_transfer'] = True cls.binary_conn = cls._connect() cls.text_cursor = cls.text_conn.cursor() cls.binary_cursor = cls.binary_conn.cursor() @classmethod def tearDownClass(cls): cls.text_conn.close() cls.binary_conn.close() def _test_equal_value(self, sql_type, data_list, assert_almost_equal=False): for data in data_list: query = u"SELECT {}{}".format(data, "::" + sql_type if sql_type else '') self.text_cursor.execute(query) self.binary_cursor.execute(query) text_val = self.text_cursor.fetchone()[0] binary_val = self.binary_cursor.fetchone()[0] if assert_almost_equal: self.assertAlmostEqual(text_val, binary_val) else: self.assertEqual(text_val, binary_val) def test_boolean_type(self): self._test_equal_value("BOOLEAN", ['true', 'false']) def test_integer_type(self): self._test_equal_value("INTEGER", ["-314", "0", "365", "111111111111"]) def test_float_type(self): self._test_equal_value("FLOAT", [ "'Infinity'", "'-Infinity'", "'1.23456e+18'", "'1.23456'", "'1.23456e-18'"]) # binary transfer offers slightly greater precision than text transfer # binary: 1.489968353486419 # text: 1.48996835348642 self._test_equal_value(None, ["ATAN(12.345)"], True) def test_numeric_type(self): self._test_equal_value("NUMERIC", ["0", "-1.1", "1234567890123456789.0123456789"]) self._test_equal_value("DECIMAL", ["123456789.98765"]) def test_char_type(self): self._test_equal_value("CHAR(8)", [u"'\u16b1'"]) self._test_equal_value("VARCHAR", [u"'foo\u16b1'"]) self._test_equal_value("LONG VARCHAR", [u"'foo \u16b1 bar'"]) def test_datetime_type(self): self._test_equal_value("DATE", ["'0340-01-20'", "'2001-12-01'", "'9999-12-31'"]) self._test_equal_value("TIME(3)", ["'00:00:00.00'", "'22:36:33.123956'", "'23:59:59.999'"]) self._test_equal_value("TIMETZ(3)", ["'23:59:59.999-00:30'", "'22:36:33.123456+0630'", "'800-02-03 22:36:33.123456 America/Cayman'"]) self._test_equal_value("TIMESTAMP", ["'276-12-1 11:22:33'", "'2001-12-01 00:30:45.087'"]) self._test_equal_value("TIMESTAMPTZ(4)", ["'1582-09-24 00:30:45.087-08'", "'0001-1-1 11:22:33'", "'2020-12-31 10:43:09.05'"]) def test_interval_type(self): self._test_equal_value("INTERVAL DAY TO SECOND", ["'1 02:03:04.0005'", "'1 02:03:04'", "'02:03:04.0005'", "'02:03'"]) self._test_equal_value("INTERVAL DAY TO MINUTE", ["'1 02:03'", "'02:03'"]) self._test_equal_value("INTERVAL DAY TO HOUR", ["'1 22'"]) self._test_equal_value("INTERVAL DAY", ["'132'"]) self._test_equal_value("INTERVAL HOUR TO SECOND", ["'02:03:04'"]) self._test_equal_value("INTERVAL HOUR TO MINUTE", ["'02:03'"]) self._test_equal_value("INTERVAL HOUR", ["'02'"]) self._test_equal_value("INTERVAL MINUTE TO SECOND", ["'00:04.0005'", "'03:04'"]) self._test_equal_value("INTERVAL MINUTE", ["'03'"]) self._test_equal_value("INTERVAL SECOND", ["'216901.24'", "'216901'"]) self._test_equal_value("INTERVAL YEAR", ["'1y 10m'"]) self._test_equal_value("INTERVAL YEAR TO MONTH", ["'1y 10m'"]) self._test_equal_value("INTERVAL MONTH", ["'1y 10m'"]) def test_UUID_type(self): self.require_protocol_at_least(3 << 16 | 8) self._test_equal_value("UUID", ["'00010203-0405-0607-0809-0a0b0c0d0e0f'", "'123e4567-e89b-12d3-a456-426655440a00'"]) def test_binary_type(self): self._test_equal_value("BINARY(2)", [u"'\303\261'"]) self._test_equal_value("VARBINARY", [u"'\303\261'"]) self._test_equal_value("LONG VARBINARY", [u"'\303\261\303\260'"]) def test_array_type(self): self._test_equal_value("ARRAY[INT]", ["ARRAY[1,2,3]"]) self._test_equal_value("ARRAY[ARRAY[INT]]", ["ARRAY[ARRAY[1,2],ARRAY[3,4]]"]) def test_set_type(self): self._test_equal_value("SET[INT]", ["SET[1,2,3]"]) def test_row_type(self): self._test_equal_value("ROW(name varchar, age int, c ARRAY[INT])", ["ROW('Amy',25,ARRAY[1,2,3])"]) exec(DataTransferFormatTestCase.createPrepStmtClass()) vertica-python-1.4.0/vertica_python/tests/integration_tests/test_unicode.py000066400000000000000000000106201464740151100274760ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations 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) vertica-python-1.4.0/vertica_python/tests/unit_tests/000077500000000000000000000000001464740151100230745ustar00rootroot00000000000000vertica-python-1.4.0/vertica_python/tests/unit_tests/__init__.py000066400000000000000000000033061464740151100252070ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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.vertica-python-1.4.0/vertica_python/tests/unit_tests/base.py000066400000000000000000000045061464740151100243650ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations import pytest from ..common.base import VerticaPythonTestCase @pytest.mark.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 vertica-python-1.4.0/vertica_python/tests/unit_tests/test_errors.py000066400000000000000000000036741464740151100260330ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # # 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. from __future__ import print_function, division, absolute_import, annotations from .base import VerticaPythonUnitTestCase from ...errors import VerticaSyntaxError from ...vertica.messages.backend_messages.error_response import ErrorResponse import pickle # Using a subclass of ErrorResponse for this test, to avoid the complexity of # creating an ErrorResponse object. At the time of writing, ErrorResponse and # other BackendMessage instances can only be created from server-provided data. # # This subclass allows for simpler instantiation without binding to any details # of server data serialization, a la NoticeResponseAttrMixin and NoticeResponse class MockErrorResponse(ErrorResponse): def __init__(self): # does NOT call super self._notice_attrs = {} def error_message(self): return "Manufactured error message for testing" class ErrorsTestCase(VerticaPythonUnitTestCase): def test_pickling(self): err_response = MockErrorResponse() sql = "select 1;" exc = VerticaSyntaxError(err_response, sql) serde = pickle.loads(pickle.dumps(exc)) assert isinstance(serde, VerticaSyntaxError) assert str(serde) == str(exc) assert isinstance(serde.error_response, MockErrorResponse) assert serde.error_response.error_message() == err_response.error_message() assert serde.sql == exc.sql vertica-python-1.4.0/vertica_python/tests/unit_tests/test_logging.py000066400000000000000000000032651464740151100261410ustar00rootroot00000000000000# Copyright (c) 2019-2024 Open Text. # # 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. from __future__ import print_function, division, absolute_import, annotations import logging import os from ...vertica.log import VerticaLogging from .base import VerticaPythonUnitTestCase class LoggingTestCase(VerticaPythonUnitTestCase): def test_file_handler(self): logger_name = "test_file_handler" logger = logging.getLogger(logger_name) self.assertNotEqual(logging.getLevelName(logger.getEffectiveLevel()), 'DEBUG') log_file = os.path.join(self.test_config['log_dir'], 'test_file_handler.log') VerticaLogging.setup_logging(logger_name, log_file, 'DEBUG') self.assertEqual(len(logger.handlers), 1) self.assertEqual(logging.getLevelName(logger.getEffectiveLevel()), 'DEBUG') def test_missing_file(self): logger_name = "test_missing_file" logger = logging.getLogger(logger_name) VerticaLogging.setup_logging(logger_name, None, 'DEBUG') VerticaLogging.setup_logging(logger_name, '', 'DEBUG') self.assertEqual(len(logger.handlers), 0) self.assertEqual(logging.getLevelName(logger.getEffectiveLevel()), 'DEBUG') vertica-python-1.4.0/vertica_python/tests/unit_tests/test_notice.py000066400000000000000000000062561464740151100257770ustar00rootroot00000000000000# Copyright (c) 2019-2024 Open Text. # # 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. from __future__ import print_function, division, absolute_import, annotations import mock from .base import VerticaPythonUnitTestCase from ...vertica.messages import NoticeResponse from ...errors import QueryError class NoticeTestCase(VerticaPythonUnitTestCase): SAMPLE_DATA = {b'S': 'FATAL', b'H': 'This is a test hint', b'L': '9999', b'M': 'Failure is on purpose'} @mock.patch.object(NoticeResponse, '_unpack_data') def test_error_message(self, mock_unpack_data): mock_unpack_data.return_value = NoticeTestCase.SAMPLE_DATA notice = NoticeResponse(b'ignored-due-to-mock') self.assertEqual( notice.error_message(), 'Severity: FATAL, Message: Failure is on purpose, Hint: This is a test hint, Line: 9999' ) @mock.patch.object(NoticeResponse, '_unpack_data') def test_attribute_properties(self, mock_unpack_data): mock_unpack_data.return_value = NoticeTestCase.SAMPLE_DATA notice = NoticeResponse(b'ignored-due-to-mock') self.assertEqual(notice.severity, 'FATAL') self.assertEqual(notice.hint, 'This is a test hint') # yes, line is still a string. self.assertEqual(notice.line, '9999') self.assertEqual(notice.message, 'Failure is on purpose') self.assertIsNone(notice.detail) self.assertIsNone(notice.sqlstate) @mock.patch.object(NoticeResponse, '_unpack_data') def test_labeled_values(self, mock_unpack_data): mock_unpack_data.return_value = NoticeTestCase.SAMPLE_DATA notice = NoticeResponse(b'ignored-due-to-mock') self.assertEqual(notice.values, { 'Severity': 'FATAL', 'Hint': 'This is a test hint', 'Line': '9999', 'Message': 'Failure is on purpose'}) @mock.patch.object(NoticeResponse, '_unpack_data') def test_query_error(self, mock_unpack_data): mock_unpack_data.return_value = NoticeTestCase.SAMPLE_DATA notice = NoticeResponse(b'ignored-due-to-mock') query_error = QueryError(notice, 'Select Fake();') self.assertEqual(query_error.severity, 'FATAL') self.assertEqual(query_error.hint, 'This is a test hint') self.assertEqual(query_error.line, '9999') self.assertEqual(query_error.message, 'Failure is on purpose') self.assertIsNone(query_error.detail) self.assertIsNone(query_error.sqlstate) self.assertEqual( str(query_error), 'Severity: FATAL, Message: Failure is on purpose, Hint: This is a test hint, Line: 9999, SQL: \'Select Fake();\'') vertica-python-1.4.0/vertica_python/tests/unit_tests/test_parsedsn.py000066400000000000000000000120421464740151100263230ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # # 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. from __future__ import print_function, division, absolute_import, annotations from .base import VerticaPythonUnitTestCase from ...vertica.connection import parse_dsn class ParseDSNTestCase(VerticaPythonUnitTestCase): def test_basic(self): dsn = 'vertica://admin@192.168.10.1' expected = {'host': '192.168.10.1', 'user': 'admin'} parsed = parse_dsn(dsn) self.assertDictEqual(expected, parsed) dsn = 'vertica://mike@127.0.0.1/db1' expected = {'host': '127.0.0.1', 'user': 'mike', 'database': 'db1'} parsed = parse_dsn(dsn) self.assertDictEqual(expected, parsed) dsn = 'vertica://john:pwd@example.com:5433/db1' expected = {'database': 'db1', 'host': 'example.com', 'password': 'pwd', 'port': 5433, 'user': 'john'} parsed = parse_dsn(dsn) self.assertDictEqual(expected, parsed) def test_str_arguments(self): dsn = ('vertica://john:pwd@localhost:5433/db1?' 'session_label=vpclient&unicode_error=strict&' 'log_path=/home/admin/vClient.log&log_level=DEBUG&' 'oauth_access_token=GciOiJSUzI1NiI&' 'workload=python_test_workload&tlsmode=verify-ca&' 'tls_cafile=tls/ca_cert.pem&tls_certfile=tls/cert.pem&' 'tls_keyfile=tls/key.pem&' 'kerberos_service_name=krb_service&kerberos_host_name=krb_host') expected = {'database': 'db1', 'host': 'localhost', 'user': 'john', 'password': 'pwd', 'port': 5433, 'log_level': 'DEBUG', 'session_label': 'vpclient', 'unicode_error': 'strict', 'log_path': '/home/admin/vClient.log', 'oauth_access_token': 'GciOiJSUzI1NiI', 'workload': 'python_test_workload', 'tlsmode': 'verify-ca', 'tls_cafile': 'tls/ca_cert.pem', 'tls_certfile': 'tls/cert.pem', 'tls_keyfile': 'tls/key.pem', 'kerberos_service_name': 'krb_service', 'kerberos_host_name': 'krb_host'} parsed = parse_dsn(dsn) self.assertDictEqual(expected, parsed) def test_boolean_arguments(self): dsn = ('vertica://mike@127.0.0.1/db1?connection_load_balance=True&' 'use_prepared_statements=0&ssl=false&disable_copy_local=on&' 'autocommit=true&binary_transfer=1&request_complex_types=off') expected = {'database': 'db1', 'connection_load_balance': True, 'use_prepared_statements': False, 'ssl': False, 'disable_copy_local': True, 'autocommit': True, 'binary_transfer': True, 'request_complex_types': False, 'host': '127.0.0.1', 'user': 'mike'} parsed = parse_dsn(dsn) self.assertDictEqual(expected, parsed) def test_numeric_arguments(self): dsn = 'vertica://mike@127.0.0.1/db1?connection_timeout=1.5&log_level=10' expected = {'host': '127.0.0.1', 'user': 'mike', 'database': 'db1', 'connection_timeout': 1.5, 'log_level': 10} parsed = parse_dsn(dsn) self.assertDictEqual(expected, parsed) def test_ignored_arguments(self): # Invalid value dsn = ('vertica://mike@127.0.0.1/db1?ssl=ssl_context&' 'connection_load_balance=unknown') expected = {'host': '127.0.0.1', 'user': 'mike', 'database': 'db1'} parsed = parse_dsn(dsn) self.assertDictEqual(expected, parsed) # Unsupported argument dsn = 'vertica://mike@127.0.0.1/db1?backup_server_node=123.456.789.123' expected = {'host': '127.0.0.1', 'user': 'mike', 'database': 'db1'} parsed = parse_dsn(dsn) self.assertDictEqual(expected, parsed) def test_overwrite_arguments(self): dsn = 'vertica://mike@127.0.0.1/db1?ssl=on&ssl=off&ssl=1&ssl=0' expected = {'host': '127.0.0.1', 'user': 'mike', 'database': 'db1', 'ssl': False} parsed = parse_dsn(dsn) self.assertDictEqual(expected, parsed) def test_arguments_blank_values(self): dsn = ('vertica://mike@127.0.0.1/db1?connection_timeout=1.5&log_path=&' 'ssl=&connection_timeout=2&log_path=&connection_timeout=') expected = {'host': '127.0.0.1', 'user': 'mike', 'database': 'db1', 'connection_timeout': 2.0, 'log_path': ''} parsed = parse_dsn(dsn) self.assertDictEqual(expected, parsed) vertica-python-1.4.0/vertica_python/tests/unit_tests/test_sql_literal.py000066400000000000000000000067101464740151100270240ustar00rootroot00000000000000# Copyright (c) 2020-2024 Open Text. # # 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. from __future__ import print_function, division, absolute_import, annotations from collections import namedtuple from decimal import Decimal from uuid import UUID import datetime import pytest from ...vertica.cursor import Cursor from .base import VerticaPythonUnitTestCase class SqlLiteralTestCase(VerticaPythonUnitTestCase): def test_default_adapters(self): cursor = Cursor(None, self.logger) # None self.assertEqual(cursor.object_to_sql_literal(None), "NULL") # Boolean self.assertEqual(cursor.object_to_sql_literal(True), "True") self.assertEqual(cursor.object_to_sql_literal(False), "False") # Numeric self.assertEqual(cursor.object_to_sql_literal(123), "123") self.assertEqual(cursor.object_to_sql_literal(123.45), "123.45") self.assertEqual(cursor.object_to_sql_literal(Decimal("10.00000")), "10.00000") # UUID self.assertEqual(cursor.object_to_sql_literal( UUID('00010203-0405-0607-0809-0a0b0c0d0e0f')), "'00010203-0405-0607-0809-0a0b0c0d0e0f'") # Time self.assertEqual(cursor.object_to_sql_literal( datetime.datetime(2018, 9, 7, 15, 38, 19, 769000)), "'2018-09-07 15:38:19.769000'") self.assertEqual(cursor.object_to_sql_literal(datetime.date(2018, 9, 7)), "'2018-09-07'") self.assertEqual(cursor.object_to_sql_literal(datetime.time(13, 50, 9)), "'13:50:09'") # String self.assertEqual(cursor.object_to_sql_literal(u"string'1"), "'string''1'") self.assertEqual(cursor.object_to_sql_literal(b"string'1"), "'string''1'") # Tuple and namedtuple self.assertEqual(cursor.object_to_sql_literal( (123, u"string'1", None)), "(123,'string''1',NULL)") self.assertEqual(cursor.object_to_sql_literal( ((1, u"a"), (2, u"b"), (3, u"c"))), "((1,'a'),(2,'b'),(3,'c'))") Point = namedtuple('Point', ['x', 'y', 'z']) p = Point(x=11, y=22, z=33) self.assertEqual(cursor.object_to_sql_literal(p), "(11,22,33)") def test_register_adapters(self): class Point(object): def __init__(self, x, y): self.x = x self.y = y def adapt_point(point): return "STV_GeometryPoint({},{})".format(point.x, point.y) cursor = Cursor(None, self.logger) err_msg = "Please register a new adapter for this type" with pytest.raises(TypeError, match=err_msg): result = cursor.object_to_sql_literal(Point(-71.13, 42.36)) err_msg = "The adapter is not callable" with pytest.raises(TypeError, match=err_msg): cursor.register_sql_literal_adapter(Point, "not-callable") cursor.register_sql_literal_adapter(Point, adapt_point) self.assertEqual( cursor.object_to_sql_literal(Point(-71.13, 42.36)), "STV_GeometryPoint(-71.13,42.36)") vertica-python-1.4.0/vertica_python/tests/unit_tests/test_timestamps.py000066400000000000000000000110471464740151100266760ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from collections import namedtuple from datetime import datetime from .base import VerticaPythonUnitTestCase from ...vertica.deserializer import load_timestamp_text TimestampTestingCase = namedtuple("TimestampTestingCase", ["string", "timestamp"]) class TimestampParsingTestCase(VerticaPythonUnitTestCase): def _test_timestamps(self, test_cases, msg=None): for tc in test_cases: self.assertEqual(load_timestamp_text(tc.string, None), 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') vertica-python-1.4.0/vertica_python/vertica/000077500000000000000000000000001464740151100211665ustar00rootroot00000000000000vertica-python-1.4.0/vertica_python/vertica/__init__.py000066400000000000000000000033071464740151100233020ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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. vertica-python-1.4.0/vertica_python/vertica/column.py000066400000000000000000000116041464740151100230370ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from typing import TYPE_CHECKING, NamedTuple if TYPE_CHECKING: from typing import Optional from ..datatypes import getDisplaySize, getPrecision, getScale from ..compat import as_str, as_text # Data of a particular SQL data type might be transmitted in either "text" format or "binary" format. # The desired format for any column is specified by a format code. class FormatCode(object): TEXT = 0 BINARY = 1 class ColumnTuple(NamedTuple): name: str type_code: int display_size: Optional[int] internal_size: int precision: Optional[int] scale: Optional[int] null_ok: bool class Column(object): def __init__(self, col) -> None: # Describe one query result column self.name = col['name'] self.type_code = col['data_type_oid'] self.type_name = col['data_type_name'] self.table_oid = col['table_oid'] self.schema_name = col['schema_name'] self.table_name = col['table_name'] self.attribute_number = col['attribute_number'] self.display_size = getDisplaySize(col['data_type_oid'], col['type_modifier']) self.internal_size = col['data_type_size'] self.precision = getPrecision(col['data_type_oid'], col['type_modifier']) self.scale = getScale(col['data_type_oid'], col['type_modifier']) self.null_ok = col['null_ok'] self.is_identity = col['is_identity'] self.format_code = col['format_code'] self.child_columns = None self.props = ColumnTuple(self.name, self.type_code, self.display_size, self.internal_size, self.precision, self.scale, self.null_ok) def add_child_column(self, col: Column) -> None: """ Complex types involve multiple columns arranged in a hierarchy of parents and children. Each parent column stores references to child columns in a list. """ if self.child_columns is None: self.child_columns = [] self.child_columns.append(col) def debug_info(self) -> str: childs = "" if self.child_columns: c = ", ".join([col.debug_info() for col in self.child_columns]) childs = f", child_columns=[{c}]" return (f"Column(name={self.name}, data_type_oid={self.type_code}, data_type_name={self.type_name}, " f"schema_name={self.schema_name}, table_name={self.table_name}, table_oid={self.table_oid}, " f"attribute_number={self.attribute_number}, precision={self.precision}, scale={self.scale}, " f"null_ok={self.null_ok}, is_identity={self.is_identity}, format_code={self.format_code}, " f"internal_size={self.internal_size}, display_size={self.display_size}{childs}" ")") def __str__(self): return as_str(str(self.props)) def __unicode__(self): return as_text(str(self.props)) def __repr__(self): return as_str(str(self.props)) def __iter__(self): for prop in self.props: yield prop def __getitem__(self, key): return self.props[key] def __len__(self): return len(self.props) vertica-python-1.4.0/vertica_python/vertica/connection.py000066400000000000000000001225521464740151100237060ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations import base64 import getpass import logging import random import socket import ssl import uuid import warnings from collections import deque from struct import unpack # noinspection PyCompatibility,PyUnresolvedReferences from urllib.parse import urlparse, parse_qs from typing import TYPE_CHECKING, NamedTuple if TYPE_CHECKING: from typing import Any, Dict, List, Optional, Type, Union, Deque, Tuple import vertica_python 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 from ..vertica.tlsmode import TLSMode DEFAULT_HOST = 'localhost' DEFAULT_PORT = 5433 DEFAULT_PASSWORD = '' DEFAULT_DATABASE = '' DEFAULT_AUTOCOMMIT = False DEFAULT_BACKUP_SERVER_NODE = [] DEFAULT_KRB_SERVICE_NAME = 'vertica' DEFAULT_LOG_LEVEL = logging.WARNING DEFAULT_LOG_PATH = 'vertica_python.log' DEFAULT_BINARY_TRANSFER = False DEFAULT_REQUEST_COMPLEX_TYPES = True DEFAULT_OAUTH_ACCESS_TOKEN = '' DEFAULT_WORKLOAD = '' DEFAULT_TLSMODE = 'prefer' try: DEFAULT_USER = getpass.getuser() except Exception as e: DEFAULT_USER = None warnings.warn(f"Cannot get the login user name: {str(e)}") def connect(**kwargs: Any) -> Connection: """Opens a new connection to a Vertica database.""" return Connection(kwargs) def parse_dsn(dsn: str) -> Dict[str, Union[str, int, bool, float]]: """Parse connection string (DSN) into a dictionary of keywords and values. Connection string format: vertica://:@:/?k1=v1&k2=v2&... """ url = urlparse(dsn) if url.scheme != 'vertica': raise ValueError("Only vertica:// scheme is supported.") # Ignore blank/invalid values result: Dict[str, Union[str, int, bool, float]] = {k: v for k, v in ( ('host', url.hostname), ('port', url.port), ('user', url.username), ('password', url.password), ('database', url.path[1:])) if v } for key, values in parse_qs(url.query, keep_blank_values=True).items(): # Try to get the last non-blank value in the list of values for each key value = '' for i in reversed(range(len(values))): value = values[i] if value != '': break if value == '' and key != 'log_path': # blank values are to be ignored continue elif key == 'backup_server_node': continue elif key in ('connection_load_balance', 'use_prepared_statements', 'disable_copy_local', 'ssl', 'autocommit', 'binary_transfer', 'request_complex_types'): lower = value.lower() if lower in ('true', 'on', '1'): result[key] = True elif lower in ('false', 'off', '0'): result[key] = False elif key == 'connection_timeout': result[key] = float(value) elif key == 'log_level' and value.isdigit(): result[key] = int(value) else: result[key] = value return result class _AddressEntry(NamedTuple): host: str resolved: bool data: Any class _AddressList(object): def __init__(self, host: str, port: Union[int, str], backup_nodes: List[Union[str, Tuple[str, Union[int, str]]]], logger: logging.Logger) -> None: """Creates a new deque with the primary host first, followed by any backup hosts""" self._logger = logger # Items in address_deque are _AddressEntry values. # host is the original hostname/ip, used by SSL option check_hostname # - when resolved is False, data is port # - when resolved is True, data is the 5-tuple from socket.getaddrinfo # This allows for lazy resolution. Seek peek() for more. self.address_deque: Deque['_AddressEntry'] = 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, str): 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: str, port: Union[int, str]) -> None: if not isinstance(host, str): 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, (str, int)): 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, str): 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(_AddressEntry(host=host, resolved=False, data=port)) def push(self, host: str, port: int) -> None: self.address_deque.appendleft(_AddressEntry(host=host, resolved=False, data=port)) def pop(self) -> None: self.address_deque.popleft() def peek(self): # do lazy DNS resolution, returning the leftmost socket.getaddrinfo result if len(self.address_deque) == 0: return None while len(self.address_deque) > 0: self._logger.debug('Peek at address list: {0}'.format(list(self.address_deque))) entry = self.address_deque[0] if entry.resolved: # return a resolved sockaddrinfo return entry.data else: # DNS resolve a single host name to multiple IP addresses self.pop() # keep host and port info for adding address entry to deque once it has been resolved host, port = entry.host, entry.data try: resolved_hosts = socket.getaddrinfo(host, port, 0, 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 addrinfo (AF_INET and AF_INET6 only) to deque random.shuffle(resolved_hosts) for addrinfo in resolved_hosts: if addrinfo[0] in (socket.AF_INET, socket.AF_INET6): self.address_deque.appendleft(_AddressEntry( host=host, resolved=True, data=addrinfo)) return None def peek_host(self) -> Optional[str]: """Return the leftmost host result.""" self._logger.debug('Peek host at address list: {0}'.format(list(self.address_deque))) if len(self.address_deque) == 0: return None return self.address_deque[0].host def _generate_session_label() -> str: return '{type}-{version}-{id}'.format( type='vertica-python', version=vertica_python.__version__, id=uuid.uuid1() ) class Connection(object): def __init__(self, options: Optional[Dict[str, Any]] = None) -> None: self.parameters: Dict[str, Union[str, int]] = {} self.session_id = None self.backend_pid = None self.backend_key = None self.transaction_status = None self.socket = None self.socket_as_file = None options = options or {} self.options = parse_dsn(options['dsn']) if 'dsn' in options else {} self.options.update({key: value for key, value in options.items() \ if key == 'log_path' or (key != 'dsn' and value is not None)}) # 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_logging(logger_name, self.options['log_path'], self.options['log_level'], str(id(self))) self.options.setdefault('host', DEFAULT_HOST) self.options.setdefault('port', DEFAULT_PORT) self.options.setdefault('database', DEFAULT_DATABASE) self.options.setdefault('password', DEFAULT_PASSWORD) self.options.setdefault('autocommit', DEFAULT_AUTOCOMMIT) self.options.setdefault('session_label', _generate_session_label()) self.options.setdefault('backup_server_node', DEFAULT_BACKUP_SERVER_NODE) self.options.setdefault('workload', DEFAULT_WORKLOAD) self.kerberos_is_set = self.options.get('kerberos_host_name', None) or self.options.get('kerberos_service_name', None) self.options.setdefault('kerberos_service_name', DEFAULT_KRB_SERVICE_NAME) # Kerberos authentication hostname defaults to the host value here so # the correct value cannot be overwritten by load balancing or failover self.options.setdefault('kerberos_host_name', self.options['host']) self.address_list = _AddressList(self.options['host'], self.options['port'], self.options['backup_server_node'], self._logger) # OAuth authentication setup self.options.setdefault('oauth_access_token', DEFAULT_OAUTH_ACCESS_TOKEN) if not isinstance(self.options['oauth_access_token'], str): raise TypeError(f'The value of connection option "oauth_access_token" should be a str') # user is required for non-OAuth connections if 'user' not in self.options: if len(self.options['oauth_access_token']) > 0: self.options['user'] = '' elif DEFAULT_USER: self.options['user'] = DEFAULT_USER else: msg = 'Connection option "user" is required' self._logger.error(msg) raise KeyError(msg) # we only support one cursor per connection self.options.setdefault('unicode_error', None) self._cursor = Cursor(self, self._logger, cursor_type=None, unicode_error=self.options['unicode_error']) # knob for using server-side prepared statements self.options.setdefault('use_prepared_statements', False) self._logger.debug('Connection prepared statements is {}'.format( 'enabled' if self.options['use_prepared_statements'] else 'disabled')) # knob for disabling COPY LOCAL operations self.options.setdefault('disable_copy_local', False) self._logger.debug('COPY LOCAL operation is {}'.format( 'disabled' if self.options['disable_copy_local'] else 'enabled')) # knob for using binary transfer format or text transfer format self.options.setdefault('binary_transfer', DEFAULT_BINARY_TRANSFER) self._logger.debug('Data binary transfer is {}'.format( 'enabled' if self.options['binary_transfer'] else 'disabled')) # knob for requesting complex types metadata self.options.setdefault('request_complex_types', DEFAULT_REQUEST_COMPLEX_TYPES) self._logger.debug('Complex types metadata is {}'.format( 'requested' if self.options['request_complex_types'] else 'not requested')) 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() # Complex types metadata is returned since protocol version 3.12 self.complex_types_enabled = self.parameters.get('protocol_version', 0) >= (3 << 16 | 12) and \ self.parameters.get('request_complex_types', 'off') == 'on' self._logger.info('Connection is ready') ############################################# # supporting `with` statements ############################################# def __enter__(self): return self def __exit__(self, type_, value, traceback): self.close() ############################################# # dbapi methods ############################################# def close(self) -> None: """Close the connection now.""" self._logger.info('Close the connection') try: self.write(messages.Terminate()) finally: self.close_socket() def commit(self) -> None: """Commit any pending transaction to the database.""" if self.closed(): raise errors.ConnectionError('Connection is closed') cur = self.cursor() cur.execute('COMMIT;') def rollback(self) -> None: """Roll back to the start of any pending transaction.""" if self.closed(): raise errors.ConnectionError('Connection is closed') cur = self.cursor() cur.execute('ROLLBACK;') def cursor(self, cursor_type: Union[None, str, Type[List[Any]], Type[Dict[Any, Any]]] = None) -> Cursor: """Return the Cursor Object using the connection. vertica-python only support one cursor per connection. Argument cursor_type determines the type of query result rows. The following cases return each row as a list. E.g. [ [1, 'foo'], [2, 'bar'] ] - cursor() - cursor(cursor_type=list) - cursor(cursor_type='list') The following cases return each row as a dict with column names as keys. E.g. [ {'id': 1, 'value': 'foo'}, {'id': 2, 'value': 'bar'} ] - cursor(cursor_type=dict) - cursor(cursor_type='dict') """ if self.closed(): raise errors.ConnectionError('Connection is closed') if self._cursor.closed(): self._cursor._closed = False # let user change type if they want? self._cursor.cursor_type = cursor_type return self._cursor ############################################# # non-dbapi methods ############################################# @property def autocommit(self) -> bool: """Read the connection's AUTOCOMMIT setting from cache.""" # For a new session, autocommit is off by default return self.parameters.get('auto_commit', 'off') == 'on' @autocommit.setter def autocommit(self, value: bool) -> None: """Change the connection's AUTOCOMMIT setting.""" if self.autocommit is value: return val = 'on' if value else 'off' cur = self.cursor() cur.execute('SET SESSION AUTOCOMMIT TO {}'.format(val), use_prepared_statements=False) cur.fetchall() # check for errors and update the cache def cancel(self) -> None: """Cancel the current database operation. This method can be called from a different thread than the one currently executing a database operation. """ if self.closed(): raise errors.ConnectionError('Connection is closed') self._logger.info('Canceling the current database operation') # Must create a new socket connection to the server temp_socket = self.establish_socket_connection(self.address_list) self.write(CancelRequest(self.backend_pid, self.backend_key), temp_socket) temp_socket.close() self._logger.info('Cancel request issued') def opened(self) -> bool: """Returns True if the connection is opened.""" return (self.socket is not None and self.backend_pid is not None and self.transaction_status is not None) def closed(self) -> bool: """Returns True if the connection is closed.""" return not self.opened() def __str__(self) -> str: safe_options = {key: value for key, value in self.options.items() if key != 'password'} s1 = " socket.socket: """Create a TCP socket object.""" raw_socket = socket.socket(family, 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) return raw_socket def balance_load(self, raw_socket: socket.socket) -> socket.socket: # Send load balance request and read server response self._logger.debug('=> %s', messages.LoadBalanceRequest()) raw_socket.sendall(messages.LoadBalanceRequest().get_message()) response = raw_socket.recv(1) if response == b'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)) self._logger.debug('<= %s', res) host = res.get_host() port = res.get_port() self._logger.info('Load balancing to host "{0}" on port {1}'.format(host, port)) peer = raw_socket.getpeername() socket_host, socket_port = peer[0], peer[1] 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_socket_connection(self.address_list) else: self._logger.debug('<= LoadBalanceResponse: %s', response) no_load_balancing = "Load balancing requested but not supported by server" warnings.warn(no_load_balancing) self._logger.warning(no_load_balancing) return raw_socket def enable_ssl(self, raw_socket: socket.socket, ssl_context: ssl.SSLContext, force: bool) -> Union[socket.socket, ssl.SSLSocket]: # Send SSL request and read server response self._logger.debug('=> %s', messages.SslRequest()) raw_socket.sendall(messages.SslRequest().get_message()) response = raw_socket.recv(1) self._logger.debug('<= SslResponse: %s', response) if response == b'S': self._logger.info('Enabling TLS') try: server_host = self.address_list.peek_host() if server_host is None: # This should not happen msg = 'Cannot get the connected server host while enabling TLS' self._logger.error(msg) raise errors.ConnectionError(msg) raw_socket = ssl_context.wrap_socket(raw_socket, server_hostname=server_host) except ssl.CertificateError as e: raise errors.ConnectionError(str(e)) except ssl.SSLError as e: raise errors.ConnectionError(str(e)) elif force: err_msg = "SSL requested but disabled on the server" self._logger.error(err_msg) raise errors.SSLNotSupported(err_msg) else: msg = 'TLS is not configured on the server. Proceeding with an unencrypted channel.' hint = "\nHINT: Set connection option 'tlsmode' to 'disable' to explicitly create a non-TLS connection." warnings.warn(msg + hint) self._logger.warning(msg) return raw_socket def establish_socket_connection(self, address_list: _AddressList) -> socket.socket: """Given a list of database node addresses, establish the socket connection to the database server. Return a connected socket object. """ addrinfo = address_list.peek() raw_socket = None last_exception = None # Failover: loop to try all addresses while addrinfo: (family, _socktype, _proto, _canonname, sockaddr) = addrinfo last_exception = None # _AddressList filters all addrs to AF_INET and AF_INET6, which both # have host and port as values 0, 1 in the sockaddr tuple. host = sockaddr[0] port = sockaddr[1] self._logger.info('Establishing connection to host "{0}" on port {1}'.format(host, port)) try: raw_socket = self.create_socket(family) raw_socket.connect(sockaddr) 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 address_list.pop() addrinfo = 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) -> bool: """Returns True if the TCP socket is a SSL socket.""" return self.socket is not None and isinstance(self.socket, ssl.SSLSocket) def write(self, message: FrontendMessage, vsocket: Optional[Union[socket.socket, ssl.SSLSocket]] = None) -> None: if not isinstance(message, FrontendMessage): raise TypeError("invalid message: ({0})".format(message)) if vsocket is None: vsocket = self._socket() self._logger.debug('=> %s', message) try: for data in message.fetch_message(): size = 8192 # Max msg size, consistent with how the server works pos = 0 while pos < len(data): sent = vsocket.send(data[pos : pos + size]) if sent == 0: raise errors.ConnectionError("Couldn't send message: Socket connection broken") pos += sent except Exception as e: self.close_socket() self._logger.error(str(e)) if isinstance(e, IOError): raise errors.ConnectionError(str(e)) else: raise def close_socket(self) -> None: self._logger.debug("Close connection's socket") try: if self.socket is not None: self._socket().close() if self.socket_as_file is not None: self._socket_as_file().close() finally: self.reset_values() def reset_connection(self) -> None: self.close() self.startup_connection() def is_asynchronous_message(self, message: BackendMessage) -> bool: # Check if it is an asynchronous response message # Note: ErrorResponse is a subclass of NoticeResponse return (isinstance(message, messages.ParameterStatus) or (isinstance(message, messages.NoticeResponse) and not isinstance(message, messages.ErrorResponse))) def handle_asynchronous_message(self, message: Union[messages.ParameterStatus, messages.NoticeResponse]) -> None: if isinstance(message, messages.ParameterStatus): if message.name == 'protocol_version': message.value = int(message.value) self.parameters[message.name] = message.value elif (isinstance(message, messages.NoticeResponse) and not isinstance(message, messages.ErrorResponse)): if getattr(self, 'notice_handler', None) is not None: self.notice_handler(message) else: notice = f'{message.severity} {message.error_code}: {message.message}' if message.hint is not None: notice += f'\nHINT: {message.hint}' warnings.warn(notice) self._logger.warning(message.error_message()) def read_string(self) -> bytearray: s = bytearray() while True: char = self.read_bytes(1) if char == b'\x00': break s.extend(char) return s def read_message(self) -> BackendMessage: while True: try: type_ = self.read_bytes(1) size = unpack('!I', self.read_bytes(4))[0] if size < 4: raise errors.MessageError("Bad message size: {0}".format(size)) if type_ == messages.WriteFile.message_id: # The whole WriteFile message may not be read at here. # Instead, only the file name and file length is read. # This is because the message could be too large to read all at once. f = self.read_string() filename = f.decode('utf-8') file_length = unpack('!I', self.read_bytes(4))[0] size -= 4 + len(f) + 1 + 4 if size != file_length: raise errors.MessageError("Bad message size: {0}".format(size)) if filename == '': # If there is no filename, then this is really RETURNREJECTED data, not a rejected file if file_length % 8 != 0: raise errors.MessageError("Bad RETURNREJECTED data size: {0}".format(file_length)) data = self.read_bytes(file_length) message = messages.WriteFile(filename, file_length, data) else: # The rest of the message is read later with write_to_disk() message = messages.WriteFile(filename, file_length) elif type_ == messages.RowDescription.message_id: message = BackendMessage.from_type(type_, self.read_bytes(size - 4), complex_types_enabled=self.complex_types_enabled) else: message = BackendMessage.from_type(type_, self.read_bytes(size - 4)) self._logger.debug('<= %s', message) self.handle_asynchronous_message(message) # handle transaction status if isinstance(message, messages.ReadyForQuery): self.transaction_status = message.transaction_status except (SystemError, IOError) as e: self.close_socket() # noinspection PyTypeChecker self._logger.error(e) raise errors.ConnectionError(str(e)) if not self.is_asynchronous_message(message): break return message def read_expected_message(self, expected_types, error_handler=None): # Reads a message and does some basic error handling. # expected_types must be a class (e.g. messages.BindComplete) or a tuple of classes message = self.read_message() if isinstance(message, expected_types): return message elif isinstance(message, messages.ErrorResponse): if error_handler is not None: error_handler(message) else: raise errors.DatabaseError(message.error_message()) else: msg = 'Received unexpected message type: {}. '.format(type(message).__name__) if isinstance(expected_types, tuple): msg += 'Expected types: {}'.format(", ".join([t.__name__ for t in expected_types])) else: msg += 'Expected type: {}'.format(expected_types.__name__) self._logger.error(msg) raise errors.MessageError(msg) def read_bytes(self, n: int) -> bytes: if n == 1: result = self._socket_as_file().read(1) if not result: raise errors.ConnectionError("Connection closed by Vertica") return result else: buf = b"" to_read = n while to_read > 0: data = self._socket_as_file().read(to_read) received = len(data) if received == 0: raise errors.ConnectionError("Connection closed by Vertica") buf += data to_read -= received return buf def send_GSS_response_and_receive_challenge(self, response): # Send the GSS response data to the vertica server token = base64.b64decode(response) self.write(messages.Password(token, messages.Authentication.GSS)) # Receive the challenge from the vertica server message = self.read_expected_message(messages.Authentication) if message.code != messages.Authentication.GSS_CONTINUE: msg = ('Received unexpected message type: Authentication(type={}).' ' Expected type: Authentication(type={})'.format( message.code, messages.Authentication.GSS_CONTINUE)) self._logger.error(msg) raise errors.MessageError(msg) return message.auth_data def make_GSS_authentication(self) -> None: try: import kerberos except ImportError as e: raise errors.ConnectionError("{}\nCannot make a Kerberos " "authentication because no Kerberos package is installed. " "Get it with 'pip install kerberos'.".format(str(e))) # Set GSS flags gssflag = (kerberos.GSS_C_DELEG_FLAG | kerberos.GSS_C_MUTUAL_FLAG | kerberos.GSS_C_SEQUENCE_FLAG | kerberos.GSS_C_REPLAY_FLAG) # Generate the GSS-style service principal name service_principal = "{}@{}".format(self.options['kerberos_service_name'], self.options['kerberos_host_name']) # Initializes a context object with a service principal self._logger.info('Initializing a context for GSSAPI client-side ' 'authentication with service principal {}'.format(service_principal)) try: result, context = kerberos.authGSSClientInit(service_principal, gssflags=gssflag) except kerberos.GSSError as err: msg = "GSSAPI initialization error: {}".format(str(err)) self._logger.error(msg) raise errors.KerberosError(msg) if result != kerberos.AUTH_GSS_COMPLETE: msg = ('Failed to initialize a context for GSSAPI client-side ' 'authentication with service principal {}'.format(service_principal)) self._logger.error(msg) raise errors.KerberosError(msg) # Processes GSSAPI client-side steps try: challenge = b'' while True: self._logger.info('Processing a single GSSAPI client-side step') challenge = base64.b64encode(challenge).decode("utf-8") result = kerberos.authGSSClientStep(context, challenge) if result == kerberos.AUTH_GSS_COMPLETE: self._logger.info('Result: GSSAPI step complete') break elif result == kerberos.AUTH_GSS_CONTINUE: self._logger.info('Result: GSSAPI step continuation') # Get the response from the last successful GSSAPI client-side step response = kerberos.authGSSClientResponse(context) challenge = self.send_GSS_response_and_receive_challenge(response) else: msg = "GSSAPI client-side step error status {}".format(result) self._logger.error(msg) raise errors.KerberosError(msg) except kerberos.GSSError as err: msg = "GSSAPI client-side step error: {}".format(str(err)) self._logger.error(msg) raise errors.KerberosError(msg) def startup_connection(self) -> None: user = self.options['user'] database = self.options['database'] session_label = self.options['session_label'] os_user_name = DEFAULT_USER if DEFAULT_USER else '' password = self.options['password'] autocommit = self.options['autocommit'] binary_transfer = self.options['binary_transfer'] request_complex_types = self.options['request_complex_types'] oauth_access_token = self.options['oauth_access_token'] workload = self.options['workload'] if len(oauth_access_token) > 0: auth_category = 'OAuth' elif self.kerberos_is_set: auth_category = 'Kerberos' elif password: auth_category = 'User' else: auth_category = '' self.write(messages.Startup(user, database, session_label, os_user_name, autocommit, binary_transfer, request_complex_types, oauth_access_token, workload, auth_category)) while True: message = self.read_message() if isinstance(message, messages.Authentication): if message.code == messages.Authentication.OK: self._logger.info("User {} successfully authenticated" .format(self.options['user'])) elif message.code == messages.Authentication.CHANGE_PASSWORD: msg = "The password for user {} has expired".format(self.options['user']) self._logger.error(msg) raise errors.ConnectionError(msg) elif message.code == messages.Authentication.PASSWORD_GRACE: password_grace = f'The password for user {self.options["user"]} will expire soon. Please consider changing it.' warnings.warn(password_grace) self._logger.warning(password_grace) elif message.code == messages.Authentication.GSS: self.make_GSS_authentication() elif message.code == messages.Authentication.OAUTH: self.write(messages.Password(oauth_access_token, message.code)) else: self.write(messages.Password(password, message.code, {'user': user, 'salt': getattr(message, 'salt', None), 'usersalt': getattr(message, 'usersalt', None)})) elif isinstance(message, messages.BackendKeyData): self.backend_pid = message.pid self.backend_key = message.key elif isinstance(message, messages.ReadyForQuery): break elif isinstance(message, messages.ErrorResponse): self._logger.error(message.error_message()) raise errors.ConnectionError(message.error_message()) else: msg = "Received unexpected startup message: {0}".format(message) self._logger.error(msg) raise errors.MessageError(msg) vertica-python-1.4.0/vertica_python/vertica/cursor.py000066400000000000000000001360211464740151100230600ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations import datetime import glob import inspect import os import re import sys import traceback import warnings from decimal import Decimal from io import IOBase, BytesIO, StringIO from math import isnan from tempfile import NamedTemporaryFile, SpooledTemporaryFile, TemporaryFile from uuid import UUID from collections import OrderedDict # _TemporaryFileWrapper is an undocumented implementation detail, so # import defensively. try: from tempfile import _TemporaryFileWrapper except ImportError: _TemporaryFileWrapper = None from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import IO, Any, AnyStr, Callable, Dict, Generator, List, NoReturn, Optional, Sequence, Tuple, Type, TypeVar, Union from typing_extensions import Self from .connection import Connection from logging import Logger T = TypeVar('T') from .. import errors, os_utils from ..compat import as_text from ..vertica import messages from ..vertica.column import Column from ..vertica.deserializer import Deserializer from ..vertica.messages.message import BackendMessage # A note regarding support for temporary files: # # Since Python 2.6, the tempfile module offers three kinds of temporary # files: # # * NamedTemporaryFile # * SpooledTemporaryFile # * TemporaryFile # # NamedTemporaryFile is not a class, but a function that returns # an instance of the tempfile._TemporaryFileWrapper class. # _TemporaryFileWrapper is a direct subclass of object. # # * https://github.com/python/cpython/blob/v3.8.0/Lib/tempfile.py#L546 # * https://github.com/python/cpython/blob/v3.8.0/Lib/tempfile.py#L450 # # SpooledTemporaryFile is a class that is a direct subclass of object. # # * https://github.com/python/cpython/blob/v3.8.0/Lib/tempfile.py#L623 # # TemporaryFile is a class that is either NamedTemporaryFile or an # indirect subclass of io.IOBase, depending on the platform. # # * https://bugs.python.org/issue33762 # * https://github.com/python/cpython/blob/v3.8.0/Lib/tempfile.py#L552-L555 # * https://github.com/python/cpython/blob/v3.8.0/Lib/tempfile.py#L606-L608 # * https://github.com/python/cpython/blob/v3.8.0/Lib/tempfile.py#L617-L618 # # As a result, for Python 2.6 and newer, it seems the best way to test # for a file-like object inclusive of temporary files is via: # # isinstance(obj, (IOBase, SpooledTemporaryFile, _TemporaryFileWrapper)) # Of the following "types", only include those that are classes in # file_type so that isinstance(obj, file_type) won't fail. As of Python # 3.8 only IOBase, SpooledTemporaryFile and _TemporaryFileWrapper are # classes, but if future Python versions implement NamedTemporaryFile # and TemporaryFile as classes, the following code should account for # that accordingly. file_type = tuple( type_ for type_ in [ IOBase, NamedTemporaryFile, SpooledTemporaryFile, TemporaryFile, _TemporaryFileWrapper, ] if inspect.isclass(type_) ) RE_NAME_BASE = u"[0-9a-zA-Z_][\\w\\d\\$_]*" RE_NAME = u'(("{0}")|({0}))'.format(RE_NAME_BASE) RE_BASIC_INSERT_STAT = ( u"\\s*INSERT\\s+INTO\\s+(?P({0}\\.)?{0})" u"\\s*\\(\\s*(?P{0}(\\s*,\\s*{0})*)\\s*\\)" u"\\s+VALUES\\s*\\(\\s*(?P(.|\\s)*)\\s*\\)").format(RE_NAME) END_OF_RESULT_RESPONSES = (messages.CommandComplete, messages.PortalSuspended) END_OF_BATCH_RESPONSES = (messages.WriteFile, messages.EndOfBatchResponse) DEFAULT_BUFFER_SIZE = 131072 class Cursor(object): # 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: Connection, logger: Logger, cursor_type: Union[None, str, Type[List[Any]], Type[Dict[Any, Any]]] = None, unicode_error: Optional[str] = None) -> 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 self._message = None self.operation = None self.prepared_sql = None # last statement been prepared self.prepared_name = "s0" self._sql_literal_adapters = {} self._disable_sqldata_converter = False self._sqldata_converters = {} self._des = Deserializer() # # dbapi attributes # self.description = None self.rowcount = -1 self.arraysize = 1 ############################################# # supporting `with` statements ############################################# def __enter__(self) -> Self: return self def __exit__(self, type_, value, traceback): self.close() ############################################# # decorators ############################################# def handle_ctrl_c(func): """ On Ctrl-C, try to cancel the query in the server """ def wrap(self, *args, **kwargs): try: return func(self, *args, **kwargs) except KeyboardInterrupt: self.connection.cancel() self.flush_to_query_ready() # ignore errors.QueryCanceled raise return wrap ############################################# # dbapi methods ############################################# # noinspection PyMethodMayBeStatic def callproc(self, procname, parameters=None) -> NoReturn: raise errors.NotSupportedError('Cursor.callproc() is not implemented') def close(self) -> None: """Close the cursor now.""" self._logger.info('Close the cursor') if not self.closed() and self.prepared_sql: self._close_prepared_statement() self._closed = True @handle_ctrl_c def execute(self, operation: str, parameters: Optional[Union[List[Any], Tuple[Any], Dict[str, Any]]] = None, use_prepared_statements: Optional[bool] = None, copy_stdin: Optional[Union[IO[AnyStr], List[IO[AnyStr]]]] = None, buffer_size: int = DEFAULT_BUFFER_SIZE) -> Self: """Execute a query or command to the database.""" if self.closed(): raise errors.InterfaceError('Cursor is closed') self.flush_to_query_ready() operation = as_text(operation) self.operation = operation self.rowcount = -1 if copy_stdin is None: self.copy_stdin_list = [] elif (isinstance(copy_stdin, list) and all(callable(getattr(i, 'read', None)) for i in copy_stdin)): self.copy_stdin_list = copy_stdin elif callable(getattr(copy_stdin, 'read', None)): self.copy_stdin_list = [copy_stdin] else: raise TypeError("Cursor.execute 'copy_stdin' parameter should be" " a file-like object or a list of file-like objects") self.buffer_size = buffer_size # For copy-local read and write use_prepared = bool(self.connection.options['use_prepared_statements'] if use_prepared_statements is None else use_prepared_statements) if use_prepared: ################################################################# # Execute the SQL as prepared statement (server-side bindings) ################################################################# if parameters is not None: if not isinstance(parameters, (list, tuple)): raise TypeError("Execute parameters should be a list/tuple") elif parameters and '?' not in operation: raise ValueError(f'Invalid SQL: {operation}' '\nHINT: When use_prepared_statements=True, variables in SQL should be specified with ' 'question mark (?) placeholders. Positional format (%s) placeholders have to be used ' 'with use_prepared_statements=False setting.') # If the SQL has not been prepared, prepare the SQL if operation != self.prepared_sql: self._prepare(operation) self.prepared_sql = operation # the prepared statement is kept # Bind the parameters and execute self._execute_prepared_statement([parameters]) else: ################################################################# # Execute the SQL directly (client-side bindings) ################################################################# if parameters: operation = self.format_operation_with_parameters(operation, parameters) self._execute_simple_query(operation) return self @handle_ctrl_c def executemany(self, operation: str, seq_of_parameters: Sequence[Union[List[Any], Tuple[Any], Dict[str, Any]]], use_prepared_statements: Optional[bool] = None) -> None: """Execute the same command with a sequence of input data.""" if not isinstance(seq_of_parameters, (list, tuple)): raise TypeError("seq_of_parameters should be list/tuple") if self.closed(): raise errors.InterfaceError('Cursor is closed') self.flush_to_query_ready() operation = as_text(operation) self.operation = operation use_prepared = bool(self.connection.options['use_prepared_statements'] if use_prepared_statements is None else use_prepared_statements) if use_prepared: ################################################################# # Execute the SQL as prepared statement (server-side bindings) ################################################################# if len(seq_of_parameters) == 0: raise ValueError("seq_of_parameters should not be empty") if not all(isinstance(elem, (list, tuple)) for elem in seq_of_parameters): raise TypeError("Each seq_of_parameters element should be a list/tuple") # If the SQL has not been prepared, prepare the SQL if operation != self.prepared_sql: self._prepare(operation) self.prepared_sql = operation # the prepared statement is kept # Bind the parameters and execute self._execute_prepared_statement(seq_of_parameters) else: ################################################################# # Rewrite the INSERT SQL into a COPY statement ################################################################# m = self._insert_statement.match(operation) if m: target = as_text(m.group('target')) variables = as_text(m.group('variables')) variables = ",".join([variable.strip().strip('"') for variable in variables.split(",")]) values = as_text(m.group('values')) values = "|".join([value.strip().strip('"') for value in values.split(",")]) seq_of_values = [self.format_operation_with_parameters(values, parameters, is_copy_data=True) for parameters in seq_of_parameters] data = "\n".join(seq_of_values) copy_statement = ( u"COPY {0} ({1}) FROM STDIN " u"ENCLOSED BY '''' " # '/r' will have trouble if ENCLOSED BY is not set u"ENFORCELENGTH ABORT ON ERROR{2}").format(target, variables, " NO COMMIT" if not self.connection.autocommit else '') self.copy(copy_statement, data) else: raise NotImplementedError( "executemany is implemented for simple INSERT statements only") def fetchone(self) -> Optional[Union[List[Any], OrderedDict[str, Any]]]: """Return the next record from the current statement result set.""" while True: if isinstance(self._message, messages.DataRow): if self.rowcount == -1: self.rowcount = 1 else: self.rowcount += 1 row = self.row_formatter(self._message) # fetch next message self._message = self.connection.read_message() return row elif isinstance(self._message, messages.RowDescription): self.description = self._message.get_description() self._deserializers = self.get_deserializers() elif isinstance(self._message, messages.ReadyForQuery): return None elif isinstance(self._message, END_OF_RESULT_RESPONSES): return None elif isinstance(self._message, messages.EmptyQueryResponse): pass elif isinstance(self._message, messages.VerifyFiles): self._handle_copy_local_protocol() elif isinstance(self._message, messages.EndOfBatchResponse): pass elif isinstance(self._message, messages.CopyDoneResponse): pass elif isinstance(self._message, messages.ErrorResponse): raise errors.QueryError.from_error_response(self._message, self.operation) else: raise errors.MessageError('Unexpected fetchone() state: {}'.format( type(self._message).__name__)) self._message = self.connection.read_message() def fetchmany(self, size: Optional[int] = None) -> List[Union[List[Any], OrderedDict[str, Any]]]: """Return the next `size` records from the current statement result set. `size` default to `cursor.arraysize` if not specified. """ if not size: size = self.arraysize results = [] while True: row = self.fetchone() if not row: break results.append(row) if len(results) >= size: break return results def fetchall(self) -> List[Union[List[Any], OrderedDict[str, Any]]]: """Return all the remaining records from the current statement result set.""" return list(self.iterate()) def nextset(self) -> bool: """ Skip to the next available result set, discarding any remaining rows from the current result set. If there are no more result sets, this method returns False. Otherwise, it returns a True and subsequent calls to the fetch*() methods will return rows from the next result set. """ # skip any data for this set if exists self.flush_to_end_of_result() if self._message is None: return False elif isinstance(self._message, END_OF_RESULT_RESPONSES): # there might be another set, read next message to find out self._message = self.connection.read_message() if isinstance(self._message, messages.RowDescription): self.description = self._message.get_description() self._deserializers = self.get_deserializers() self._message = self.connection.read_message() if isinstance(self._message, messages.VerifyFiles): self._handle_copy_local_protocol() self.rowcount = -1 return True elif isinstance(self._message, messages.BindComplete): self._message = self.connection.read_message() self.rowcount = -1 return True elif isinstance(self._message, messages.ReadyForQuery): return False elif isinstance(self._message, END_OF_RESULT_RESPONSES): # result of a DDL/transaction self.rowcount = -1 return True elif isinstance(self._message, messages.CopyInResponse): raise errors.MessageError( 'Unexpected nextset() state after END_OF_RESULT_RESPONSES: {self._message}\n' 'HINT: Do you pass multiple COPY statements into Cursor.copy()?') elif isinstance(self._message, messages.ErrorResponse): raise errors.QueryError.from_error_response(self._message, self.operation) else: raise errors.MessageError( 'Unexpected nextset() state after END_OF_RESULT_RESPONSES: {0}'.format(self._message)) elif isinstance(self._message, messages.ReadyForQuery): # no more sets left to be read return False else: raise errors.MessageError('Unexpected nextset() state: {0}'.format(self._message)) def setinputsizes(self, sizes) -> None: pass def setoutputsize(self, size, column=None) -> None: pass ############################################# # non-dbapi methods ############################################# def closed(self) -> bool: """Returns True if the cursor is closed.""" return self._closed or self.connection.closed() def cancel(self) -> NoReturn: """Cancel is a session-level operation, cursor-level API does not make sense. Keep this API for backward compatibility. """ raise errors.NotSupportedError( 'Cursor.cancel() is deprecated. Call Connection.cancel() ' 'to cancel the current database operation.') def iterate(self) -> Generator[Union[List[Any], OrderedDict[str, Any]], None, None]: """Yield the next record from the current statement result set.""" row = self.fetchone() while row: yield row row = self.fetchone() def copy(self, sql: str, data: Union[IO[AnyStr], bytes, str], **kwargs: Any) -> None: """ Execute a "COPY FROM STDIN" SQL. EXAMPLE: ``` >> with open("/tmp/file.csv", "rb") as fs: >> cursor.copy("COPY table(field1,field2) FROM STDIN DELIMITER ',' ENCLOSED BY ''''", >> fs, buffer_size=65536) ``` """ sql = as_text(sql) self.operation = sql if self.closed(): raise errors.InterfaceError('Cursor is closed') self.flush_to_query_ready() if isinstance(data, bytes): stream = BytesIO(data) elif isinstance(data, str): stream = StringIO(data) elif isinstance(data, file_type) or callable(getattr(data, 'read', None)): stream = data else: raise TypeError("Not valid type of data {0}".format(type(data))) self._logger.info(u'Execute COPY statement: [{}]'.format(sql)) # Execute a `COPY FROM STDIN` SQL statement self.connection.write(messages.Query(sql)) self.buffer_size = kwargs.get('buffer_size', DEFAULT_BUFFER_SIZE) while True: message = self.connection.read_message() self._message = message if isinstance(message, messages.ErrorResponse): raise errors.QueryError.from_error_response(message, sql) elif isinstance(message, messages.ReadyForQuery): break elif isinstance(message, messages.CommandComplete): break elif isinstance(message, messages.CopyInResponse): try: self._send_copy_data(stream, self.buffer_size) except Exception as e: # COPY termination: report the cause of failure to the backend self.connection.write(messages.CopyFail(str(e))) self._logger.error(str(e)) self.flush_to_query_ready() raise errors.DataError('Failed to send a COPY data stream: {}'.format(str(e))) # Successful termination for COPY self.connection.write(messages.CopyDone()) elif isinstance(message, messages.RowDescription): raise errors.MessageError(f'Unexpected message: {message}\n' f'HINT: Query for Cursor.copy() should be a `COPY FROM STDIN` SQL statement.' ' `COPY FROM LOCAL` should be executed with Cursor.execute().\n' f'SQL: {sql}') else: raise errors.MessageError(f'Unexpected message: {message}') def object_to_sql_literal(self, py_obj: Any) -> str: """Returns the SQL literal string converted from a Python object.""" return self.object_to_string(py_obj, False) def register_sql_literal_adapter(self, obj_type: T, adapter_func: Callable[[T], str]) -> None: """Register a sql literal adapter, which adapt a Python type/class to SQL literals.""" if not callable(adapter_func): raise TypeError("Cannot register this sql literal adapter. The adapter is not callable.") self._sql_literal_adapters[obj_type] = adapter_func @property def disable_sqldata_converter(self) -> bool: return self._disable_sqldata_converter @disable_sqldata_converter.setter def disable_sqldata_converter(self, value: bool) -> None: """By default, the client does data conversions for query results: reading a bytes sequence from server and creating a Python object out of it. If set to True, bypass conversions from SQL type raw data to the native Python object. """ self._disable_sqldata_converter = bool(value) def register_sqldata_converter(self, oid: int, converter_func: Callable[[bytes, Dict[str, Any]], Any]) -> None: """Customize how SQL data values are converted to Python objects when query results are returned.""" if not isinstance(oid, int): raise TypeError(f"sqldata converters should be registered on oid integer, got {oid} instead.") if not callable(converter_func): raise TypeError("Cannot register this sqldata converter. The converter is not callable.") # For an oid, transfer format (BINARY/TEXT) is fixed in a connection self._sqldata_converters[oid] = converter_func # For prepared statements, need to reset self._deserializers if self.description: self._deserializers = self.get_deserializers() def unregister_sqldata_converter(self, oid: int) -> None: """Cancel customized SQL data values converter and use the default converter.""" if oid in self._sqldata_converters: del self._sqldata_converters[oid] # For prepared statements, need to reset self._deserializers if self.description: self._deserializers = self.get_deserializers() else: no_such_oid = f'Nothing was unregistered (oid={oid})' warnings.warn(no_such_oid) ############################################# # internal ############################################# def get_deserializers(self): return self._des.get_row_deserializers( self.description, self._sqldata_converters, {'unicode_error': self.unicode_error, 'session_tz': self.connection.parameters.get('timezone', 'unknown'), 'complex_types_enabled': self.connection.complex_types_enabled,} ) def flush_to_query_ready(self) -> None: # if the last message isn't empty or ReadyForQuery, read all remaining messages if self._message is None \ or isinstance(self._message, messages.ReadyForQuery): return while True: message = self.connection.read_message() if isinstance(message, messages.ReadyForQuery): self._message = message break elif isinstance(message, messages.VerifyFiles): self._message = message self._handle_copy_local_protocol() def flush_to_end_of_result(self) -> None: # if the last message isn't empty or END_OF_RESULT_RESPONSES, # read messages until it is if (self._message is None or isinstance(self._message, messages.ReadyForQuery) or isinstance(self._message, END_OF_RESULT_RESPONSES)): return while True: message = self.connection.read_message() if (isinstance(message, messages.ReadyForQuery) or isinstance(message, END_OF_RESULT_RESPONSES)): self._message = message break def row_formatter(self, row_data): if self.cursor_type is None: return self.format_row_as_array(row_data) elif self.cursor_type in (list, 'list'): return self.format_row_as_array(row_data) elif self.cursor_type in (dict, 'dict'): return self.format_row_as_dict(row_data) else: raise TypeError('Unrecognized cursor_type: {0}'.format(self.cursor_type)) def format_row_as_dict(self, row_data) -> OrderedDict[str, Any]: if self._disable_sqldata_converter: return OrderedDict((descr.name, value) for descr, value in zip(self.description, row_data.values)) return OrderedDict( (descr.name, convert(value)) for descr, convert, value in zip(self.description, self._deserializers, row_data.values) ) def format_row_as_array(self, row_data) -> List[Any]: if self._disable_sqldata_converter: return row_data.values return [convert(value) for convert, value in zip(self._deserializers, row_data.values)] def object_to_string(self, py_obj: Any, is_copy_data: bool, is_collection: bool = False) -> str: """Return the SQL representation of the object as a string""" if type(py_obj) in self._sql_literal_adapters and not is_copy_data: adapter = self._sql_literal_adapters[type(py_obj)] result = adapter(py_obj) if not isinstance(result, (str, bytes)): raise TypeError("Unexpected return type of {} adapter: {}, expected a string type." .format(type(py_obj), type(result))) return as_text(result) if isinstance(py_obj, type(None)): return '' if is_copy_data and not is_collection else 'NULL' elif isinstance(py_obj, bool): return str(py_obj) elif isinstance(py_obj, (str, bytes)): return self.format_quote(as_text(py_obj), is_copy_data, is_collection) elif isinstance(py_obj, (int, Decimal)): return str(py_obj) elif isinstance(py_obj, float): if not is_copy_data and py_obj in (float('Inf'), float('-Inf')) or isnan(py_obj): return f"'{str(py_obj)}'::FLOAT" return str(py_obj) elif isinstance(py_obj, tuple): # tuple and namedtuple elements = [None] * len(py_obj) for i in range(len(py_obj)): elements[i] = self.object_to_string(py_obj[i], is_copy_data) return "(" + ",".join(elements) + ")" elif isinstance(py_obj, list): elements = [None] * len(py_obj) if is_copy_data: for i in range(len(py_obj)): elements[i] = self.object_to_string(py_obj[i], True, True) return f'[{",".join(elements)}]' else: for i in range(len(py_obj)): elements[i] = self.object_to_string(py_obj[i], False) # Use the ARRAY keyword to construct an array value return f'ARRAY[{",".join(elements)}]' elif isinstance(py_obj, set): elements = [None] * len(py_obj) i = 0 if is_copy_data: for o in py_obj: elements[i] = self.object_to_string(o, True, True) i += 1 return f'[{",".join(elements)}]' else: for o in py_obj: elements[i] = self.object_to_string(o, False) i += 1 # Use the SET keyword to construct a set value return f'SET[{",".join(elements)}]' elif isinstance(py_obj, dict) and not is_copy_data: elements = [None] * len(py_obj) i = 0 for k, v in py_obj.items(): elements[i] = self.object_to_string(v, False) + f' AS "{k}"' i += 1 # Use the ROW keyword to construct a row value return f'ROW({",".join(elements)})' elif isinstance(py_obj, (datetime.datetime, datetime.date, datetime.time, UUID)): return self.format_quote(as_text(str(py_obj)), is_copy_data, is_collection) else: if is_copy_data: return str(py_obj) else: msg = ("Cannot convert {} type object to an SQL string. " "Please register a new adapter for this type via the " "Cursor.register_sql_literal_adapter() function." .format(type(py_obj))) raise TypeError(msg) # noinspection PyArgumentList def format_operation_with_parameters(self, operation: str, parameters: Union[List[Any], Tuple[Any], Dict[str, Any]], is_copy_data: bool = False) -> str: if isinstance(parameters, dict): if parameters and ':' not in operation and os.environ.get('VERTICA_PYTHON_IGNORE_NAMED_PARAMETER_CHECK') != '1': raise ValueError(f'Invalid SQL: {operation}' "\nHINT: When argument 'parameters' is a dict, variables in SQL should be specified with named (:name) placeholders." " If you use a dict to represent the value of a ROW type column, enclose the dict with brackets('[]') to construct a list.") for key, param in parameters.items(): if not isinstance(key, str): key = str(key) key = as_text(key) value = self.object_to_string(param, is_copy_data) # Using a regex with word boundary to correctly handle params with similar names # such as :s and :start match_str = u":{0}\\b".format(key) operation = re.sub(match_str, lambda _: value, operation, flags=re.U) elif isinstance(parameters, (tuple, list)): if parameters and '%s' not in operation: raise ValueError(f'Invalid SQL: {operation}' "\nHINT: When argument 'parameters' is a tuple/list, " 'variables in SQL should be specified with positional format (%s) placeholders. ' 'Question mark (?) placeholders have to be used with use_prepared_statements=True setting.') tlist = [] for param in parameters: value = self.object_to_string(param, is_copy_data) tlist.append(value) operation = operation % tuple(tlist) else: raise TypeError("Argument 'parameters' must be dict or tuple/list") return operation def format_quote(self, param: str, is_copy_data: bool, is_collection: bool) -> str: if is_collection: # COPY COLLECTIONENCLOSE s = list(param) for i, c in enumerate(param): if c in '\\\n\"': s[i] = "\\" + c return u'"{0}"'.format(u"".join(s)) elif is_copy_data: # COPY ENCLOSED BY s = list(param) for i, c in enumerate(param): if c in '\\|\n\'': s[i] = "\\" + c return u"'{0}'".format(u"".join(s)) else: return u"'{0}'".format(param.replace(u"'", u"''")) def _execute_simple_query(self, query: str) -> None: """ Send the query to the server using the simple query protocol. """ self._logger.info(u'Execute simple query: [{}]'.format(query)) # All of the statements in the query are sent here in a single message self.connection.write(messages.Query(query)) # The first response could be a number of things: # ErrorResponse: Something went wrong on the server. # EmptyQueryResponse: The query being executed is empty. (e.g. the string "--comment") # RowDescription: This is the "normal" case when executing a query. # It marks the start of the results. # CommandComplete: This occurs when executing DDL/transactions. self._message = self.connection.read_message() if isinstance(self._message, messages.ErrorResponse): raise errors.QueryError.from_error_response(self._message, query) elif isinstance(self._message, messages.RowDescription): self.description = self._message.get_description() self._deserializers = self.get_deserializers() self._message = self.connection.read_message() if isinstance(self._message, messages.ErrorResponse): raise errors.QueryError.from_error_response(self._message, query) elif isinstance(self._message, messages.VerifyFiles): self._handle_copy_local_protocol() def _handle_copy_local_protocol(self) -> None: if self.connection.options['disable_copy_local']: msg = 'COPY LOCAL operation is disabled.' self.connection.write(messages.CopyError(msg)) self.flush_to_query_ready() raise errors.InterfaceError(msg) # Extract info from VerifyFiles message input_files = self._message.input_files rejections_file = self._message.rejections_file exceptions_file = self._message.exceptions_file # Verify the file(s) present in the COPY FROM LOCAL statement are indeed accessible self.valid_write_file_path = [] try: # Check that the output files are writable if rejections_file != '': if rejections_file not in self.operation: raise errors.MessageError('Server requests for writing to' ' invalid rejected file path: {}'.format(rejections_file)) os_utils.check_file_writable(rejections_file) self.valid_write_file_path.append(rejections_file) if exceptions_file != '': if exceptions_file not in self.operation: raise errors.MessageError('Server requests for writing to' ' invalid exceptions file path: {}'.format(exceptions_file)) os_utils.check_file_writable(exceptions_file) self.valid_write_file_path.append(exceptions_file) # Check that the input files are readable self.valid_read_file_path = self._check_copy_local_files(input_files) self.connection.write(messages.VerifiedFiles(self.valid_read_file_path, self.connection.parameters.get('protocol_version', 0))) except Exception as e: tb = sys.exc_info()[2] stk = traceback.extract_tb(tb, 1) self.connection.write(messages.CopyError(str(e), stk[0])) self.flush_to_query_ready() raise # Server should be ready to receive copy data from STDIN/files self._message = self.connection.read_message() if isinstance(self._message, messages.ErrorResponse): raise errors.QueryError.from_error_response(self._message, self.operation) elif not isinstance(self._message, (messages.CopyInResponse, messages.LoadFile)): raise errors.MessageError('Unexpected COPY FROM LOCAL state: {}'.format( type(self._message).__name__)) try: if isinstance(self._message, messages.CopyInResponse): self._logger.info('Sending STDIN data to server') if len(self.copy_stdin_list) == 0: raise ValueError('No STDIN source to load. Please specify "copy_stdin" parameter in Cursor.execute()') stdin = self.copy_stdin_list.pop(0) self._send_copy_data(stdin, self.buffer_size) self.connection.write(messages.EndOfBatchRequest()) self._read_copy_data_response(is_stdin_copy=True) elif isinstance(self._message, messages.LoadFile): while True: self._send_copy_file_data() if not self._read_copy_data_response(): break except errors.QueryError: # A server-detected error. # The server issues an ErrorResponse message and a ReadyForQuery message. raise except Exception as e: # A client-detected error. # The client terminates COPY LOCAL protocol by sending a CopyError message, # which will cause the COPY SQL statement to fail with an ErrorResponse message. tb = sys.exc_info()[2] stk = traceback.extract_tb(tb, 1) self.connection.write(messages.CopyError(str(e), stk[0])) self.flush_to_query_ready() raise def _check_copy_local_files(self, input_files): # Return an empty list when the copy input is STDIN if len(input_files) == 0: return [] file_list = [] for file_pattern in input_files: if file_pattern not in self.operation: raise errors.MessageError('Server requests for loading invalid' ' file: {}, Query: {}'.format(file_pattern, self.operation)) # Expand the glob patterns expanded_files = glob.glob(file_pattern) if len(expanded_files) == 0: raise OSError('{} does not exist'.format(file_pattern)) # Check file permissions for f in expanded_files: os_utils.check_file_readable(f) file_list.append(f) # Return a non-empty list when the copy input is FILE # Note: Sending an empty list of files will make server kill the session. return file_list def _send_copy_data(self, stream, buffer_size) -> None: # Send zero or more CopyData messages, forming a stream of input data while True: chunk = stream.read(buffer_size) if not chunk: break self.connection.write(messages.CopyData(chunk, self.unicode_error)) def _send_copy_file_data(self) -> None: filename = self._message.filename self._logger.info('Sending {} data to server'.format(filename)) if filename not in self.valid_read_file_path: raise errors.MessageError('Server requests for loading invalid' ' file: {}'.format(filename)) with open(filename, "rb") as f: self._send_copy_data(f, self.buffer_size) self.connection.write(messages.EndOfBatchRequest()) def _read_copy_data_response(self, is_stdin_copy: bool = False) -> bool: """Returns True if the server wants us to load more data, False if we are done.""" self._message = self.connection.read_expected_message(END_OF_BATCH_RESPONSES) # Check for rejections during this load while isinstance(self._message, messages.WriteFile): if self._message.filename == '': self._logger.info('COPY-LOCAL rejected row numbers: {}'.format(self._message.rejected_rows)) elif self._message.filename in self.valid_write_file_path: self._message.write_to_disk(self.connection, self.buffer_size) else: raise errors.MessageError('Server requests for writing to' ' invalid file path: {}'.format(self._message.filename)) self._message = self.connection.read_expected_message(END_OF_BATCH_RESPONSES) # For STDIN copy, there will be no incoming message until we send # another EndOfBatchRequest or CopyDone if is_stdin_copy: self.connection.write(messages.CopyDone()) # End this copy self._message = self.connection.read_message() if isinstance(self._message, messages.ErrorResponse): raise errors.QueryError.from_error_response(self._message, self.operation) return False # For file copy, peek the next message self._message = self.connection.read_message() if isinstance(self._message, messages.LoadFile): # Indicate there are more local files to load return True elif isinstance(self._message, messages.ErrorResponse): raise errors.QueryError.from_error_response(self._message, self.operation) elif not isinstance(self._message, messages.CopyDoneResponse): raise errors.MessageError('Unexpected COPY FROM LOCAL state: {}'.format( type(self._message).__name__)) return False def _error_handler(self, msg: BackendMessage) -> NoReturn: self.connection.write(messages.Sync()) raise errors.QueryError.from_error_response(msg, self.operation) def _prepare(self, query: str) -> None: """ Send the query to be prepared to the server. The server will parse the query and return some metadata. """ self._logger.info(u'Prepare a statement: [{}]'.format(query)) # Send Parse message to server # We don't need to tell the server the parameter types yet self.connection.write(messages.Parse(self.prepared_name, query, param_types=())) # Send Describe message to server self.connection.write(messages.Describe('prepared_statement', self.prepared_name)) self.connection.write(messages.Flush()) # Read expected message: ParseComplete self._message = self.connection.read_expected_message(messages.ParseComplete, self._error_handler) # Read expected message: ParameterDescription self._message = self.connection.read_expected_message(messages.ParameterDescription, self._error_handler) self._param_metadata = self._message.parameters # Read expected message: RowDescription or NoData self._message = self.connection.read_expected_message( (messages.RowDescription, messages.NoData), self._error_handler) if isinstance(self._message, messages.NoData): self.description = None # response was NoData for a DDL/transaction PreparedStatement else: self.description = self._message.get_description() self._deserializers = self.get_deserializers() # Read expected message: CommandDescription self._message = self.connection.read_expected_message(messages.CommandDescription, self._error_handler) if len(self._message.command_tag) == 0: msg = 'The statement being prepared is empty' self._logger.error(msg) self.connection.write(messages.Sync()) raise errors.EmptyQueryError(msg) self._logger.info('Finish preparing the statement') def _execute_prepared_statement(self, list_of_parameter_values: Sequence[Any]) -> None: """ Send multiple statement parameter sets to the server using the extended query protocol. The server would bind and execute each set of parameter values. This function should not be called without first calling _prepare() to prepare a statement. """ portal_name = "" parameter_type_oids = [metadata['data_type_oid'] for metadata in self._param_metadata] parameter_count = len(self._param_metadata) try: if len(list_of_parameter_values) == 0: raise ValueError("Empty list/tuple, nothing to execute") for parameter_values in list_of_parameter_values: if parameter_values is None: parameter_values = () self._logger.info(u'Bind parameters: {}'.format(parameter_values)) if len(parameter_values) != parameter_count: msg = ("Invalid number of parameters for {}: {} given, {} expected" .format(parameter_values, len(parameter_values), parameter_count)) raise ValueError(msg) self.connection.write(messages.Bind(portal_name, self.prepared_name, parameter_values, parameter_type_oids, self.connection.options['binary_transfer'])) self.connection.write(messages.Execute(portal_name, 0)) self.connection.write(messages.Sync()) except Exception as e: self._logger.error(str(e)) # the server will not send anything until we issue a sync self.connection.write(messages.Sync()) self._message = self.connection.read_message() raise self.connection.write(messages.Flush()) # Read expected message: BindComplete self.connection.read_expected_message(messages.BindComplete) self._message = self.connection.read_message() if isinstance(self._message, messages.ErrorResponse): raise errors.QueryError.from_error_response(self._message, self.prepared_sql) def _close_prepared_statement(self) -> None: """ Close the prepared statement on the server. """ self.prepared_sql = None self.flush_to_query_ready() self.connection.write(messages.Close('prepared_statement', self.prepared_name)) self.connection.write(messages.Flush()) self._message = self.connection.read_expected_message(messages.CloseComplete) self.connection.write(messages.Sync()) vertica-python-1.4.0/vertica_python/vertica/deserializer.py000066400000000000000000000642441464740151100242340ustar00rootroot00000000000000# Copyright (c) 2022-2024 Open Text. # # 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. from __future__ import print_function, division, absolute_import, annotations import json import re from datetime import date, datetime, time, timedelta from dateutil import tz from dateutil.relativedelta import relativedelta from decimal import Context, Decimal from struct import unpack from uuid import UUID from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Any, Callable, Dict, List, Optional, Set, Union from ..vertica.column import Column from .. import errors from ..compat import as_str, as_bytes from ..datatypes import VerticaType from ..vertica.column import FormatCode class Deserializer(object): def get_row_deserializers(self, columns: List[Column], custom_converters: Dict[int, Callable[[bytes, Dict[str, Any]], Any]], context: Dict[str, Any]) -> List[Callable[[Optional[bytes]], Any]]: result = [None] * len(columns) for idx, col in enumerate(columns): result[idx] = self.get_column_deserializer(col, custom_converters, context) return result def get_column_deserializer(self, col: Column, custom_converters: Dict[int, Callable[[bytes, Dict[str, Any]], Any]], context: Dict[str, Any]) -> Callable[[Optional[bytes]], Any]: """Return a function that inputs a column's raw data and returns a Python object.""" if col.type_code in custom_converters: f = custom_converters[col.type_code] else: f = DEFAULTS.get(col.format_code, {}).get(col.type_code) if f is None: # skip conversion return lambda data: data def deserializer(data: Optional[bytes]): if data is None: # null return None return f(data, ctx={'column': col, **context}) return deserializer YEAR_TO_MONTH_RE = re.compile(r"(-)?(\d+)-(\d+)") TIMETZ_RE = re.compile( r"""(?ix) ^ (\d+) : (\d+) : (\d+) (?: \. (\d+) )? # Time and micros ([-+]) (\d+) (?: : (\d+) )? (?: : (\d+) )? # Timezone $ """ ) TZ_RE = re.compile(r"(?ix) ^([-+]) (\d+) (?: : (\d+) )? (?: : (\d+) )? $") SECONDS_PER_DAY = 86400 def load_bool_binary(val: bytes, ctx: Dict[str, Any]) -> bool: """ Parses binary representation of a BOOLEAN type. :param val: a byte - b'\x01' for True, b'\x00' for False :param ctx: dict :return: an instance of bool """ return val == b'\x01' def load_int8_binary(val: bytes, ctx: Dict[str, Any]) -> int: """ Parses binary representation of a INTEGER type. :param val: bytes - a 64-bit integer. :param ctx: dict :return: int """ return unpack("!q", val)[0] def load_float8_binary(val: bytes, ctx: Dict[str, Any]) -> float: """ Parses binary representation of a FLOAT type. :param val: bytes - a float encoded in IEEE-754 format. :param ctx: dict :return: float """ return unpack("!d", val)[0] def load_numeric_binary(val: bytes, ctx: Dict[str, Any]) -> Decimal: """ Parses binary representation of a NUMERIC type. :param val: bytes :param ctx: dict :return: decimal.Decimal """ # N-byte signed integer represents the unscaled value of the numeric # N is roughly (precision//19+1)*8 unscaledVal = int.from_bytes(val, byteorder='big', signed=True) precision = ctx['column'].precision scale = ctx['column'].scale # The numeric value is (unscaledVal * 10^(-scale)) return Decimal(unscaledVal).scaleb(-scale, context=Context(prec=precision)) def load_varchar_text(val: bytes, ctx: Dict[str, Any]) -> str: """ Parses text/binary representation of a CHAR / VARCHAR / LONG VARCHAR type. :param val: bytes :param ctx: dict :return: str """ return val.decode('utf-8', ctx['unicode_error']) def load_date_text(val: bytes, ctx: Dict[str, Any]) -> date: """ Parses text representation of a DATE type. :param val: bytes :param ctx: dict :return: datetime.date :raises NotSupportedError when a date Before Christ is encountered """ s = as_str(val) if s.endswith(' BC'): raise errors.NotSupportedError('Dates Before Christ are not supported by datetime.date. Got: {0}'.format(s)) try: return date(*map(lambda x: int(x), s.split('-'))) except ValueError: raise errors.NotSupportedError('Dates after year 9999 are not supported by datetime.date. Got: {0}'.format(s)) def load_date_binary(val: bytes, ctx: Dict[str, Any]) -> date: """ Parses binary representation of a DATE type. :param val: bytes :param ctx: dict :return: datetime.date :raises NotSupportedError when a date Before Christ is encountered """ # 8-byte integer represents the Julian day number # https://en.wikipedia.org/wiki/Julian_day jdn = load_int8_binary(val, ctx) days = jdn - 1721426 + 1 # shift epoch to 0001-1-1 (J1721426) if days < date.min.toordinal(): raise errors.NotSupportedError('Dates Before Christ are not supported by datetime.date. Got: Julian day number {0}'.format(jdn)) elif days > date.max.toordinal(): raise errors.NotSupportedError('Dates after year 9999 are not supported by datetime.date. Got: Julian day number {0}'.format(jdn)) return date.fromordinal(days) def load_time_text(val: bytes, ctx: Dict[str, Any]) -> time: """ Parses text representation of a TIME type. :param val: bytes :param ctx: dict :return: datetime.time """ val = as_str(val) if len(val) == 8: return datetime.strptime(val, '%H:%M:%S').time() return datetime.strptime(val, '%H:%M:%S.%f').time() def load_time_binary(val: bytes, ctx: Dict[str, Any]) -> time: """ Parses binary representation of a TIME type. :param val: bytes :param ctx: dict :return: datetime.time """ # 8-byte integer represents the number of microseconds # since midnight in the UTC time zone. msecs = load_int8_binary(val, ctx) msecs, fraction = divmod(msecs, 1000000) msecs, second = divmod(msecs, 60) hour, minute = divmod(msecs, 60) try: return time(hour, minute, second, fraction) except ValueError: raise errors.NotSupportedError("Time not supported by datetime.time. Got: hour={}".format(hour)) def load_timetz_text(val: bytes, ctx: Dict[str, Any]) -> time: """ Parses text representation of a TIMETZ type. :param val: bytes :param ctx: dict :return: datetime.time """ s = as_str(val) m = TIMETZ_RE.match(s) if not m: raise errors.DataError("Cannot parse time with time zone '{}'".format(s)) hr, mi, sec, fr, sign, oh, om, os = m.groups() # Pad the fraction of second until it represents 6 digits us = 0 if fr: pad = 6 - len(fr) us = int(fr) * (10**pad) # Calculate timezone # Note: before python version 3.7 timezone offset is restricted to a whole number of minutes # tz.tzoffset() will round seconds in the offset to whole minutes tz_offset = 60 * 60 * int(oh) if om: tz_offset += 60 * int(om) if os: tz_offset += int(os) if sign == "-": tz_offset = -tz_offset return time(int(hr), int(mi), int(sec), us, tz.tzoffset(None, tz_offset)) def load_timetz_binary(val: bytes, ctx: Dict[str, Any]) -> time: """ Parses binary representation of a TIMETZ type. :param val: bytes :param ctx: dict :return: datetime.time """ # 8-byte value where # - Upper 40 bits contain the number of microseconds since midnight in the UTC time zone. # - Lower 24 bits contain time zone as the UTC offset in seconds. v = load_int8_binary(val, ctx) tz_offset = SECONDS_PER_DAY - (v & 0xffffff) # in seconds msecs = v >> 24 # shift to given time zone msecs += tz_offset * 1000000 msecs %= SECONDS_PER_DAY * 1000000 msecs, fraction = divmod(msecs, 1000000) msecs, second = divmod(msecs, 60) hour, minute = divmod(msecs, 60) return time(hour, minute, second, fraction, tz.tzoffset(None, tz_offset)) def load_timestamp_text(val: bytes, ctx: Dict[str, Any]) -> datetime: """ Parses text representation of a TIMESTAMP type. :param val: bytes :param ctx: dict :return: datetime.datetime """ s = as_str(val) if s.endswith(" BC"): raise errors.NotSupportedError('Timestamps Before Christ are not supported by datetime.datetime. Got: {0}'.format(s)) fmt = '%Y-%m-%d %H:%M:%S.%f' if '.' in s else '%Y-%m-%d %H:%M:%S' try: return datetime.strptime(s, fmt) except ValueError: raise errors.NotSupportedError('Timestamps after year 9999 are not supported by datetime.datetime. Got: {0}'.format(s)) def load_timestamp_binary(val: bytes, ctx: Dict[str, Any]) -> datetime: """ Parses binary representation of a TIMESTAMP type. :param val: bytes :param ctx: dict :return: datetime.datetime """ # 8-byte integer represents the number of microseconds since 2000-01-01 00:00:00. msecs = load_int8_binary(val, ctx) _datetime_epoch = datetime(2000, 1, 1) try: return _datetime_epoch + timedelta(microseconds=msecs) except OverflowError: if msecs < 0: raise errors.NotSupportedError('Timestamps Before Christ are not supported by datetime.datetime.') else: raise errors.NotSupportedError('Timestamps after year 9999 are not supported by datetime.datetime.') def load_timestamptz_text(val: bytes, ctx: Dict[str, Any]) -> datetime: """ Parses text representation of a TIMESTAMPTZ type. :param val: bytes :param ctx: dict :return: datetime.datetime """ s = as_str(val) if s.endswith(" BC"): raise errors.NotSupportedError('TimestampTzs Before Christ are not supported by datetime.datetime. Got: {0}'.format(s)) dt = s.split(' ') # split into date part and time part if len(dt) != 2: raise errors.DataError("Cannot parse TIMESTAMPTZ '{}'".format(s)) try: d = date(*map(lambda x: int(x), dt[0].split('-'))) except ValueError: # year might be over 9999 raise errors.NotSupportedError('TimestampTzs after year 9999 are not supported by datetime.datetime. Got: {0}'.format(s)) t = load_timetz_text(dt[1], ctx) return datetime.combine(d, t) def load_timestamptz_binary(val: bytes, ctx: Dict[str, Any]) -> datetime: """ Parses binary representation of a TIMESTAMPTZ type. :param val: bytes :param ctx: dict :return: datetime.datetime """ # 8-byte integer represents the number of microseconds since 2000-01-01 00:00:00 in the UTC timezone. msecs = load_int8_binary(val, ctx) _datetimetz_epoch = datetime(2000, 1, 1, tzinfo=tz.tzutc()) # Process session time zone setting if TZ_RE.match(ctx['session_tz']): # -HH:MM / +HH:MM ctx['session_tz'] = 'UTC' + ctx['session_tz'] session_tz = tz.gettz(ctx['session_tz']) # Use local time zone if session time zone is unknown timezone = session_tz if session_tz else tz.gettz() try: ts = _datetimetz_epoch + timedelta(microseconds=msecs) return ts.astimezone(timezone) except OverflowError: if msecs < 0: raise errors.NotSupportedError('TimestampTzs Before Christ are not supported by datetime.datetime.') else: # year might be over 9999 raise errors.NotSupportedError('TimestampTzs after year 9999 are not supported by datetime.datetime.') def load_interval_text(val: bytes, ctx: Dict[str, Any]) -> relativedelta: """ Parses text representation of a INTERVAL day-time type. :param val: bytes :param ctx: dict :return: dateutil.relativedelta.relativedelta """ # [-]dd hh:mm:ss.ffffff interval = as_str(val) sign = -1 if interval[0] == '-' else 1 parts = [0] * 5 # value of [day, hour, minute, second, fraction] sp = interval.split('.') if len(sp) > 1: # Extract the fractional second part fraction = sp[1] pad = 6 - len(fraction) # pad the fraction until it represents 6 digits parts[4] = sign * int(fraction) * (10**pad) interval = sp[0] # Extract the first number idx = 0 while idx < len(interval) and interval[idx] not in (' ', ':'): idx += 1 num = int(interval[:idx]) saw_days = idx < len(interval) and interval[idx] == ' ' idx += 1 # Determine the unit for the first number type_name = ctx['column'].type_name parts_idx = 0 # Interval Day if type_name in ('Interval Day to Hour', 'Interval Day to Minute', 'Interval Day to Second'): parts_idx = 0 if (saw_days or idx > len(interval)) else 1 elif type_name in ('Interval Hour', 'Interval Hour to Minute', 'Interval Hour to Second'): parts_idx = 1 elif type_name in ('Interval Minute', 'Interval Minute to Second'): parts_idx = 2 elif type_name == 'Interval Second': parts_idx = 3 parts[parts_idx] = num # Save the first number if idx < len(interval): # Parse the rest of interval parts_idx += 1 ts = interval[idx:].split(':') for val in ts: parts[parts_idx] = sign * int(val) parts_idx += 1 return relativedelta(days=parts[0], hours=parts[1], minutes=parts[2], seconds=parts[3], microseconds=parts[4]) def load_interval_binary(val: bytes, ctx: Dict[str, Any]) -> relativedelta: """ Parses binary representation of a INTERVAL day-time type. :param val: bytes :param ctx: dict :return: dateutil.relativedelta.relativedelta """ # 8-byte integer containing the number of microseconds in the interval msecs = load_int8_binary(val, ctx) return relativedelta(microseconds=msecs) def load_intervalYM_text(val: bytes, ctx: Dict[str, Any]) -> relativedelta: """ Parses text representation of a INTERVAL YEAR TO MONTH / INTERVAL YEAR / INTERVAL MONTH type. :param val: bytes :param ctx: dict :return: dateutil.relativedelta.relativedelta """ s = as_str(val) type_name = ctx['column'].type_name if type_name == 'Interval Year to Month': m = YEAR_TO_MONTH_RE.match(s) if not m: raise errors.DataError("Cannot parse interval '{}'".format(s)) sign, year, month = m.groups() sign = -1 if sign else 1 return relativedelta(years=sign*int(year), months=sign*int(month)) else: try: interval = int(s) except ValueError: raise errors.DataError("Cannot parse interval '{}'".format(s)) if type_name == 'Interval Year': return relativedelta(years=interval) else: # Interval Month return relativedelta(months=interval) def load_intervalYM_binary(val: bytes, ctx: Dict[str, Any]) -> relativedelta: """ Parses binary representation of a INTERVAL YEAR TO MONTH / INTERVAL YEAR / INTERVAL MONTH type. :param val: bytes :param ctx: dict :return: dateutil.relativedelta.relativedelta """ # 8-byte integer containing the number of months in the interval months = load_int8_binary(val, ctx) return relativedelta(months=months) def load_uuid_binary(val: bytes, ctx: Dict[str, Any]) -> UUID: """ Parses binary representation of a UUID type. :param val: bytes :param ctx: dict :return: uuid.UUID """ # 16-byte value in big-endian order interpreted as UUID return UUID(bytes=bytes(val)) def load_varbinary_text(s: bytes, ctx: Dict[str, Any]) -> bytes: """ Parses text representation of a BINARY / VARBINARY / LONG VARBINARY type. :param s: bytes :param ctx: dict :return: bytes """ s = as_bytes(s) buf = [] i = 0 while i < len(s): c = s[i: i+1] if c == b'\\': c2 = s[i+1: i+2] if c2 == b'\\': # escaped \ i += 2 else: # A \xxx octal string c = bytes([int(s[i+1: i+4], 8)]) i += 4 else: i += 1 buf.append(c) return b''.join(buf) def load_array_text(val: bytes, ctx: Dict[str, Any]) -> Union[str, List[Any]]: """ Parses text/binary representation of an ARRAY type. :param val: bytes :param ctx: dict :return: list """ v = val.decode('utf-8', ctx['unicode_error']) # Some old servers have a bug of sending ARRAY oid without child metadata if not ctx['complex_types_enabled']: return v json_data = json.loads(v) return parse_array(json_data, ctx) def load_set_text(val: bytes, ctx: Dict[str, Any]) -> Set[Any]: """ Parses text/binary representation of a SET type. :param val: bytes :param ctx: dict :return: set """ return set(load_array_text(val, ctx)) def parse_array(json_data: List[Any], ctx: Dict[str, Any]) -> List[Any]: if not isinstance(json_data, list): raise TypeError('Expected a list, got {}'.format(json_data)) # An array has only one child, all elements in the array are the same type. child_ctx = ctx.copy() child_ctx['column'] = ctx['column'].child_columns[0] # Shortcut: return data parsed by the default JSONDecoder if child_ctx['column'].type_code in (VerticaType.BOOL, VerticaType.INT8, VerticaType.CHAR, VerticaType.VARCHAR, VerticaType.LONGVARCHAR): return json_data parsed_array = [None] * len(json_data) for idx, element in enumerate(json_data): if element is None: continue parsed_array[idx] = parse_json_element(element, child_ctx) return parsed_array def load_row_text(val: bytes, ctx: Dict[str, Any]) -> Union[str, Dict[str, Any]]: """ Parses text/binary representation of a ROW type. :param val: bytes :param ctx: dict :return: dict """ v = val.decode('utf-8', ctx['unicode_error']) # Some old servers have a bug of sending ROW oid without child metadata if not ctx['complex_types_enabled']: return v json_data = json.loads(v) return parse_row(json_data, ctx) def parse_row(json_data: Dict[str, Any], ctx: Dict[str, Any]) -> Dict[str, Any]: if not isinstance(json_data, dict): raise TypeError('Expected a dict, got {}'.format(json_data)) # A row has one or more child fields child_columns = ctx['column'].child_columns if child_columns is None: # Special case: SELECT ROW(); return json_data if len(json_data) != len(child_columns): # This situation should never occur raise ValueError('The metadata does not match the fields in the ROW.') parsed_row = {} for child_column in child_columns: key = child_column.name element = json_data[key] if element is None: parsed_row[key] = None continue child_ctx = ctx.copy() child_ctx['column'] = child_column parsed_row[key] = parse_json_element(element, child_ctx) return parsed_row def parse_json_element(element: Any, ctx: Dict[str, Any]) -> Any: type_code = ctx['column'].type_code if type_code in (VerticaType.BOOL, VerticaType.INT8, VerticaType.CHAR, VerticaType.VARCHAR, VerticaType.LONGVARCHAR): return element # "-Infinity", "Infinity", "NaN" if type_code == VerticaType.FLOAT8: return float(element) # element type: str if type_code in (VerticaType.DATE, VerticaType.TIME, VerticaType.TIMETZ, VerticaType.TIMESTAMP, VerticaType.TIMESTAMPTZ, VerticaType.INTERVAL, VerticaType.INTERVALYM, VerticaType.BINARY, VerticaType.VARBINARY, VerticaType.LONGVARBINARY): return DEFAULTS[FormatCode.TEXT][type_code](element, ctx) elif type_code == VerticaType.NUMERIC: return Decimal(element) elif type_code == VerticaType.UUID: return UUID(element) # element type: list elif type_code == VerticaType.ARRAY: return parse_array(element, ctx) # element type: dict elif type_code == VerticaType.ROW: return parse_row(element, ctx) return element DEFAULTS = { FormatCode.TEXT: { VerticaType.UNKNOWN: None, VerticaType.BOOL: lambda val, ctx: val == b't', VerticaType.INT8: lambda val, ctx: int(val), VerticaType.FLOAT8: lambda val, ctx: float(val), VerticaType.NUMERIC: lambda val, ctx: Decimal(val.decode('utf-8')), VerticaType.CHAR: load_varchar_text, VerticaType.VARCHAR: load_varchar_text, VerticaType.LONGVARCHAR: load_varchar_text, VerticaType.DATE: load_date_text, VerticaType.TIME: load_time_text, VerticaType.TIMETZ: load_timetz_text, VerticaType.TIMESTAMP: load_timestamp_text, VerticaType.TIMESTAMPTZ: load_timestamptz_text, VerticaType.INTERVAL: load_interval_text, VerticaType.INTERVALYM: load_intervalYM_text, VerticaType.UUID: lambda val, ctx: UUID(val.decode('utf-8')), VerticaType.BINARY: load_varbinary_text, VerticaType.VARBINARY: load_varbinary_text, VerticaType.LONGVARBINARY: load_varbinary_text, VerticaType.ARRAY: load_array_text, VerticaType.ARRAY1D_BOOL: load_array_text, VerticaType.ARRAY1D_INT8: load_array_text, VerticaType.ARRAY1D_FLOAT8: load_array_text, VerticaType.ARRAY1D_NUMERIC: load_array_text, VerticaType.ARRAY1D_CHAR: load_array_text, VerticaType.ARRAY1D_VARCHAR: load_array_text, VerticaType.ARRAY1D_LONGVARCHAR: load_array_text, VerticaType.ARRAY1D_DATE: load_array_text, VerticaType.ARRAY1D_TIME: load_array_text, VerticaType.ARRAY1D_TIMETZ: load_array_text, VerticaType.ARRAY1D_TIMESTAMP: load_array_text, VerticaType.ARRAY1D_TIMESTAMPTZ: load_array_text, VerticaType.ARRAY1D_INTERVAL: load_array_text, VerticaType.ARRAY1D_INTERVALYM: load_array_text, VerticaType.ARRAY1D_UUID: load_array_text, VerticaType.ARRAY1D_BINARY: load_array_text, VerticaType.ARRAY1D_VARBINARY: load_array_text, VerticaType.ARRAY1D_LONGVARBINARY: load_array_text, VerticaType.SET_BOOL: load_set_text, VerticaType.SET_INT8: load_set_text, VerticaType.SET_FLOAT8: load_set_text, VerticaType.SET_CHAR: load_set_text, VerticaType.SET_VARCHAR: load_set_text, VerticaType.SET_DATE: load_set_text, VerticaType.SET_TIME: load_set_text, VerticaType.SET_TIMESTAMP: load_set_text, VerticaType.SET_TIMESTAMPTZ: load_set_text, VerticaType.SET_TIMETZ: load_set_text, VerticaType.SET_INTERVAL: load_set_text, VerticaType.SET_INTERVALYM: load_set_text, VerticaType.SET_NUMERIC: load_set_text, VerticaType.SET_VARBINARY: load_set_text, VerticaType.SET_UUID: load_set_text, VerticaType.SET_BINARY: load_set_text, VerticaType.SET_LONGVARCHAR: load_set_text, VerticaType.SET_LONGVARBINARY: load_set_text, VerticaType.ROW: load_row_text, VerticaType.MAP: load_row_text, }, FormatCode.BINARY: { VerticaType.UNKNOWN: None, VerticaType.BOOL: load_bool_binary, VerticaType.INT8: load_int8_binary, VerticaType.FLOAT8: load_float8_binary, VerticaType.NUMERIC: load_numeric_binary, VerticaType.CHAR: load_varchar_text, VerticaType.VARCHAR: load_varchar_text, VerticaType.LONGVARCHAR: load_varchar_text, VerticaType.DATE: load_date_binary, VerticaType.TIME: load_time_binary, VerticaType.TIMETZ: load_timetz_binary, VerticaType.TIMESTAMP: load_timestamp_binary, VerticaType.TIMESTAMPTZ: load_timestamptz_binary, VerticaType.INTERVAL: load_interval_binary, VerticaType.INTERVALYM: load_intervalYM_binary, VerticaType.UUID: load_uuid_binary, VerticaType.BINARY: None, VerticaType.VARBINARY: None, VerticaType.LONGVARBINARY: None, VerticaType.ARRAY: load_array_text, VerticaType.ARRAY1D_BOOL: load_array_text, VerticaType.ARRAY1D_INT8: load_array_text, VerticaType.ARRAY1D_FLOAT8: load_array_text, VerticaType.ARRAY1D_NUMERIC: load_array_text, VerticaType.ARRAY1D_CHAR: load_array_text, VerticaType.ARRAY1D_VARCHAR: load_array_text, VerticaType.ARRAY1D_LONGVARCHAR: load_array_text, VerticaType.ARRAY1D_DATE: load_array_text, VerticaType.ARRAY1D_TIME: load_array_text, VerticaType.ARRAY1D_TIMETZ: load_array_text, VerticaType.ARRAY1D_TIMESTAMP: load_array_text, VerticaType.ARRAY1D_TIMESTAMPTZ: load_array_text, VerticaType.ARRAY1D_INTERVAL: load_array_text, VerticaType.ARRAY1D_INTERVALYM: load_array_text, VerticaType.ARRAY1D_UUID: load_array_text, VerticaType.ARRAY1D_BINARY: load_array_text, VerticaType.ARRAY1D_VARBINARY: load_array_text, VerticaType.ARRAY1D_LONGVARBINARY: load_array_text, VerticaType.SET_BOOL: load_set_text, VerticaType.SET_INT8: load_set_text, VerticaType.SET_FLOAT8: load_set_text, VerticaType.SET_CHAR: load_set_text, VerticaType.SET_VARCHAR: load_set_text, VerticaType.SET_DATE: load_set_text, VerticaType.SET_TIME: load_set_text, VerticaType.SET_TIMESTAMP: load_set_text, VerticaType.SET_TIMESTAMPTZ: load_set_text, VerticaType.SET_TIMETZ: load_set_text, VerticaType.SET_INTERVAL: load_set_text, VerticaType.SET_INTERVALYM: load_set_text, VerticaType.SET_NUMERIC: load_set_text, VerticaType.SET_VARBINARY: load_set_text, VerticaType.SET_UUID: load_set_text, VerticaType.SET_BINARY: load_set_text, VerticaType.SET_LONGVARCHAR: load_set_text, VerticaType.SET_LONGVARBINARY: load_set_text, VerticaType.ROW: load_row_text, VerticaType.MAP: load_row_text, }, } vertica-python-1.4.0/vertica_python/vertica/log.py000066400000000000000000000053041464740151100223230ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations import logging from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Union from ..os_utils import ensure_dir_exists class VerticaLogging(object): @classmethod def setup_logging(cls, logger_name: str, logfile: str, log_level: Union[int, str] = logging.INFO, context: str = '') -> None: logger = logging.getLogger(logger_name) logger.setLevel(log_level) if logfile: 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') ensure_dir_exists(logfile) file_handler = logging.FileHandler(logfile, encoding='utf-8') file_handler.setFormatter(formatter) logger.addHandler(file_handler) vertica-python-1.4.0/vertica_python/vertica/messages/000077500000000000000000000000001464740151100227755ustar00rootroot00000000000000vertica-python-1.4.0/vertica_python/vertica/messages/__init__.py000066400000000000000000000037771464740151100251240ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from ..messages import backend_messages from ..messages.backend_messages import * from ..messages import frontend_messages from ..messages.frontend_messages import * __all__ = backend_messages.__all__ + frontend_messages.__all__ vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/000077500000000000000000000000001464740151100262535ustar00rootroot00000000000000vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/__init__.py000066400000000000000000000065041464740151100303710ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from .authentication import Authentication from .backend_key_data import BackendKeyData from .bind_complete import BindComplete from .close_complete import CloseComplete from .command_complete import CommandComplete from .command_description import CommandDescription from .copy_done_response import CopyDoneResponse from .copy_in_response import CopyInResponse from .data_row import DataRow from .empty_query_response import EmptyQueryResponse from .end_of_batch_response import EndOfBatchResponse from .error_response import ErrorResponse from .load_balance_response import LoadBalanceResponse from .load_file import LoadFile from .no_data import NoData from .notice_response import NoticeResponse from .parameter_description import ParameterDescription from .parameter_status import ParameterStatus from .parse_complete import ParseComplete from .portal_suspended import PortalSuspended from .ready_for_query import ReadyForQuery from .row_description import RowDescription from .unknown import Unknown from .verify_files import VerifyFiles from .write_file import WriteFile __all__ = ['Authentication', 'BackendKeyData', 'BindComplete', 'CloseComplete', 'CommandComplete', 'CommandDescription', 'CopyDoneResponse', 'CopyInResponse', 'DataRow', 'EmptyQueryResponse', 'EndOfBatchResponse', 'ErrorResponse', 'LoadBalanceResponse', 'LoadFile', 'NoData', 'NoticeResponse', 'ParameterDescription', 'ParameterStatus', 'ParseComplete', 'PortalSuspended', 'ReadyForQuery', 'RowDescription', 'Unknown', 'VerifyFiles', 'WriteFile'] vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/authentication.py000066400000000000000000000063201464740151100316450ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from struct import unpack from ..message import BackendMessage from .... import errors class Authentication(BackendMessage): message_id = b'R' OK = 0 KERBEROS_V4 = 1 KERBEROS_V5 = 2 CLEARTEXT_PASSWORD = 3 CRYPT_PASSWORD = 4 # obsolete MD5_PASSWORD = 5 SCM_CREDENTIAL = 6 GSS = 7 GSS_CONTINUE = 8 CHANGE_PASSWORD = 9 PASSWORD_CHANGED = 10 # client doesn't do password changing, this should never be seen PASSWORD_GRACE = 11 OAUTH = 12 HASH = 65536 HASH_MD5 = 65536 + 5 HASH_SHA512 = 65536 + 512 def __init__(self, data): BackendMessage.__init__(self) self.code, other = unpack('!I{0}s'.format(len(data) - 4), data) if self.code == self.CRYPT_PASSWORD: self.salt = other elif self.code in (self.MD5_PASSWORD, self.HASH_MD5): self.salt = other[:4] elif self.code in (self.HASH, self.HASH_SHA512): self.salt = other[:4] userSaltLen = unpack('!I', other[4:8])[0] if userSaltLen != 16: raise errors.MessageError( 'Received wrong user salt size: {}'.format(userSaltLen)) self.usersalt = unpack('!{0}s'.format(userSaltLen), other[8:])[0] elif self.code in [self.GSS_CONTINUE]: self.auth_data = other def __str__(self): return "Authentication: type={}".format(self.code) BackendMessage.register(Authentication) vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/backend_key_data.py000066400000000000000000000043201464740151100320540ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from struct import unpack from ..message import BackendMessage class BackendKeyData(BackendMessage): message_id = b'K' def __init__(self, data): BackendMessage.__init__(self) unpacked = unpack('!2I', data) self.pid = unpacked[0] self.key = unpacked[1] def __str__(self): return "BackendKeyData: pid = {}, key = {}".format(self.pid, self.key) BackendMessage.register(BackendKeyData) vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/bind_complete.py000066400000000000000000000037451464740151100314420ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from ..message import BackendMessage class BindComplete(BackendMessage): message_id = b'2' def __init__(self, data): BackendMessage.__init__(self) BackendMessage.register(BindComplete) vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/close_complete.py000066400000000000000000000037471464740151100316350ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from ..message import BackendMessage class CloseComplete(BackendMessage): message_id = b'3' def __init__(self, data): BackendMessage.__init__(self) BackendMessage.register(CloseComplete) vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/command_complete.py000066400000000000000000000053621464740151100321410ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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. """ CommandComplete message The server prompt that indicates a command has completed. The command tag string is the name of the command that was run. """ from __future__ import print_function, division, absolute_import, annotations import re import warnings from struct import unpack from ..message import BackendMessage class CommandComplete(BackendMessage): message_id = b'C' def __init__(self, data): BackendMessage.__init__(self) data = unpack('{0}sx'.format(len(data) - 1), data)[0] try: self.command_tag = data.decode('utf-8') except Exception as e: # VER-86494 warnings.warn( f"\n{'-'*70}\n" "Hit a known server bug (#493). To fix it,\n" "please upgrade your server to 12.0.4-3 or higher version.\n" f"{'-'*70}\n") self.command_tag = 'x' def __str__(self): return 'CommandComplete: command_tag = "{}"'.format(self.command_tag) BackendMessage.register(CommandComplete) vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/command_description.py000066400000000000000000000056601464740151100326550ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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. """ CommandDescription message -- part of the response to a Describe request message. This response informs the client about the type of command being executed. If the command is a parameterized INSERT statement, the copy_rewrite field may include a semantically-equivalent COPY STDIN statement. Clients can choose to run this statement instead to achieve better performance when loading many batches of parameters. """ from __future__ import print_function, division, absolute_import, annotations from struct import unpack from ..message import BackendMessage class CommandDescription(BackendMessage): message_id = b'm' def __init__(self, data): BackendMessage.__init__(self) pos = data.find(b'\x00') unpacked = unpack("!{0}sxH{1}sx".format(pos, len(data) - pos - 4), data) self.command_tag = unpacked[0].decode('utf-8') self.has_copy_rewrite = (unpacked[1] == 1) self.copy_rewrite = unpacked[2].decode('utf-8') def __str__(self): return ('CommandDescription: command_tag = "{}", has_copy_rewrite = {},' ' copy_rewrite = "{}"'.format( self.command_tag, self.has_copy_rewrite, self.copy_rewrite)) BackendMessage.register(CommandDescription) vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/copy_done_response.py000066400000000000000000000015551464740151100325300ustar00rootroot00000000000000# Copyright (c) 2020-2024 Open Text. # # 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. from __future__ import print_function, division, absolute_import, annotations from ..message import BackendMessage class CopyDoneResponse(BackendMessage): message_id = b'c' def __init__(self, data): BackendMessage.__init__(self) BackendMessage.register(CopyDoneResponse) vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/copy_in_response.py000066400000000000000000000042231464740151100322040ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from struct import unpack from ..message import BackendMessage class CopyInResponse(BackendMessage): message_id = b'G' def __init__(self, data): BackendMessage.__init__(self) values = unpack('!B{0}H'.format((len(data) - 1) // 2), data) self.format = values[0] self.column_formats = values[2::] BackendMessage.register(CopyInResponse) vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/data_row.py000066400000000000000000000045101464740151100304250ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from struct import unpack_from from ..message import BackendMessage class DataRow(BackendMessage): message_id = b'D' def __init__(self, data): BackendMessage.__init__(self) field_count = unpack_from('!H', data, 0)[0] self.values = [None] * field_count pos = 2 for i in range(field_count): size = unpack_from('!i', data, pos)[0] pos += 4 if size != -1: self.values[i] = data[pos : pos + size] pos += size BackendMessage.register(DataRow) vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/empty_query_response.py000066400000000000000000000037611464740151100331350ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from ..message import BackendMessage class EmptyQueryResponse(BackendMessage): message_id = b'I' def __init__(self, data): BackendMessage.__init__(self) BackendMessage.register(EmptyQueryResponse) vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/end_of_batch_response.py000066400000000000000000000015611464740151100331410ustar00rootroot00000000000000# Copyright (c) 2020-2024 Open Text. # # 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. from __future__ import print_function, division, absolute_import, annotations from ..message import BackendMessage class EndOfBatchResponse(BackendMessage): message_id = b'J' def __init__(self, data): BackendMessage.__init__(self) BackendMessage.register(EndOfBatchResponse) vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/error_response.py000066400000000000000000000041461464740151100317010ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from ..message import BackendMessage from vertica_python.vertica.messages.backend_messages.notice_response import NoticeResponse class ErrorResponse(NoticeResponse, BackendMessage): message_id = b'E' def __str__(self): return "ErrorResponse: {}".format(self.error_message()) BackendMessage.register(ErrorResponse) vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/load_balance_response.py000066400000000000000000000045741464740151100331410ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations 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].decode('utf-8') def get_port(self) -> int: return self.port def get_host(self) -> str: return self.host def __str__(self): return "LoadBalanceResponse: host={}, port={}".format(self.host, self.port) BackendMessage.register(LoadBalanceResponse) vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/load_file.py000066400000000000000000000021041464740151100305400ustar00rootroot00000000000000# Copyright (c) 2020-2024 Open Text. # # 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. from __future__ import print_function, division, absolute_import, annotations from struct import unpack from ..message import BackendMessage class LoadFile(BackendMessage): message_id = b'H' def __init__(self, data): BackendMessage.__init__(self) unpacked = unpack('!{0}sx'.format(data.find(b'\x00')), data) self.filename = unpacked[0].decode('utf-8') def __str__(self): return "LoadFile: name = {}".format(self.filename) BackendMessage.register(LoadFile) vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/no_data.py000066400000000000000000000037311464740151100302360ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from ..message import BackendMessage class NoData(BackendMessage): message_id = b'n' def __init__(self, data): BackendMessage.__init__(self) BackendMessage.register(NoData) vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/notice_response.py000066400000000000000000000074631464740151100320360ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from struct import unpack_from from ...mixins.notice_response_attr import _NoticeResponseAttrMixin from ..message import BackendMessage class NoticeResponse(_NoticeResponseAttrMixin, BackendMessage): message_id = b'N' def __init__(self, data): BackendMessage.__init__(self) # `_notice_attrs` is required by _NoticeResponseAttrMixin and also used # by QueryError self._notice_attrs = NoticeResponse._unpack_data(data) def error_message(self): return ', '.join([ "{0}: {1}".format(name, value) for (name, value) in self.values.items() ]) def __str__(self): return "NoticeResponse: {}".format(self.error_message()) @property def values(self): """ A mapping of server-provided values describing this notice. The keys of this mapping are user-facing strings. The contents of any given NoticeResponse can vary based on the context or version of Vertica. For access to specific values, the appropriate property getter is recommended. Example return value: ``` { 'Severity': 'ERROR', 'Message': 'Syntax error at or near "foobar"', 'Sqlstate': '42601', 'Position': '1', 'Routine': 'base_yyerror', 'File': '/data/.../vertica/Parser/scan.l', 'Line': '1043', 'Error Code': '4856' } ``` """ return self._get_labeled_values() @staticmethod def _unpack_data(data): data_mapping = {} pos = 0 while pos < len(data) - 1: null_byte = data.find(b'\x00', pos) unpacked = unpack_from('c{0}sx'.format(null_byte - 1 - pos), data, pos) key = unpacked[0] value = unpacked[1] data_mapping[key] = value.decode('utf-8', 'backslashreplace') pos += (len(value) + 2) return data_mapping BackendMessage.register(NoticeResponse) vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/parameter_description.py000066400000000000000000000067431464740151100332220ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from struct import unpack, unpack_from, calcsize from ..message import BackendMessage from ....datatypes import getTypeName class ParameterDescription(BackendMessage): message_id = b't' def __init__(self, data): BackendMessage.__init__(self) self.parameters = [] self.parameter_count = unpack('!H', data[0:2])[0] if self.parameter_count == 0: return # read type pool # used for special types e.g. GEOMETRY, GEOGRAPHY user_types = [] type_pool_count = unpack('!I', data[2:6])[0] pos = 6 for _ in range(type_pool_count): base_type_oid = unpack('!I', data[pos:(pos + 4)])[0] pos += 4 type_name = unpack_from("!{0}sx".format(data.find(b'\x00', pos) - pos), data, pos)[0] pos += len(type_name) + 1 user_types.append((base_type_oid, type_name)) # read info of each parameter offset = calcsize("!BIiH") for _ in range(self.parameter_count): field_info = unpack_from("!BIiH", data, pos) pos += offset if field_info[0] == 1: data_type_oid, data_type_name = user_types[field_info[1]] else: data_type_oid = field_info[1] data_type_name = getTypeName(data_type_oid, field_info[2]) self.parameters.append({ 'data_type_oid': data_type_oid, 'data_type_name': data_type_name, 'type_modifier': field_info[2], 'null_ok': field_info[3] != 1, }) def __str__(self): return "ParameterDescription: {}".format(self.parameters) BackendMessage.register(ParameterDescription) vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/parameter_status.py000066400000000000000000000057351464740151100322220ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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. """ ParameterStatus message A ParameterStatus message will be generated whenever the backend believes the frontend should know about a setting parameter value. For example, when you do SET SESSION AUTOCOMMIT ON | OFF, you get back a parameter status telling you the new value of autocommit. At present Vertica supports a handful of parameters, they are: standard_conforming_strings, server_version, client_locale, client_label, long_string_types, protocol_version, auto_commit, MARS More parameters would be added in the future. Accordingly, a frontend should simply ignore ParameterStatus for parameters that it does not understand or care about. """ from __future__ import print_function, division, absolute_import, annotations from struct import unpack from ..message import BackendMessage class ParameterStatus(BackendMessage): message_id = b'S' def __init__(self, data): BackendMessage.__init__(self) null_byte = data.find(b'\x00') unpacked = unpack('{0}sx{1}sx'.format(null_byte, len(data) - null_byte - 2), data) self.name = unpacked[0].decode('utf-8') self.value = unpacked[1].decode('utf-8') def __str__(self): return "ParameterStatus: {} = {}".format(self.name, self.value) BackendMessage.register(ParameterStatus) vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/parse_complete.py000066400000000000000000000037471464740151100316420ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from ..message import BackendMessage class ParseComplete(BackendMessage): message_id = b'1' def __init__(self, data): BackendMessage.__init__(self) BackendMessage.register(ParseComplete) vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/portal_suspended.py000066400000000000000000000051721464740151100322050ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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. """ PortalSuspended message A PortalSuspended message indicates that a portal has stopped execution. Vertica does not support portals in the same way postgres does. A portal is never truly "suspended" because Vertica always returns all results, regardless of how many were requested in a Bind message. This effectively means PortalSuspended has the same meaning as a CommandComplete message. The only meaningful difference being PortalSuspended occurs during the extended query protocol, while CommandComplete happens with the simple query protocol. In the future, Vertica may change to restore semantics more similar to those intended by Postgres. """ from __future__ import print_function, division, absolute_import, annotations from ..message import BackendMessage class PortalSuspended(BackendMessage): message_id = b's' def __init__(self, data): BackendMessage.__init__(self) BackendMessage.register(PortalSuspended) vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/ready_for_query.py000066400000000000000000000047571464740151100320410ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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. """ ReadyForQuery message The backend informs the frontend that it may safely send a new command. The ReadyForQuery message is the same one that the backend will issue after each command cycle. """ from __future__ import print_function, division, absolute_import, annotations from struct import unpack from ..message import BackendMessage class ReadyForQuery(BackendMessage): message_id = b'Z' STATUSES = { b'I': 'no_transaction', b'T': 'in_transaction', b'E': 'failed_transaction' } def __init__(self, data): BackendMessage.__init__(self) self.transaction_status = self.STATUSES[unpack('c', data)[0]] def __str__(self): return "ReadyForQuery: status = {}".format(self.transaction_status) BackendMessage.register(ReadyForQuery) vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/row_description.py000066400000000000000000000147401464740151100320450ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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. """ RowDescription message RowDescription message describes the column layout of the rows that will be returned in response to a SELECT, FETCH, etc query. """ from __future__ import print_function, division, absolute_import, annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import List from struct import unpack, unpack_from, calcsize from ..message import BackendMessage from ...column import Column from ....datatypes import getTypeName, getComplexElementType class RowDescription(BackendMessage): message_id = b'T' def __init__(self, data, complex_types_enabled): BackendMessage.__init__(self) self.fields = [] field_dict = {} field_count = unpack('!H', data[0:2])[0] if field_count == 0: return # read type mapping pool # used for user-defined types e.g. GEOMETRY, GEOGRAPHY user_types = [] type_pool_count = unpack('!I', data[2:6])[0] pos = 6 for _ in range(type_pool_count): base_type_oid = unpack('!I', data[pos:(pos + 4)])[0] pos += 4 type_name = unpack_from("!{0}sx".format(data.find(b'\x00', pos) - pos), data, pos)[0] pos += len(type_name) + 1 user_types.append((base_type_oid, type_name.decode('utf-8'))) # read info of each field offset = calcsize("!BIHHHiH") for _ in range(field_count): field_name = unpack_from("!{0}sx".format(data.find(b'\x00', pos) - pos), data, pos)[0] pos += len(field_name) + 1 field_name = field_name.decode('utf-8') table_oid = unpack('!Q', data[pos:(pos + 8)])[0] pos += 8 schema_name, table_name = None, None if table_oid != 0: schema_name = unpack_from("!{0}sx".format(data.find(b'\x00', pos) - pos), data, pos)[0] pos += len(schema_name) + 1 schema_name = schema_name.decode('utf-8') table_name = unpack_from("!{0}sx".format(data.find(b'\x00', pos) - pos), data, pos)[0] pos += len(table_name) + 1 table_name = table_name.decode('utf-8') attribute_number = unpack_from("!H", data, pos)[0] pos += 2 if complex_types_enabled: parent_attribute_number = unpack_from("!H", data, pos)[0] pos += 2 else: parent_attribute_number = 0 field_info = unpack_from("!BIhHHiH", data, pos) pos += offset if field_info[0] == 1: # An user-defined type data_type_oid, data_type_name = user_types[field_info[1]] else: # A primitive type data_type_oid = field_info[1] data_type_name = getTypeName(data_type_oid, field_info[5]) # Create a Column object metadata = { 'name': field_name, 'table_oid': table_oid, 'schema_name': schema_name, 'table_name': table_name, 'attribute_number': attribute_number, 'data_type_oid': data_type_oid, 'data_type_size': field_info[2], 'data_type_name': data_type_name, 'null_ok': field_info[3] == 1, 'is_identity': field_info[4] == 1, 'type_modifier': field_info[5], 'format_code': field_info[6], } column = Column(metadata) # Add every column description to the dict so we can set the parents later field_dict[(table_oid, attribute_number)] = column if parent_attribute_number == 0: self.fields.append(column) else: parent_col = field_dict.get((table_oid, parent_attribute_number)) if not parent_col: raise KeyError("Complex type parent column not found: table_oid={}, attribute_number={}".format(table_oid, parent_attribute_number)) parent_col.add_child_column(column) element_type_oid = getComplexElementType(data_type_oid) if element_type_oid is not None: metadata['data_type_oid'] = element_type_oid metadata['name'] = 'elements' metadata['data_type_name'] = getTypeName(element_type_oid, field_info[5]) # elements may only be primitive types child_column = Column(metadata) column.add_child_column(child_column) def get_description(self) -> List[Column]: # return a list of Column objects for Cursor.description return self.fields def __str__(self): s = ",\n".join([c.debug_info() for c in self.fields]) return f"RowDescription: [\n{s}]" BackendMessage.register(RowDescription) vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/unknown.py000066400000000000000000000042711464740151100303300ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from ..message import BackendMessage class Unknown(BackendMessage): def __init__(self, message_id, data): BackendMessage.__init__(self) self._message_id = message_id self.data = data @property def message_id(self): return self._message_id def __str__(self): return 'Unknown: message_id = {}, data = {}'.format( self._message_id, repr(self.data)) vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/verify_files.py000066400000000000000000000040551464740151100313170ustar00rootroot00000000000000# Copyright (c) 2020-2024 Open Text. # # 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. """ VerifyFiles message VerifyFiles message is sent by the server when the client issues a COPY FROM LOCAL command. The server parses the file names out of the command, and sends them back to the client in this message. The client has to verify that these files exist and are readable before running the copy. """ from __future__ import print_function, division, absolute_import, annotations from struct import unpack_from from ..message import BackendMessage class VerifyFiles(BackendMessage): message_id = b'F' def __init__(self, data): BackendMessage.__init__(self) files_count = unpack_from('!H', data, 0)[0] self.input_files = [None] * files_count pos = 2 for i in range(files_count): filename = unpack_from("!{0}sx".format(data.find(b'\x00', pos) - pos), data, pos)[0] self.input_files[i] = filename.decode('utf-8') pos += len(filename) + 1 filename = unpack_from("!{0}sx".format(data.find(b'\x00', pos) - pos), data, pos)[0] self.rejections_file = filename.decode('utf-8') pos += len(filename) + 1 filename = unpack_from("!{0}sx".format(data.find(b'\x00', pos) - pos), data, pos)[0] self.exceptions_file = filename.decode('utf-8') def __str__(self): return "VerifyFiles: InputFiles = {}, RejectedDataFile = {}, ExceptionsFile = {}".format( self.input_files, self.rejections_file, self.exceptions_file) BackendMessage.register(VerifyFiles) vertica-python-1.4.0/vertica_python/vertica/messages/backend_messages/write_file.py000066400000000000000000000043421464740151100307610ustar00rootroot00000000000000# Copyright (c) 2020-2024 Open Text. # # 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. """ WriteFile message In the copy-local protocol, the server may send a WriteFile message when it receives a batch of copy data. If the COPY FROM LOCAL command uses the REJECTED DATA and/or EXCEPTIONS parameters, this message contains content of rejected rows or exceptions output files. If the command uses the RETURNREJECTED parameters instead, this message is a series of row numbers saying which rows in the load were rejected. """ from __future__ import print_function, division, absolute_import, annotations from struct import unpack_from from ..message import BackendMessage class WriteFile(BackendMessage): message_id = b'O' def __init__(self, filename, file_length, data=None): BackendMessage.__init__(self) self.filename = filename self.file_length = file_length # Parse RETURNREJECTED data if self.filename == '': row_count = self.file_length // 8 # Rejected row numbers come in little endian format self.rejected_rows = unpack_from('<{0}Q'.format(row_count), data) def write_to_disk(self, connection, buffer_size): # Read the rest of the message from wire and write the file bytes_left = self.file_length with open(self.filename, 'ab') as f: pos = 0 while bytes_left > 0: bytes_to_read = min(buffer_size, bytes_left) content = connection.read_bytes(bytes_to_read) f.write(content) bytes_left -= bytes_to_read def __str__(self): return "WriteFile: Filename = {}, FileLength = {}".format(self.filename, self.file_length) BackendMessage.register(WriteFile) vertica-python-1.4.0/vertica_python/vertica/messages/frontend_messages/000077500000000000000000000000001464740151100265035ustar00rootroot00000000000000vertica-python-1.4.0/vertica_python/vertica/messages/frontend_messages/__init__.py000066400000000000000000000053161464740151100306210ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from .bind import Bind from .cancel_request import CancelRequest from .close import Close from .copy_data import CopyData from .copy_done import CopyDone from .copy_error import CopyError from .copy_fail import CopyFail from .describe import Describe from .end_of_batch_request import EndOfBatchRequest 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 from .ssl_request import SslRequest from .startup import Startup from .sync import Sync from .terminate import Terminate from .verified_files import VerifiedFiles __all__ = ['Bind', 'CancelRequest', 'Close', 'CopyData', 'CopyDone', 'CopyError', 'CopyFail', 'Describe', 'EndOfBatchRequest', 'Execute', 'Flush', 'LoadBalanceRequest', 'Parse', 'Password', 'Query', 'SslRequest', 'Startup', 'Sync', 'Terminate', 'VerifiedFiles'] vertica-python-1.4.0/vertica_python/vertica/messages/frontend_messages/bind.py000066400000000000000000000114501464740151100277720ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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. """ Bind message In the extended query protocol, the frontend sends a Bind message to bind values to parameter placeholders present in an existing prepared statement. The response is either BindComplete or ErrorResponse. """ from __future__ import print_function, division, absolute_import, annotations from struct import pack from ..message import BulkFrontendMessage from ....datatypes import VerticaType from ....compat import as_bytes BACKSLASH = b'\\' BACKSLASH_ESCAPE = b'\\134' class Bind(BulkFrontendMessage): message_id = b'B' def __init__(self, portal_name: str, prepared_statement_name: str, parameter_values, parameter_type_oids, binary_transfer: bool) -> None: BulkFrontendMessage.__init__(self) self._portal_name = portal_name self._prepared_statement_name = prepared_statement_name self._parameter_values = parameter_values self._parameter_type_oids = parameter_type_oids self._binary_transfer = binary_transfer def read_bytes(self): utf_portal_name = self._portal_name.encode('utf-8') utf_prepared_statement_name = self._prepared_statement_name.encode('utf-8') bytes_ = pack('!{0}sx{1}sx'.format(len(utf_portal_name), len(utf_prepared_statement_name)), utf_portal_name, utf_prepared_statement_name) # Parameter format codes -- use the default format (text) bytes_ += pack('!H', 0) # Number of parameters bytes_ += pack('!H', len(self._parameter_type_oids)) param_bytes_ = b'' for oid, val in zip(self._parameter_type_oids, self._parameter_values): # Parameter type oids bytes_ += pack('!I', oid) # Parameter values if val is None: # -1 indicates a NULL parameter value param_bytes_ += pack('!i', -1) elif oid in (VerticaType.BINARY, VerticaType.VARBINARY, VerticaType.LONGVARBINARY): # Encode binary data as UTF8 bytes val = as_bytes(val) # Escape the byte value \ with "\134"(octal for backslash) val = val.replace(BACKSLASH, BACKSLASH_ESCAPE) param_bytes_ += pack('!I{0}s'.format(len(val)), len(val), val) else: # Convert input to string if oid == VerticaType.BOOL: val = '1' if str(val).lower() in ('t', 'true', 'y', 'yes', '1') else '0' elif not isinstance(val, (str, bytes)): val = str(val) # Encode string as UTF8 bytes val = val.encode('utf-8') if not isinstance(val, bytes) else val param_bytes_ += pack('!I{0}s'.format(len(val)), len(val), val) bytes_ += param_bytes_ # Result column transfer format if self._binary_transfer: bytes_ += pack('!H', 1) # Specify the number of format codes followed bytes_ += pack('!H', 1) # Use binary format for all result columns else: # Use the default format (text) for all result columns bytes_ += pack('!H', 0) return bytes_ vertica-python-1.4.0/vertica_python/vertica/messages/frontend_messages/cancel_request.py000066400000000000000000000055011464740151100320530ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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. """ CancelRequest message The frontend sends a CancelRequest message to cancel the processing of the current operation. The cancel request must be sent across a new connection to the server. The server will process this request and then close the connection. The cancel request might or might not have any effect. If the cancellation is effective, the current command will terminate early and return an error message. If the cancellation fails (e.g. the server has finished processing the command), then there will be no visible result at all. """ from __future__ import print_function, division, absolute_import, annotations from struct import pack from ..message import BulkFrontendMessage class CancelRequest(BulkFrontendMessage): message_id = None def __init__(self, backend_pid, backend_key): BulkFrontendMessage.__init__(self) self._backend_pid = backend_pid # The process ID of the target backend self._backend_key = backend_key # The secret key of the target backend def read_bytes(self): bytes_ = pack('!3I', 80877102, self._backend_pid, self._backend_key) return bytes_ vertica-python-1.4.0/vertica_python/vertica/messages/frontend_messages/close.py000066400000000000000000000055271464740151100301730ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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. """ Close message In the extended query protocol, the frontend sends a Close message to close an existing prepared statement or portal and release resources. The response is either CloseComplete or ErrorResponse. It is not an error to issue Close against a nonexistent statement or portal name. """ from __future__ import print_function, division, absolute_import, annotations from struct import pack from ..message import BulkFrontendMessage class Close(BulkFrontendMessage): message_id = b'C' def __init__(self, close_type, close_name): BulkFrontendMessage.__init__(self) self._close_name = close_name if close_type == 'portal': self._close_type = b'P' elif close_type == 'prepared_statement': self._close_type = b'S' else: raise ValueError("{0} is not a valid close_type. " "Must be either portal or prepared_statement".format(close_type)) def read_bytes(self): utf_close_name = self._close_name.encode('utf-8') bytes_ = pack('c{0}sx'.format(len(utf_close_name)), self._close_type, utf_close_name) return bytes_ vertica-python-1.4.0/vertica_python/vertica/messages/frontend_messages/copy_data.py000066400000000000000000000044271464740151100310270ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from ..message import BulkFrontendMessage class CopyData(BulkFrontendMessage): message_id = b'd' def __init__(self, data, unicode_error='strict'): BulkFrontendMessage.__init__(self) if isinstance(data, str): self.bytes_ = data.encode(encoding='utf-8', errors=unicode_error) elif isinstance(data, bytes): self.bytes_ = data else: raise TypeError("Data should be string or bytes") def read_bytes(self): return self.bytes_ vertica-python-1.4.0/vertica_python/vertica/messages/frontend_messages/copy_done.py000066400000000000000000000035761464740151100310470ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from ..message import BulkFrontendMessage class CopyDone(BulkFrontendMessage): message_id = b'c' vertica-python-1.4.0/vertica_python/vertica/messages/frontend_messages/copy_error.py000066400000000000000000000031301464740151100312350ustar00rootroot00000000000000# Copyright (c) 2020-2024 Open Text. # # 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. """ CopyError message In the copy-local protocol, the frontend can terminate the cycle by sending a CopyError message, which will cause the COPY SQL statement to fail with an error. """ from __future__ import print_function, division, absolute_import, annotations from struct import pack from ..message import BulkFrontendMessage class CopyError(BulkFrontendMessage): message_id = b'e' def __init__(self, error_msg, stack_trace=None): BulkFrontendMessage.__init__(self) self.error_msg = error_msg.encode('utf-8') self.file_name = stack_trace[0].encode('utf-8') if stack_trace else b'' self.line_number = stack_trace[1] if stack_trace else 0 self.func_name = stack_trace[2].encode('utf-8') if stack_trace else b'' def read_bytes(self): bytes_ = pack('!{0}sxI{1}sx{2}sx'.format( len(self.file_name), len(self.func_name), len(self.error_msg)), self.file_name, self.line_number, self.func_name, self.error_msg) return bytes_ vertica-python-1.4.0/vertica_python/vertica/messages/frontend_messages/copy_fail.py000066400000000000000000000046401464740151100310260ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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. """ CopyFail message In the copy-in protocol, the frontend can terminate the cycle by sending a CopyFail message, which will cause the COPY SQL statement to fail with an error. """ from __future__ import print_function, division, absolute_import, annotations from struct import pack from ..message import BulkFrontendMessage class CopyFail(BulkFrontendMessage): message_id = b'f' def __init__(self, error_message): BulkFrontendMessage.__init__(self) # An error message to report as the cause of failure self._error_message = error_message.encode('utf-8') def read_bytes(self): bytes_ = pack('{0}sx'.format(len(self._error_message)), self._error_message) return bytes_ vertica-python-1.4.0/vertica_python/vertica/messages/frontend_messages/crypt_windows.py000077500000000000000000000240451464740151100320000ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations # Initial permutation IP = ( 58, 50, 42, 34, 26, 18, 10, 2, 60, 52, 44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14, 6, 64, 56, 48, 40, 32, 24, 16, 8, 57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3, 61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7, ) # Final permutation, FP = IP^(-1) FP = ( 40, 8, 48, 16, 56, 24, 64, 32, 39, 7, 47, 15, 55, 23, 63, 31, 38, 6, 46, 14, 54, 22, 62, 30, 37, 5, 45, 13, 53, 21, 61, 29, 36, 4, 44, 12, 52, 20, 60, 28, 35, 3, 43, 11, 51, 19, 59, 27, 34, 2, 42, 10, 50, 18, 58, 26, 33, 1, 41, 9, 49, 17, 57, 25, ) # Permuted-choice 1 from the key bits to yield C and D. # Note that bits 8,16... are left out: They are intended for a parity check. PC1_C = ( 57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35, 27, 19, 11, 3, 60, 52, 44, 36, ) PC1_D = ( 63, 55, 47, 39, 31, 23, 15, 7, 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 28, 20, 12, 4, ) # Permuted-choice 2, to pick out the bits from the CD array that generate the # key schedule. PC2_C = ( 14, 17, 11, 24, 1, 5, 3, 28, 15, 6, 21, 10, 23, 19, 12, 4, 26, 8, 16, 7, 27, 20, 13, 2, ) PC2_D = ( 41, 52, 31, 37, 47, 55, 30, 40, 51, 45, 33, 48, 44, 49, 39, 56, 34, 53, 46, 42, 50, 36, 29, 32, ) # The C and D arrays are used to calculate the key schedule. C = [0] * 28 D = [0] * 28 # The key schedule. Generated from the key. KS = [[0] * 48 for _ in range(16)] # The E bit-selection table. E = [0] * 48 e2 = ( 32, 1, 2, 3, 4, 5, 4, 5, 6, 7, 8, 9, 8, 9, 10, 11, 12, 13, 12, 13, 14, 15, 16, 17, 16, 17, 18, 19, 20, 21, 20, 21, 22, 23, 24, 25, 24, 25, 26, 27, 28, 29, 28, 29, 30, 31, 32, 1, ) # S-boxes. S = ( ( 14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8, 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13 ), ( 15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, 3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5, 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15, 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9 ), ( 10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1, 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7, 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12 ), ( 7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9, 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4, 3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14 ), ( 2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6, 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14, 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3 ), ( 12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8, 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6, 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13 ), ( 4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6, 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2, 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12 ), ( 13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2, 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8, 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11 ) ) # P is a permutation on the selected combination of the current L and key. P = ( 16, 7, 20, 21, 29, 12, 28, 17, 1, 15, 23, 26, 5, 18, 31, 10, 2, 8, 24, 14, 32, 27, 3, 9, 19, 13, 30, 6, 22, 11, 4, 25, ) # The combination of the key and the input, before selection. preS = [0] * 48 def __setkey(key): """ Set up the key schedule from the encryption key. """ global C, D, KS, E shifts = (1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1) # First, generate C and D by permuting the key. The lower order bit of each # 8-bit char is not used, so C and D are only 28 bits apiece. for i in range(28): C[i] = key[PC1_C[i] - 1] D[i] = key[PC1_D[i] - 1] for i in range(16): # rotate for k in range(shifts[i]): temp = C[0] for j in range(27): C[j] = C[j + 1] C[27] = temp temp = D[0] for j in range(27): D[j] = D[j + 1] D[27] = temp # get Ki. Note C and D are concatenated for j in range(24): KS[i][j] = C[PC2_C[j] - 1] KS[i][j + 24] = D[PC2_D[j] - 28 - 1] # load E with the initial E bit selections for i in range(48): E[i] = e2[i] def __encrypt(block): global preS left, right = [], [] # block in two halves f = [0] * 32 # First, permute the bits in the input for j in range(32): left.append(block[IP[j] - 1]) for j in range(32, 64): right.append(block[IP[j] - 1]) # Perform an encryption operation 16 times. for i in range(16): # Save the right array, which will be the new left. old = right[:] # Expand right to 48 bits using the E selector and exclusive-or with # the current key bits. for j in range(48): preS[j] = right[E[j] - 1] ^ KS[i][j] # The pre-select bits are now considered in 8 groups of 6 bits each. # The 8 selection functions map these 6-bit quantities into 4-bit # quantities and the results are permuted to make an f(R, K). # The indexing into the selection functions is peculiar; it could be # simplified by rewriting the tables. for j in range(8): temp = 6 * j k = S[j][(preS[temp + 0] << 5) + (preS[temp + 1] << 3) + (preS[temp + 2] << 2) + (preS[temp + 3] << 1) + (preS[temp + 4] << 0) + (preS[temp + 5] << 4)] temp = 4 * j f[temp + 0] = (k >> 3) & 1 f[temp + 1] = (k >> 2) & 1 f[temp + 2] = (k >> 1) & 1 f[temp + 3] = (k >> 0) & 1 # The new right is left ^ f(R, K). # The f here has to be permuted first, though. for j in range(32): right[j] = left[j] ^ f[P[j] - 1] # Finally the new left (the original right) is copied back. left = old # The output left and right are reversed. left, right = right, left # The final output gets the inverse permutation of the very original for j in range(64): i = FP[j] if i < 33: block[j] = left[i - 1] else: block[j] = right[i - 33] return block def crypt(pw, salt): iobuf = [] # break pw into 64 bits block = [] for c in pw: c = ord(c) for j in range(7): block.append((c >> (6 - j)) & 1) block.append(0) block += [0] * (64 - len(block)) # set key based on pw __setkey(block) for i in range(2): # store salt at beginning of results iobuf.append(salt[i]) c = ord(salt[i]) if c > ord('Z'): c -= 6 if c > ord('9'): c -= 7 c -= ord('.') # use salt to effect the E-bit selection for j in range(6): if (c >> j) & 1: E[6 * i + j], E[6 * i + j + 24] = E[6 * i + j + 24], E[6 * i + j] # call DES encryption 25 times using pw as key and initial data = 0 block = [0] * 66 for i in range(25): block = __encrypt(block) # format encrypted block for standard crypt(3) output for i in range(11): c = 0 for j in range(6): c <<= 1 c |= block[6 * i + j] c += ord('.') if c > ord('9'): c += 7 if c > ord('Z'): c += 6 iobuf.append(chr(c)) return ''.join(iobuf) vertica-python-1.4.0/vertica_python/vertica/messages/frontend_messages/describe.py000066400000000000000000000065211464740151100306410ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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. """ Describe message In the extended query protocol, the frontend sends a Describe message, which specifies the name of an existing prepared statement. The first response is a ParameterDescription message describing the parameters needed by the statement. The second response is a RowDescription message describing the rows that will be returned when the statement is eventually executed (or a NoData message if the statement will not return rows). The third response is a CommandDescription message describing the type of command to be executed and any semantically-equivalent COPY statement. """ from __future__ import print_function, division, absolute_import, annotations from struct import pack from ..message import BulkFrontendMessage class Describe(BulkFrontendMessage): message_id = b'D' def __init__(self, describe_type, describe_name): BulkFrontendMessage.__init__(self) self._describe_name = describe_name if describe_type == 'portal': self._describe_type = b'P' elif describe_type == 'prepared_statement': self._describe_type = b'S' else: raise ValueError("{0} is not a valid describe_type. " "Must be either portal or prepared_statement".format(describe_type)) def read_bytes(self): utf_name = self._describe_name.encode('utf-8') bytes_ = pack('c{0}sx'.format(len(utf_name)), self._describe_type, utf_name) return bytes_ def __str__(self): return 'Describe: type = {}, name = "{}"'.format( 'Portal' if self._describe_type == b'P' else 'Prepared Statement' , self._describe_name) vertica-python-1.4.0/vertica_python/vertica/messages/frontend_messages/end_of_batch_request.py000066400000000000000000000017321464740151100332230ustar00rootroot00000000000000# Copyright (c) 2020-2024 Open Text. # # 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. """ EndOfBatchRequest message EndOfBatchRequest message signals that a batch of rows has been sent, and the frontend is expecting an acknowledgment and possibly rejected row descriptions from the backend. """ from __future__ import print_function, division, absolute_import, annotations from ..message import BulkFrontendMessage class EndOfBatchRequest(BulkFrontendMessage): message_id = b'j' vertica-python-1.4.0/vertica_python/vertica/messages/frontend_messages/execute.py000066400000000000000000000054141464740151100305230ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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. """ Execute message In the extended query protocol, the frontend sends a Execute message once a portal exists. Execute doesn't cause RowDescription response message to be issued, so the frontend should issue Describe before issuing Execute, to ensure that it knows how to interpret the result rows it will get back. The Execute message specifies the portal name and a maximum result-row count. Currently, Vertica backend will ignore this result-row count and send all the rows regardless of what you put here. """ from __future__ import print_function, division, absolute_import, annotations from struct import pack from ..message import BulkFrontendMessage class Execute(BulkFrontendMessage): message_id = b'E' def __init__(self, portal_name, max_rows): BulkFrontendMessage.__init__(self) self._portal_name = portal_name self._max_rows = max_rows def read_bytes(self): utf_portal_name = self._portal_name.encode('utf-8') bytes_ = pack('!{0}sxI'.format(len(utf_portal_name)), utf_portal_name, self._max_rows) return bytes_ vertica-python-1.4.0/vertica_python/vertica/messages/frontend_messages/flush.py000066400000000000000000000043571464740151100302070ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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. """ Flush message The Flush message does not cause any specific output to be generated, but forces the backend to deliver any data pending in its output buffers. For example, in the extended query protocol, a Flush must be sent after any extended-query command except Sync, if the frontend wishes to examine the results of that command before issuing more commands. """ from __future__ import print_function, division, absolute_import, annotations from ..message import BulkFrontendMessage class Flush(BulkFrontendMessage): message_id = b'H' vertica-python-1.4.0/vertica_python/vertica/messages/frontend_messages/load_balance_request.py000077500000000000000000000040531464740151100332160ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations 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_ vertica-python-1.4.0/vertica_python/vertica/messages/frontend_messages/parse.py000066400000000000000000000056641464740151100302020ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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. """ Parse message In the extended query protocol, the frontend first sends a Parse message, which contains a textual query string. The query string leaves certain values unspecified with parameter placeholders (i.e. question mark '?'). The response is either ParseComplete or ErrorResponse. The query string cannot include more than one SQL statement; else an ErrorResponse is reported. The error message would be something like "Cannot insert multiple commands into a prepared statement" """ from __future__ import print_function, division, absolute_import, annotations from struct import pack from ..message import BulkFrontendMessage class Parse(BulkFrontendMessage): message_id = b'P' def __init__(self, name, query, param_types): BulkFrontendMessage.__init__(self) self._name = name self._query = query self._param_types = param_types def read_bytes(self): utf_name = self._name.encode('utf-8') utf_query = self._query.encode('utf-8') bytes_ = pack('!{0}sx{1}sxH'.format(len(utf_name), len(utf_query)), utf_name, utf_query, len(self._param_types)) for param in self._param_types: bytes_ += pack('!I', param) return bytes_ vertica-python-1.4.0/vertica_python/vertica/messages/frontend_messages/password.py000066400000000000000000000104171464740151100307220ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations import os import hashlib from struct import pack from ..message import BulkFrontendMessage from ..backend_messages.authentication import Authentication from ....compat import as_bytes from . import crypt_windows as crypt class Password(BulkFrontendMessage): message_id = b'p' def __init__(self, password, auth_method=None, options=None): BulkFrontendMessage.__init__(self) self._password = as_bytes(password) self._options = options or {} if auth_method is not None: self._auth_method = auth_method else: self._auth_method = Authentication.CLEARTEXT_PASSWORD def encoded_password(self): if self._auth_method == Authentication.CLEARTEXT_PASSWORD: return self._password elif self._auth_method == Authentication.CRYPT_PASSWORD: return crypt.crypt(self._password, self._options['salt']) elif self._auth_method in (Authentication.MD5_PASSWORD, Authentication.HASH, Authentication.HASH_MD5, Authentication.HASH_SHA512): # Encodes user/password/salt information in the following way: # MD5(MD5(password + user) + salt) # SHA512(SHA512(password + userSalt) + salt) useMD5 = self._auth_method in (Authentication.MD5_PASSWORD, Authentication.HASH_MD5) user = self._options['user'].encode('utf-8') if useMD5 else self._options['usersalt'] for key in (user, self._options['salt']): m = hashlib.md5() if useMD5 else hashlib.sha512() m.update(self._password + key) hexdigest = m.hexdigest() self._password = hexdigest.encode('utf-8') prefix = b'md5' if useMD5 else b'sha512' return prefix + self._password elif self._auth_method == Authentication.GSS: return self._password elif self._auth_method == Authentication.OAUTH: return self._password else: raise ValueError(f"unsupported authentication method: {self._auth_method}") def read_bytes(self): encoded_pw = self.encoded_password() # Vertica server handles GSS messages differently from other passwords if self._auth_method == Authentication.GSS: bytes_ = pack('{0}s'.format(len(encoded_pw)), encoded_pw) else: bytes_ = pack('{0}sx'.format(len(encoded_pw)), encoded_pw) return bytes_ vertica-python-1.4.0/vertica_python/vertica/messages/frontend_messages/query.py000066400000000000000000000047461464740151100302350ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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. """ Query message In the simple query protocol, the frontend sends a Query message, which contains an SQL command (or commands) expressed as a text string. The backend then sends one or more response messages depending on the contents of the query command string, and finally a ReadyForQuery message. """ from __future__ import print_function, division, absolute_import, annotations from struct import pack from ..message import BulkFrontendMessage class Query(BulkFrontendMessage): message_id = b'Q' def __init__(self, query_string): BulkFrontendMessage.__init__(self) self._query_string = query_string def read_bytes(self): encoded = self._query_string.encode('utf-8') bytes_ = pack('{0}sx'.format(len(encoded)), encoded) return bytes_ vertica-python-1.4.0/vertica_python/vertica/messages/frontend_messages/ssl_request.py000066400000000000000000000040231464740151100314250ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from struct import pack from ..message import BulkFrontendMessage class SslRequest(BulkFrontendMessage): message_id = None SSL_REQUEST = 80877103 def read_bytes(self): bytes_ = pack('!I', self.SSL_REQUEST) return bytes_ vertica-python-1.4.0/vertica_python/vertica/messages/frontend_messages/startup.py000066400000000000000000000117111464740151100305600ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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. """ Startup message To begin a session, the frontend opens a connection to the backend and sends a Startup message. """ from __future__ import print_function, division, absolute_import, annotations import platform import os import socket import warnings from struct import pack # noinspection PyUnresolvedReferences,PyCompatibility import vertica_python from ..message import BulkFrontendMessage class Startup(BulkFrontendMessage): message_id = None def __init__(self, user, database, session_label, os_user_name, autocommit, binary_transfer, request_complex_types, oauth_access_token, workload, auth_category): BulkFrontendMessage.__init__(self) try: os_platform = platform.platform() except Exception as e: os_platform = '' warnings.warn(f"Cannot get the OS info: {str(e)}") try: pid = str(os.getpid()) except Exception as e: pid = '0' warnings.warn(f"Cannot get the process ID: {str(e)}") try: os_hostname = socket.gethostname() except Exception as e: os_hostname = '' warnings.warn(f"Cannot get the OS hostname: {str(e)}") request_complex_types = 'true' if request_complex_types else 'false' self.parameters = { b'user': user, b'database': database, b'client_label': session_label, b'client_type': 'vertica-python', b'client_version': vertica_python.__version__, b'client_os': os_platform, b'client_os_user_name': os_user_name, b'client_os_hostname': os_hostname, b'client_pid': pid, b'autocommit': 'on' if autocommit else 'off', b'binary_data_protocol': '1' if binary_transfer else '0', # Defaults to text format '0' b'protocol_features': '{"request_complex_types":' + request_complex_types + '}', b'protocol_compat': 'VER', b'workload': workload, b'auth_category': auth_category, } if len(oauth_access_token) > 0: # compatibility for protocol version 3.11 self.parameters[b'oauth_access_token'] = oauth_access_token def read_bytes(self): # The fixed protocol version is followed by pairs of parameter name and value strings. # A zero byte is required as a terminator after the last name/value pair. # Parameters can appear in any order. fixed_protocol_version = 3 << 16 | 5 bytes_ = pack('!I', fixed_protocol_version) # The frontend sends a requested protocol version to the backend. # Old servers (protocol < 3.7) ignore this value and use the fixed protocol version. # New servers (protocol >= 3.7) would try to find the common protocol # version in use for both client and server, and send back a ParameterStatus # message (key='protocol_version', value=) bytes_ += pack('!16sxIx', b'protocol_version', vertica_python.PROTOCOL_VERSION) for k in self.parameters: v = self.parameters[k].encode('utf-8') bytes_ += pack('!{}sx{}sx'.format(len(k), len(v)), k, v) bytes_ += pack('x') return bytes_ vertica-python-1.4.0/vertica_python/vertica/messages/frontend_messages/sync.py000066400000000000000000000035721464740151100300400ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from ..message import BulkFrontendMessage class Sync(BulkFrontendMessage): message_id = b'S' vertica-python-1.4.0/vertica_python/vertica/messages/frontend_messages/terminate.py000066400000000000000000000035771464740151100310610ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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, annotations from ..message import BulkFrontendMessage class Terminate(BulkFrontendMessage): message_id = b'X' vertica-python-1.4.0/vertica_python/vertica/messages/frontend_messages/verified_files.py000066400000000000000000000030001464740151100320250ustar00rootroot00000000000000# Copyright (c) 2020-2024 Open Text. # # 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. from __future__ import print_function, division, absolute_import, annotations import os from struct import pack from ..message import BulkFrontendMessage class VerifiedFiles(BulkFrontendMessage): message_id = b'F' def __init__(self, file_list, protocol_version): BulkFrontendMessage.__init__(self) self.filenames = file_list self.protocol_version = protocol_version def read_bytes(self): if self.protocol_version >= (3 << 16 | 15): bytes_ = pack('!I', len(self.filenames)) # Int32 else: bytes_ = pack('!H', len(self.filenames)) # Int16 for filename in self.filenames: utf_filename = filename.encode('utf-8') bytes_ += pack('!{0}sx'.format(len(utf_filename)), utf_filename) bytes_ += pack('!Q', os.path.getsize(filename)) return bytes_ def __str__(self): return "VerifiedFiles: {}".format(self.filenames) vertica-python-1.4.0/vertica_python/vertica/messages/message.py000066400000000000000000000121361464740151100247760ustar00rootroot00000000000000# Copyright (c) 2018-2024 Open Text. # 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. """ Vertica Frontend/Backend Protocol All communication between frontend (client) and backend (server) is through a stream of messages. Message Format: The first byte of a message identifies the message type, and the next four bytes specify the length of the rest of the message (this length count includes itself, but not the message-type byte). The remaining contents of the message are determined by the message type. An exception is the Startup message sent by frontend, which has no message-type byte. All data in Vertica is represented as UTF-8, so the frontend has to convert data going to the backend from Python text string into UTF-8, and to convert data coming from the backend from UTF-8 into Python text string. """ from __future__ import print_function, division, absolute_import, annotations from abc import ABCMeta from struct import pack from ..messages import * class Message(object): __metaclass__ = ABCMeta def __init__(self): pass @property def message_id(self): raise NotImplementedError("no default message_id") def _bytes_to_message(self, msg): if isinstance(msg, list): msg = ''.join(msg) if hasattr(msg, 'bytesize'): bytesize = msg.bytesize + 4 else: bytesize = len(msg) + 4 message_size = pack('!I', bytesize) if self.message_id is not None: msg_with_size = self.message_id + message_size + msg else: msg_with_size = message_size + msg return msg_with_size def __str__(self): return self.__class__.__name__ # noinspection PyAbstractClass class BackendMessage(Message): __metaclass__ = ABCMeta _message_id_map = {} @classmethod def from_type(cls, type_, data, **kwargs) -> BackendMessage: klass = cls._message_id_map.get(type_) if klass is not None: return klass(data, **kwargs) else: from .backend_messages import Unknown return Unknown(type_, data) @staticmethod def register(cls): # TODO replace _message_id() with that assert issubclass(cls, BackendMessage), \ "{0} is not subclass of BackendMessage".format(cls.__name__) assert cls.message_id not in BackendMessage._message_id_map, \ "can't write the same key twice: {0}".format(cls.message_id) BackendMessage._message_id_map[cls.message_id] = cls # noinspection PyAbstractClass class FrontendMessage(Message): __metaclass__ = ABCMeta def fetch_message(self): """Generator for getting the message's content""" raise NotImplementedError("fetch_bytes has no default implementation") # noinspection PyAbstractClass class BulkFrontendMessage(FrontendMessage): __metaclass__ = ABCMeta def read_bytes(self): return b'' def get_message(self): bytes_ = self.read_bytes() return self._bytes_to_message(bytes_) def fetch_message(self): yield self.get_message() # noinspection PyAbstractClass class StreamFrontendMessage(FrontendMessage): __metaclass__ = ABCMeta def stream_bytes(self): raise NotImplementedError("stream_bytes has no default implementation") def stream_message(self): for bytes_ in self.stream_bytes(): yield self._bytes_to_message(bytes_) def fetch_message(self): for message in self.stream_message(): yield message vertica-python-1.4.0/vertica_python/vertica/mixins/000077500000000000000000000000001464740151100224755ustar00rootroot00000000000000vertica-python-1.4.0/vertica_python/vertica/mixins/__init__.py000066400000000000000000000012251464740151100246060ustar00rootroot00000000000000# Copyright (c) 2019-2024 Open Text. # # 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. from __future__ import print_function, division, absolute_import, annotations vertica-python-1.4.0/vertica_python/vertica/mixins/notice_response_attr.py000066400000000000000000000067211464740151100273060ustar00rootroot00000000000000# Copyright (c) 2019-2024 Open Text. # # 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. from __future__ import print_function, division, absolute_import, annotations from collections import OrderedDict FIELD_DEFINITIONS = [ {'type': b'q', 'name': "Internal Query", 'attribute': 'internal_query'}, {'type': b'S', 'name': "Severity", 'attribute': 'severity'}, {'type': b'M', 'name': "Message", 'attribute': 'message'}, {'type': b'C', 'name': "Sqlstate", 'attribute': 'sqlstate'}, {'type': b'D', 'name': "Detail", 'attribute': 'detail'}, {'type': b'H', 'name': "Hint", 'attribute': 'hint'}, {'type': b'P', 'name': "Position", 'attribute': 'position'}, {'type': b'W', 'name': "Where", 'attribute': 'where'}, {'type': b'p', 'name': "Internal Position", 'attribute': 'internal_position'}, {'type': b'R', 'name': "Routine", 'attribute': 'routine'}, {'type': b'F', 'name': "File", 'attribute': 'file'}, {'type': b'L', 'name': "Line", 'attribute': 'line'}, {'type': b'V', 'name': "Error Code", 'attribute': 'error_code'} ] FIELD_ATTR_TO_TYPE = {field['attribute']: field['type'] for field in FIELD_DEFINITIONS} class _NoticeResponseAttrMixin: # class must have `self._notice_attrs` property that provides a mapping from # the type indicator (see `FIELD_DEFINITIONS`) to value. @property def internal_query(self): return self._notice_attrs.get(FIELD_ATTR_TO_TYPE['internal_query']) @property def severity(self): return self._notice_attrs.get(FIELD_ATTR_TO_TYPE['severity']) @property def message(self): return self._notice_attrs.get(FIELD_ATTR_TO_TYPE['message']) @property def sqlstate(self): return self._notice_attrs.get(FIELD_ATTR_TO_TYPE['sqlstate']) @property def detail(self): return self._notice_attrs.get(FIELD_ATTR_TO_TYPE['detail']) @property def hint(self): return self._notice_attrs.get(FIELD_ATTR_TO_TYPE['hint']) @property def position(self): return self._notice_attrs.get(FIELD_ATTR_TO_TYPE['position']) @property def where(self): return self._notice_attrs.get(FIELD_ATTR_TO_TYPE['where']) @property def internal_position(self): return self._notice_attrs.get(FIELD_ATTR_TO_TYPE['internal_position']) @property def routine(self): return self._notice_attrs.get(FIELD_ATTR_TO_TYPE['routine']) @property def file(self): return self._notice_attrs.get(FIELD_ATTR_TO_TYPE['file']) @property def line(self): return self._notice_attrs.get(FIELD_ATTR_TO_TYPE['line']) @property def error_code(self): return self._notice_attrs.get(FIELD_ATTR_TO_TYPE['error_code']) def _get_labeled_values(self): values_mapping = OrderedDict() for field_def in FIELD_DEFINITIONS: if field_def['type'] in self._notice_attrs: values_mapping[field_def['name']] = self._notice_attrs[field_def['type']] return values_mapping vertica-python-1.4.0/vertica_python/vertica/tlsmode.py000066400000000000000000000045121464740151100232110ustar00rootroot00000000000000# Copyright (c) 2024 Open Text. # # 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. from __future__ import print_function, division, absolute_import, annotations import ssl import warnings from enum import Enum from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Optional class TLSMode(Enum): DISABLE = 'disable' PREFER = 'prefer' REQUIRE = 'require' VERIFY_CA = 'verify-ca' VERIFY_FULL = 'verify-full' def requests_encryption(self) -> bool: return self != TLSMode.DISABLE def requires_encryption(self) -> bool: return self not in (TLSMode.DISABLE, TLSMode.PREFER) def verify_certificate(self) -> bool: return self in (TLSMode.VERIFY_CA, TLSMode.VERIFY_FULL) def verify_hostname(self) -> bool: return self == TLSMode.VERIFY_FULL def get_sslcontext(self, cafile: Optional[str] = None, certfile: Optional[str] = None, keyfile: Optional[str] = None) -> ssl.SSLContext: ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ssl_context.check_hostname = self.verify_hostname() if self.verify_certificate(): ssl_context.verify_mode = ssl.CERT_REQUIRED if cafile: ssl_context.load_verify_locations(cafile=cafile) # mutual mode if certfile or keyfile: ssl_context.load_cert_chain(certfile=certfile, keyfile=keyfile) else: ssl_context.verify_mode = ssl.CERT_NONE if cafile or certfile or keyfile: ignore_cert_msg = ("Ignore TLS certificate files and skip certificates" f" validation as tlsmode is not '{TLSMode.VERIFY_CA.value}'" f" or '{TLSMode.VERIFY_FULL.value}'.") warnings.warn(ignore_cert_msg) return ssl_context