├── .gitattributes ├── MANIFEST.in ├── .gitignore ├── circleci ├── install-postgresql-9.1.22.sh ├── install-postgresql-9.2.17.sh ├── install-postgresql-9.3.13.sh ├── install-postgresql-9.4.8.sh ├── install-postgresql-9.5.3.sh ├── install-pypy-2.4.0.sh ├── install-python-3.3.5.sh ├── install-python-3.4.2.sh ├── install-python-3.5.1.sh ├── install-pypy3.3-5.5.sh ├── install-jython-2.7.0.sh └── install-postgresql-generic.sh ├── multi ├── setup.cfg ├── tests ├── test_pg8000_dbapi20.py ├── connection_settings.py ├── stress.py ├── test_typeobjects.py ├── performance.py ├── test_error_recovery.py ├── test_paramstyle.py ├── test_copy.py ├── test_connection.py ├── test_dbapi.py ├── test_query.py ├── test_typeconversion.py └── dbapi20.py ├── LICENSE ├── circle.yml ├── tox.ini ├── Dockerfile-test-env ├── setup.py ├── pg8000 ├── __init__.py └── _version.py └── README.adoc /.gitattributes: -------------------------------------------------------------------------------- 1 | pg8000/_version.py export-subst 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.creole 2 | include versioneer.py 3 | include pg8000/_version.py 4 | include LICENSE 5 | include doc/* 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.swp 3 | *.orig 4 | *.class 5 | build 6 | pg8000.egg-info 7 | tmp 8 | dist 9 | .tox 10 | MANIFEST 11 | -------------------------------------------------------------------------------- /circleci/install-postgresql-9.1.22.sh: -------------------------------------------------------------------------------- 1 | set -x 2 | set -e 3 | 4 | BUILDROOT=$HOME/pg8000 5 | PG_VERSION=9.1.22 6 | PG_PORT=5491 7 | 8 | source $BUILDROOT/circleci/install-postgresql-generic.sh 9 | -------------------------------------------------------------------------------- /circleci/install-postgresql-9.2.17.sh: -------------------------------------------------------------------------------- 1 | set -x 2 | set -e 3 | 4 | BUILDROOT=$HOME/pg8000 5 | PG_VERSION=9.2.17 6 | PG_PORT=5492 7 | 8 | source $BUILDROOT/circleci/install-postgresql-generic.sh 9 | -------------------------------------------------------------------------------- /circleci/install-postgresql-9.3.13.sh: -------------------------------------------------------------------------------- 1 | set -x 2 | set -e 3 | 4 | BUILDROOT=$HOME/pg8000 5 | PG_VERSION=9.3.13 6 | PG_PORT=5493 7 | 8 | source $BUILDROOT/circleci/install-postgresql-generic.sh 9 | -------------------------------------------------------------------------------- /circleci/install-postgresql-9.4.8.sh: -------------------------------------------------------------------------------- 1 | set -x 2 | set -e 3 | 4 | BUILDROOT=$HOME/pg8000 5 | PG_VERSION=9.4.8 6 | PG_PORT=5494 7 | 8 | source $BUILDROOT/circleci/install-postgresql-generic.sh 9 | -------------------------------------------------------------------------------- /circleci/install-postgresql-9.5.3.sh: -------------------------------------------------------------------------------- 1 | set -x 2 | set -e 3 | 4 | BUILDROOT=$HOME/pg8000 5 | PG_VERSION=9.5.3 6 | PG_PORT=5495 7 | 8 | source $BUILDROOT/circleci/install-postgresql-generic.sh 9 | -------------------------------------------------------------------------------- /multi: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # set postgres share memory to minimum to trigger unpinned buffer errors. 4 | 5 | for i in {1..100} 6 | do 7 | python -m pg8000.tests.stress & 8 | done 9 | wait 10 | echo "All processes done!" 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [upload_docs] 2 | upload-dir = build/sphinx/html 3 | 4 | [bdist_wheel] 5 | universal=1 6 | 7 | [versioneer] 8 | VCS = git 9 | style = pep440 10 | versionfile_source = pg8000/_version.py 11 | versionfile_build = pg8000/_version.py 12 | tag_prefix = 13 | parentdir_prefix = pg8000- 14 | -------------------------------------------------------------------------------- /circleci/install-pypy-2.4.0.sh: -------------------------------------------------------------------------------- 1 | set -x 2 | set -e 3 | 4 | BUILDROOT=$HOME/pg8000 5 | 6 | if [[ ! -e pypy-2.4.0-linux64/bin/pypy ]]; then 7 | wget https://bitbucket.org/pypy/pypy/downloads/pypy-2.4.0-linux64.tar.bz2 8 | tar -jxf pypy-2.4.0-linux64.tar.bz2 9 | rm -f pypy-2.4.0-linux64.tar.bz2 10 | fi 11 | 12 | ln -s $BUILDROOT/pypy-2.4.0-linux64/bin/pypy ~/bin/ 13 | -------------------------------------------------------------------------------- /circleci/install-python-3.3.5.sh: -------------------------------------------------------------------------------- 1 | set -x 2 | set -e 3 | 4 | BUILDROOT=$HOME/pg8000 5 | 6 | if [[ ! -e py-3.3.5/bin/python3.3 ]]; then 7 | wget http://www.python.org/ftp/python/3.3.5/Python-3.3.5.tgz 8 | tar -zxf Python-3.3.5.tgz 9 | cd ./Python-3.3.5 10 | ./configure --prefix=$BUILDROOT/py-3.3.5 11 | make install 12 | cd $BUILDROOT 13 | rm -rf ./Python-3.3.5.tgz ./Python-3.3.5 14 | fi 15 | 16 | ln -s $BUILDROOT/py-3.3.5/bin/python3.3 ~/bin/ 17 | -------------------------------------------------------------------------------- /circleci/install-python-3.4.2.sh: -------------------------------------------------------------------------------- 1 | set -x 2 | set -e 3 | 4 | BUILDROOT=$HOME/pg8000 5 | 6 | if [[ ! -e py-3.4.2/bin/python3.4 ]]; then 7 | wget https://www.python.org/ftp/python/3.4.2/Python-3.4.2.tgz 8 | tar -zxf Python-3.4.2.tgz 9 | cd ./Python-3.4.2 10 | ./configure --prefix=$BUILDROOT/py-3.4.2 11 | make install 12 | cd $BUILDROOT 13 | rm -rf ./Python-3.4.2.tgz ./Python-3.4.2 14 | fi 15 | 16 | ln -s $BUILDROOT/py-3.4.2/bin/python3.4 ~/bin/ 17 | -------------------------------------------------------------------------------- /circleci/install-python-3.5.1.sh: -------------------------------------------------------------------------------- 1 | set -x 2 | set -e 3 | 4 | BUILDROOT=$HOME/pg8000 5 | 6 | if [[ ! -e py-3.5.1/bin/python3.5 ]]; then 7 | wget https://www.python.org/ftp/python/3.5.1/Python-3.5.1.tgz 8 | tar -zxf Python-3.5.1.tgz 9 | cd ./Python-3.5.1 10 | ./configure --prefix=$BUILDROOT/py-3.5.1 11 | make install 12 | cd $BUILDROOT 13 | rm -rf ./Python-3.5.1.tgz ./Python-3.5.1 14 | fi 15 | 16 | ln -s $BUILDROOT/py-3.5.1/bin/python3.5 ~/bin/ 17 | -------------------------------------------------------------------------------- /circleci/install-pypy3.3-5.5.sh: -------------------------------------------------------------------------------- 1 | set -x 2 | set -e 3 | 4 | BUILDROOT=$HOME/pg8000 5 | 6 | if [[ ! -e pypy3.3-5.5-alpha-20161013-linux_x86_64-portable/bin/pypy ]]; then 7 | wget https://bitbucket.org/squeaky/portable-pypy/downloads/pypy3.3-5.5-alpha-20161013-linux_x86_64-portable.tar.bz2 8 | tar -jxf pypy3.3-5.5-alpha-20161013-linux_x86_64-portable.tar.bz2 9 | rm -f pypy3.3-5.5-alpha-20161013-linux_x86_64-portable.tar.bz2 10 | fi 11 | 12 | ln -s $BUILDROOT/pypy3.3-5.5-alpha-20161013-linux_x86_64-portable/bin/pypy ~/bin/pypy3 13 | -------------------------------------------------------------------------------- /tests/test_pg8000_dbapi20.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import dbapi20 3 | import unittest 4 | import pg8000 5 | from connection_settings import db_connect 6 | 7 | 8 | class Tests(dbapi20.DatabaseAPI20Test): 9 | driver = pg8000 10 | connect_args = () 11 | connect_kw_args = db_connect 12 | 13 | lower_func = 'lower' # For stored procedure test 14 | 15 | def test_nextset(self): 16 | pass 17 | 18 | def test_setoutputsize(self): 19 | pass 20 | 21 | 22 | if __name__ == '__main__': 23 | unittest.main() 24 | -------------------------------------------------------------------------------- /circleci/install-jython-2.7.0.sh: -------------------------------------------------------------------------------- 1 | set -x 2 | set -e 3 | 4 | BUILDROOT=$HOME/pg8000 5 | 6 | if [[ ! -e jython-2.7.0/bin/jython ]]; then 7 | wget -O jython-installer-2.7.0.jar "http://search.maven.org/remotecontent?filepath=org/python/jython-installer/2.7.0/jython-installer-2.7.0.jar" 8 | mkdir $BUILDROOT/jython-2.7.0 9 | cd $BUILDROOT/jython-2.7.0 10 | unzip $BUILDROOT/jython-installer-2.7.0.jar 11 | chmod +x ./bin/jython 12 | cd $BUILDROOT 13 | rm -f jython-installer-2.7.0.jar 14 | fi 15 | 16 | ln -s $BUILDROOT/jython-2.7.0/bin/jython ~/bin/ 17 | -------------------------------------------------------------------------------- /tests/connection_settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | ''' 4 | db_stewart_connect = { 5 | "host": "127.0.0.1", 6 | "user": "pg8000-test", 7 | "database": "pg8000-test", 8 | "password": "pg8000-test", 9 | "socket_timeout": 5, 10 | "ssl": False} 11 | 12 | db_local_connect = { 13 | "unix_sock": "/tmp/.s.PGSQL.5432", 14 | "user": "mfenniak"} 15 | 16 | db_local_win_connect = { 17 | "host": "localhost", 18 | "user": "mfenniak", 19 | "password": "password", 20 | "database": "mfenniak"} 21 | 22 | db_oracledev2_connect = { 23 | "host": "oracledev2", 24 | "user": "mfenniak", 25 | "password": "password", 26 | "database": "mfenniak"} 27 | ''' 28 | 29 | NAME_VAR = "PG8000_TEST_NAME" 30 | try: 31 | TEST_NAME = os.environ[NAME_VAR] 32 | except KeyError: 33 | raise Exception( 34 | "The environment variable " + NAME_VAR + " needs to be set. It should " 35 | "contain the name of the environment variable that contains the " 36 | "kwargs for the connect() function.") 37 | 38 | db_connect = eval(os.environ[TEST_NAME]) 39 | -------------------------------------------------------------------------------- /tests/stress.py: -------------------------------------------------------------------------------- 1 | import pg8000 2 | from pg8000.tests.connection_settings import db_connect 3 | from contextlib import closing 4 | 5 | 6 | with closing(pg8000.connect(**db_connect)) as db: 7 | for i in range(100): 8 | cursor = db.cursor() 9 | cursor.execute(""" 10 | SELECT n.nspname as "Schema", 11 | pg_catalog.format_type(t.oid, NULL) AS "Name", 12 | pg_catalog.obj_description(t.oid, 'pg_type') as "Description" 13 | FROM pg_catalog.pg_type t 14 | LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace 15 | left join pg_catalog.pg_namespace kj on n.oid = t.typnamespace 16 | WHERE (t.typrelid = 0 17 | OR (SELECT c.relkind = 'c' 18 | FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid)) 19 | AND NOT EXISTS( 20 | SELECT 1 FROM pg_catalog.pg_type el 21 | WHERE el.oid = t.typelem AND el.typarray = t.oid) 22 | AND pg_catalog.pg_type_is_visible(t.oid) 23 | ORDER BY 1, 2;""") 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007-2009, Mathieu Fenniak 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | * The name of the author may not be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 20 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | POSSIBILITY OF SUCH DAMAGE. 27 | 28 | -------------------------------------------------------------------------------- /tests/test_typeobjects.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pg8000 import Interval 3 | 4 | 5 | # Type conversion tests 6 | class Tests(unittest.TestCase): 7 | def setUp(self): 8 | pass 9 | 10 | def tearDown(self): 11 | pass 12 | 13 | def testIntervalConstructor(self): 14 | i = Interval(days=1) 15 | self.assertEqual(i.months, 0) 16 | self.assertEqual(i.days, 1) 17 | self.assertEqual(i.microseconds, 0) 18 | 19 | def intervalRangeTest(self, parameter, in_range, out_of_range): 20 | for v in out_of_range: 21 | try: 22 | Interval(**{parameter: v}) 23 | self.fail("expected OverflowError") 24 | except OverflowError: 25 | pass 26 | for v in in_range: 27 | Interval(**{parameter: v}) 28 | 29 | def testIntervalDaysRange(self): 30 | out_of_range_days = (-2147483648, +2147483648,) 31 | in_range_days = (-2147483647, +2147483647,) 32 | self.intervalRangeTest("days", in_range_days, out_of_range_days) 33 | 34 | def testIntervalMonthsRange(self): 35 | out_of_range_months = (-2147483648, +2147483648,) 36 | in_range_months = (-2147483647, +2147483647,) 37 | self.intervalRangeTest("months", in_range_months, out_of_range_months) 38 | 39 | def testIntervalMicrosecondsRange(self): 40 | out_of_range_microseconds = ( 41 | -9223372036854775808, +9223372036854775808,) 42 | in_range_microseconds = ( 43 | -9223372036854775807, +9223372036854775807,) 44 | self.intervalRangeTest( 45 | "microseconds", in_range_microseconds, out_of_range_microseconds) 46 | 47 | 48 | if __name__ == "__main__": 49 | unittest.main() 50 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | environment: 3 | PG8000_TEST_NAME: PG8000_TEST_9_5 4 | PG8000_TEST_9_1: "{\"user\": \"postgres\", \"password\": \"pw\", \"port\": 5491}" 5 | PG8000_TEST_9_2: "{\"user\": \"postgres\", \"password\": \"pw\", \"port\": 5492}" 6 | PG8000_TEST_9_3: "{\"user\": \"postgres\", \"password\": \"pw\", \"port\": 5493}" 7 | PG8000_TEST_9_4: "{\"user\": \"postgres\", \"password\": \"pw\", \"port\": 5494}" 8 | PG8000_TEST_9_5: "{\"user\": \"postgres\", \"password\": \"pw\", \"port\": 5495}" 9 | 10 | java: 11 | version: openjdk7 12 | 13 | post: 14 | - pyenv global 2.7.12 3.3.6 3.4.4 3.5.2 pypy-4.0.1 15 | 16 | 17 | 18 | dependencies: 19 | cache_directories: 20 | - pgsql-9.1.22 21 | - pgsql-9.2.17 22 | - pgsql-9.3.13 23 | - pgsql-9.4.8 24 | - pgsql-9.5.3 25 | - pypy3.3-5.5-alpha-20161013-linux_x86_64-portable 26 | - jython-2.7.0 27 | pre: 28 | - sudo apt-get update 29 | - sudo apt-get install libossp-uuid-dev libssl-dev 30 | - bash ./circleci/install-postgresql-9.1.22.sh 31 | - bash ./circleci/install-postgresql-9.2.17.sh 32 | - bash ./circleci/install-postgresql-9.3.13.sh 33 | - bash ./circleci/install-postgresql-9.4.8.sh 34 | - bash ./circleci/install-postgresql-9.5.3.sh 35 | - bash ./circleci/install-pypy3.3-5.5.sh 36 | - bash ./circleci/install-jython-2.7.0.sh 37 | post: 38 | - ./pgsql-9.1.22/bin/postgres -D `pwd`/pgsql-9.1.22/data -p 5491: 39 | background: true 40 | - ./pgsql-9.2.17/bin/postgres -D `pwd`/pgsql-9.2.17/data -p 5492: 41 | background: true 42 | - ./pgsql-9.3.13/bin/postgres -D `pwd`/pgsql-9.3.13/data -p 5493: 43 | background: true 44 | - ./pgsql-9.4.8/bin/postgres -D `pwd`/pgsql-9.4.8/data -p 5494: 45 | background: true 46 | - ./pgsql-9.5.3/bin/postgres -D `pwd`/pgsql-9.5.3/data -p 5495: 47 | background: true 48 | 49 | test: 50 | override: 51 | - tox 52 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py27, py33, py34, py35, pypy, pypy3, jython, pg_9_1, pg_9_2, pg_9_3, pg_9_4 8 | 9 | 10 | [testenv] 11 | passenv = PG8000_TEST_NAME PG8000_TEST_9_5 12 | commands = 13 | nosetests 14 | deps = 15 | nose 16 | pytz 17 | 18 | 19 | [testenv:pypy3] 20 | basepython = pypy3 21 | 22 | 23 | [testenv:pg_9_1] 24 | basepython = python3.5 25 | 26 | passenv = PG8000_TEST_9_1 27 | setenv: 28 | PG8000_TEST_NAME = PG8000_TEST_9_1 29 | commands = 30 | nosetests 31 | deps = 32 | nose 33 | pytz 34 | 35 | 36 | [testenv:pg_9_2] 37 | basepython = python3.5 38 | 39 | passenv = PG8000_TEST_9_2 40 | setenv: 41 | PG8000_TEST_NAME = PG8000_TEST_9_2 42 | commands = 43 | nosetests 44 | deps = 45 | nose 46 | pytz 47 | 48 | 49 | [testenv:pg_9_3] 50 | basepython = python3.5 51 | 52 | passenv = PG8000_TEST_9_3 53 | setenv: 54 | PG8000_TEST_NAME = PG8000_TEST_9_3 55 | commands = 56 | nosetests 57 | deps = 58 | nose 59 | pytz 60 | 61 | 62 | [testenv:pg_9_4] 63 | basepython = python3.5 64 | 65 | passenv = PG8000_TEST_9_4 66 | setenv: 67 | PG8000_TEST_NAME = PG8000_TEST_9_4 68 | commands = 69 | nosetests 70 | deps = 71 | nose 72 | pytz 73 | 74 | [testenv:pg_9_5] 75 | basepython = python3.5 76 | 77 | passenv = PG8000_TEST_9_5 78 | setenv: 79 | PG8000_TEST_NAME = PG8000_TEST_9_5 80 | commands = 81 | nosetests 82 | deps = 83 | six 84 | nose 85 | pytz 86 | 87 | [testenv:py35] 88 | passenv = PG8000_TEST_NAME PG8000_TEST_9_5 89 | commands = 90 | nosetests 91 | python -m doctest README.adoc 92 | flake8 pg8000 93 | python setup.py check 94 | deps = 95 | nose 96 | flake8 97 | pytz 98 | -------------------------------------------------------------------------------- /Dockerfile-test-env: -------------------------------------------------------------------------------- 1 | # Close match to CircleCI's test environment 2 | FROM ubuntu:12.04 3 | 4 | RUN groupadd -r postgres && useradd -r -g postgres postgres 5 | ADD circleci/*.sh /home/postgres/pg8000/circleci/ 6 | RUN mkdir -p /home/postgres/bin && chown -R postgres:postgres /home/postgres 7 | WORKDIR /home/postgres/pg8000 8 | 9 | RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ 10 | DEBIAN_FRONTEND=noninteractive apt-get install -y libossp-uuid16 libossp-uuid-dev libssl-dev wget bzip2 \ 11 | libreadline-dev libgss3 libgss-dev libxml2 libxml2-dev libkrb5-3 libkrb5-dev build-essential \ 12 | ca-certificates unzip openjdk-7-jre-headless python-pip && \ 13 | pip install tox && \ 14 | su postgres -c " \ 15 | bash ./circleci/install-postgresql-9.1.22.sh && \ 16 | bash ./circleci/install-postgresql-9.2.17.sh && \ 17 | bash ./circleci/install-postgresql-9.3.13.sh && \ 18 | bash ./circleci/install-postgresql-9.4.8.sh && \ 19 | bash ./circleci/install-postgresql-9.5.3.sh && \ 20 | bash ./circleci/install-python-2.7.9.sh && \ 21 | bash ./circleci/install-python-3.3.5.sh && \ 22 | bash ./circleci/install-python-3.4.2.sh && \ 23 | bash ./circleci/install-python-3.5.1.sh && \ 24 | bash ./circleci/install-pypy-2.4.0.sh && \ 25 | bash ./circleci/install-pypy3-2.3.1.sh && \ 26 | bash ./circleci/install-jython-2.7.0.sh \ 27 | " && \ 28 | DEBIAN_FRONTEND=noninteractive apt-get remove -y libossp-uuid-dev wget bzip2 libreadline-dev \ 29 | libgss-dev libxml2-dev libkrb5-dev build-essential unzip && \ 30 | DEBIAN_FRONTEND=noninteractive apt-get autoremove -y && \ 31 | rm -rf /var/lib/apt/lists/* 32 | 33 | ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/postgres/bin 34 | 35 | ENV PG8000_TEST_NAME=PG8000_TEST_9_5 36 | ENV PG8000_TEST_9_1="{\"user\": \"postgres\", \"password\": \"pw\", \"port\": 5491}" 37 | ENV PG8000_TEST_9_2="{\"user\": \"postgres\", \"password\": \"pw\", \"port\": 5492}" 38 | ENV PG8000_TEST_9_3="{\"user\": \"postgres\", \"password\": \"pw\", \"port\": 5493}" 39 | ENV PG8000_TEST_9_4="{\"user\": \"postgres\", \"password\": \"pw\", \"port\": 5494}" 40 | ENV PG8000_TEST_9_5="{\"user\": \"postgres\", \"password\": \"pw\", \"port\": 5495}" 41 | 42 | USER postgres 43 | CMD bash ./circleci/run-postgres-and-tox.sh 44 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import versioneer 4 | from setuptools import setup 5 | 6 | long_description = """\ 7 | 8 | pg8000 9 | ------ 10 | 11 | pg8000 is a Pure-Python interface to the PostgreSQL database engine. It is \ 12 | one of many PostgreSQL interfaces for the Python programming language. pg8000 \ 13 | is somewhat distinctive in that it is written entirely in Python and does not \ 14 | rely on any external libraries (such as a compiled python module, or \ 15 | PostgreSQL's libpq library). pg8000 supports the standard Python DB-API \ 16 | version 2.0. 17 | 18 | pg8000's name comes from the belief that it is probably about the 8000th \ 19 | PostgreSQL interface for Python.""" 20 | 21 | cmdclass = dict(versioneer.get_cmdclass()) 22 | version = versioneer.get_version() 23 | 24 | setup( 25 | name="pg8000", 26 | version=version, 27 | cmdclass=cmdclass, 28 | description="PostgreSQL interface library", 29 | long_description=long_description, 30 | author="Mathieu Fenniak", 31 | author_email="biziqe@mathieu.fenniak.net", 32 | url="https://github.com/mfenniak/pg8000", 33 | license="BSD", 34 | install_requires=[ 35 | "six>=1.10.0", 36 | ], 37 | classifiers=[ 38 | "Development Status :: 4 - Beta", 39 | "Intended Audience :: Developers", 40 | "License :: OSI Approved :: BSD License", 41 | "Programming Language :: Python", 42 | "Programming Language :: Python :: 2", 43 | "Programming Language :: Python :: 2.7", 44 | "Programming Language :: Python :: 3", 45 | "Programming Language :: Python :: 3.3", 46 | "Programming Language :: Python :: 3.4", 47 | "Programming Language :: Python :: 3.5", 48 | "Programming Language :: Python :: Implementation", 49 | "Programming Language :: Python :: Implementation :: CPython", 50 | "Programming Language :: Python :: Implementation :: Jython", 51 | "Programming Language :: Python :: Implementation :: PyPy", 52 | "Operating System :: OS Independent", 53 | "Topic :: Database :: Front-Ends", 54 | "Topic :: Software Development :: Libraries :: Python Modules", 55 | ], 56 | keywords="postgresql dbapi", 57 | packages=("pg8000",), 58 | command_options={ 59 | 'build_sphinx': { 60 | 'version': ('setup.py', version), 61 | 'release': ('setup.py', version)}}, 62 | ) 63 | -------------------------------------------------------------------------------- /tests/performance.py: -------------------------------------------------------------------------------- 1 | import pg8000 2 | from pg8000.tests.connection_settings import db_connect 3 | import time 4 | import warnings 5 | from contextlib import closing 6 | from decimal import Decimal 7 | 8 | 9 | whole_begin_time = time.time() 10 | 11 | tests = ( 12 | ("cast(id / 100 as int2)", 'int2'), 13 | ("cast(id as int4)", 'int4'), 14 | ("cast(id * 100 as int8)", 'int8'), 15 | ("(id %% 2) = 0", 'bool'), 16 | ("N'Static text string'", 'txt'), 17 | ("cast(id / 100 as float4)", 'float4'), 18 | ("cast(id / 100 as float8)", 'float8'), 19 | ("cast(id / 100 as numeric)", 'numeric'), 20 | ("timestamp '2001-09-28' + id * interval '1 second'", 'timestamp'), 21 | ) 22 | 23 | with warnings.catch_warnings(), closing(pg8000.connect(**db_connect)) as db: 24 | for txt, name in tests: 25 | query = """SELECT {0} AS column1, {0} AS column2, {0} AS column3, 26 | {0} AS column4, {0} AS column5, {0} AS column6, {0} AS column7 27 | FROM (SELECT generate_series(1, 10000) AS id) AS tbl""".format(txt) 28 | cursor = db.cursor() 29 | print("Beginning %s test..." % name) 30 | for i in range(1, 5): 31 | begin_time = time.time() 32 | cursor.execute(query) 33 | for row in cursor: 34 | pass 35 | end_time = time.time() 36 | print("Attempt %s - %s seconds." % (i, end_time - begin_time)) 37 | db.commit() 38 | cursor = db.cursor() 39 | cursor.execute( 40 | "CREATE TEMPORARY TABLE t1 (f1 serial primary key, " 41 | "f2 bigint not null, f3 varchar(50) null, f4 bool)") 42 | db.commit() 43 | params = [(Decimal('7.4009'), 'season of mists...', True)] * 1000 44 | print("Beginning executemany test...") 45 | for i in range(1, 5): 46 | begin_time = time.time() 47 | cursor.executemany( 48 | "insert into t1 (f2, f3, f4) values (%s, %s, %s)", params) 49 | db.commit() 50 | end_time = time.time() 51 | print("Attempt {0} took {1} seconds.".format(i, end_time - begin_time)) 52 | 53 | print("Beginning reuse statements test...") 54 | begin_time = time.time() 55 | for i in range(2000): 56 | cursor.execute("select count(*) from t1") 57 | cursor.fetchall() 58 | print("Took {0} seconds.".format(time.time() - begin_time)) 59 | 60 | print("Whole time - %s seconds." % (time.time() - whole_begin_time)) 61 | -------------------------------------------------------------------------------- /tests/test_error_recovery.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import pg8000 3 | from connection_settings import db_connect 4 | import warnings 5 | import datetime 6 | from sys import exc_info 7 | 8 | 9 | class TestException(Exception): 10 | pass 11 | 12 | 13 | class Tests(unittest.TestCase): 14 | def setUp(self): 15 | self.db = pg8000.connect(**db_connect) 16 | 17 | def tearDown(self): 18 | self.db.close() 19 | 20 | def raiseException(self, value): 21 | raise TestException("oh noes!") 22 | 23 | def testPyValueFail(self): 24 | # Ensure that if types.py_value throws an exception, the original 25 | # exception is raised (TestException), and the connection is 26 | # still usable after the error. 27 | orig = self.db.py_types[datetime.time] 28 | self.db.py_types[datetime.time] = ( 29 | orig[0], orig[1], self.raiseException) 30 | 31 | try: 32 | c = self.db.cursor() 33 | try: 34 | try: 35 | c.execute("SELECT %s as f1", (datetime.time(10, 30),)) 36 | c.fetchall() 37 | # shouldn't get here, exception should be thrown 38 | self.fail() 39 | except TestException: 40 | # should be TestException type, this is OK! 41 | self.db.rollback() 42 | finally: 43 | self.db.py_types[datetime.time] = orig 44 | 45 | # ensure that the connection is still usable for a new query 46 | c.execute("VALUES ('hw3'::text)") 47 | self.assertEqual(c.fetchone()[0], "hw3") 48 | finally: 49 | c.close() 50 | 51 | def testNoDataErrorRecovery(self): 52 | for i in range(1, 4): 53 | try: 54 | try: 55 | cursor = self.db.cursor() 56 | cursor.execute("DROP TABLE t1") 57 | finally: 58 | cursor.close() 59 | except pg8000.DatabaseError: 60 | e = exc_info()[1] 61 | # the only acceptable error is: 62 | self.assertEqual(e.args[1], '42P01') # table does not exist 63 | self.db.rollback() 64 | 65 | def testClosedConnection(self): 66 | warnings.simplefilter("ignore") 67 | my_db = pg8000.connect(**db_connect) 68 | cursor = my_db.cursor() 69 | my_db.close() 70 | try: 71 | cursor.execute("VALUES ('hw1'::text)") 72 | self.fail("Should have raised an exception") 73 | except: 74 | e = exc_info()[1] 75 | self.assertTrue(isinstance(e, self.db.InterfaceError)) 76 | self.assertEqual(str(e), 'connection is closed') 77 | 78 | warnings.resetwarnings() 79 | 80 | 81 | if __name__ == "__main__": 82 | unittest.main() 83 | -------------------------------------------------------------------------------- /tests/test_paramstyle.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import pg8000 3 | 4 | 5 | # Tests of the convert_paramstyle function. 6 | class Tests(unittest.TestCase): 7 | def testQmark(self): 8 | new_query, make_args = pg8000.core.convert_paramstyle( 9 | "qmark", "SELECT ?, ?, \"field_?\" FROM t " 10 | "WHERE a='say ''what?''' AND b=? AND c=E'?\\'test\\'?'") 11 | self.assertEqual( 12 | new_query, "SELECT $1, $2, \"field_?\" FROM t WHERE " 13 | "a='say ''what?''' AND b=$3 AND c=E'?\\'test\\'?'") 14 | self.assertEqual(make_args((1, 2, 3)), (1, 2, 3)) 15 | 16 | def testQmark2(self): 17 | new_query, make_args = pg8000.core.convert_paramstyle( 18 | "qmark", "SELECT ?, ?, * FROM t WHERE a=? AND b='are you ''sure?'") 19 | self.assertEqual( 20 | new_query, 21 | "SELECT $1, $2, * FROM t WHERE a=$3 AND b='are you ''sure?'") 22 | self.assertEqual(make_args((1, 2, 3)), (1, 2, 3)) 23 | 24 | def testNumeric(self): 25 | new_query, make_args = pg8000.core.convert_paramstyle( 26 | "numeric", "SELECT sum(x)::decimal(5, 2) :2, :1, * FROM t WHERE a=:3") 27 | self.assertEqual(new_query, "SELECT sum(x)::decimal(5, 2) $2, $1, * FROM t WHERE a=$3") 28 | self.assertEqual(make_args((1, 2, 3)), (1, 2, 3)) 29 | 30 | def testNamed(self): 31 | new_query, make_args = pg8000.core.convert_paramstyle( 32 | "named", "SELECT sum(x)::decimal(5, 2) :f_2, :f1 FROM t WHERE a=:f_2") 33 | self.assertEqual(new_query, "SELECT sum(x)::decimal(5, 2) $1, $2 FROM t WHERE a=$1") 34 | self.assertEqual(make_args({"f_2": 1, "f1": 2}), (1, 2)) 35 | 36 | def testFormat(self): 37 | new_query, make_args = pg8000.core.convert_paramstyle( 38 | "format", "SELECT %s, %s, \"f1_%%\", E'txt_%%' " 39 | "FROM t WHERE a=%s AND b='75%%'") 40 | self.assertEqual( 41 | new_query, 42 | "SELECT $1, $2, \"f1_%%\", E'txt_%%' FROM t WHERE a=$3 AND " 43 | "b='75%%'") 44 | self.assertEqual(make_args((1, 2, 3)), (1, 2, 3)) 45 | 46 | def testPyformat(self): 47 | new_query, make_args = pg8000.core.convert_paramstyle( 48 | "pyformat", "SELECT %(f2)s, %(f1)s, \"f1_%%\", E'txt_%%' " 49 | "FROM t WHERE a=%(f2)s AND b='75%%'") 50 | self.assertEqual( 51 | new_query, 52 | "SELECT $1, $2, \"f1_%%\", E'txt_%%' FROM t WHERE a=$1 AND " 53 | "b='75%%'") 54 | self.assertEqual(make_args({"f2": 1, "f1": 2, "f3": 3}), (1, 2)) 55 | 56 | # pyformat should support %s and an array, too: 57 | new_query, make_args = pg8000.core.convert_paramstyle( 58 | "pyformat", "SELECT %s, %s, \"f1_%%\", E'txt_%%' " 59 | "FROM t WHERE a=%s AND b='75%%'") 60 | self.assertEqual( 61 | new_query, 62 | "SELECT $1, $2, \"f1_%%\", E'txt_%%' FROM t WHERE a=$3 AND " 63 | "b='75%%'") 64 | self.assertEqual(make_args((1, 2, 3)), (1, 2, 3)) 65 | 66 | 67 | if __name__ == "__main__": 68 | unittest.main() 69 | -------------------------------------------------------------------------------- /tests/test_copy.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import pg8000 3 | from connection_settings import db_connect 4 | from six import b, BytesIO, u, iteritems 5 | from sys import exc_info 6 | 7 | 8 | class Tests(unittest.TestCase): 9 | def setUp(self): 10 | self.db = pg8000.connect(**db_connect) 11 | try: 12 | cursor = self.db.cursor() 13 | try: 14 | cursor = self.db.cursor() 15 | cursor.execute("DROP TABLE t1") 16 | except pg8000.DatabaseError: 17 | e = exc_info()[1] 18 | # the only acceptable error is: 19 | self.assertEqual( 20 | e.args[1], '42P01', # table does not exist 21 | "incorrect error for drop table") 22 | self.db.rollback() 23 | cursor.execute( 24 | "CREATE TEMPORARY TABLE t1 (f1 int primary key, " 25 | "f2 int not null, f3 varchar(50) null)") 26 | finally: 27 | cursor.close() 28 | 29 | def tearDown(self): 30 | self.db.close() 31 | 32 | def testCopyToWithTable(self): 33 | try: 34 | cursor = self.db.cursor() 35 | cursor.execute( 36 | "INSERT INTO t1 (f1, f2, f3) VALUES (%s, %s, %s)", (1, 1, 1)) 37 | cursor.execute( 38 | "INSERT INTO t1 (f1, f2, f3) VALUES (%s, %s, %s)", (2, 2, 2)) 39 | cursor.execute( 40 | "INSERT INTO t1 (f1, f2, f3) VALUES (%s, %s, %s)", (3, 3, 3)) 41 | 42 | stream = BytesIO() 43 | cursor.execute("copy t1 to stdout", stream=stream) 44 | self.assertEqual( 45 | stream.getvalue(), b("1\t1\t1\n2\t2\t2\n3\t3\t3\n")) 46 | self.assertEqual(cursor.rowcount, 3) 47 | self.db.commit() 48 | finally: 49 | cursor.close() 50 | 51 | def testCopyToWithQuery(self): 52 | try: 53 | cursor = self.db.cursor() 54 | stream = BytesIO() 55 | cursor.execute( 56 | "COPY (SELECT 1 as One, 2 as Two) TO STDOUT WITH DELIMITER " 57 | "'X' CSV HEADER QUOTE AS 'Y' FORCE QUOTE Two", stream=stream) 58 | self.assertEqual(stream.getvalue(), b('oneXtwo\n1XY2Y\n')) 59 | self.assertEqual(cursor.rowcount, 1) 60 | self.db.rollback() 61 | finally: 62 | cursor.close() 63 | 64 | def testCopyFromWithTable(self): 65 | try: 66 | cursor = self.db.cursor() 67 | stream = BytesIO(b("1\t1\t1\n2\t2\t2\n3\t3\t3\n")) 68 | cursor.execute("copy t1 from STDIN", stream=stream) 69 | self.assertEqual(cursor.rowcount, 3) 70 | 71 | cursor.execute("SELECT * FROM t1 ORDER BY f1") 72 | retval = cursor.fetchall() 73 | self.assertEqual(retval, ([1, 1, '1'], [2, 2, '2'], [3, 3, '3'])) 74 | self.db.rollback() 75 | finally: 76 | cursor.close() 77 | 78 | def testCopyFromWithQuery(self): 79 | try: 80 | cursor = self.db.cursor() 81 | stream = BytesIO(b("f1Xf2\n1XY1Y\n")) 82 | cursor.execute( 83 | "COPY t1 (f1, f2) FROM STDIN WITH DELIMITER 'X' CSV HEADER " 84 | "QUOTE AS 'Y' FORCE NOT NULL f1", stream=stream) 85 | self.assertEqual(cursor.rowcount, 1) 86 | 87 | cursor.execute("SELECT * FROM t1 ORDER BY f1") 88 | retval = cursor.fetchall() 89 | self.assertEqual(retval, ([1, 1, None],)) 90 | self.db.commit() 91 | finally: 92 | cursor.close() 93 | 94 | def testCopyFromWithError(self): 95 | try: 96 | cursor = self.db.cursor() 97 | stream = BytesIO(b("f1Xf2\n\n1XY1Y\n")) 98 | cursor.execute( 99 | "COPY t1 (f1, f2) FROM STDIN WITH DELIMITER 'X' CSV HEADER " 100 | "QUOTE AS 'Y' FORCE NOT NULL f1", stream=stream) 101 | self.assertTrue(False, "Should have raised an exception") 102 | except: 103 | args_dict = { 104 | 0: u('ERROR'), 105 | 1: u('22P02'), 106 | 2: u('invalid input syntax for integer: ""'), 107 | 3: u('COPY t1, line 2, column f1: ""'), 108 | 4: u('numutils.c'), 109 | 6: u('pg_atoi')} 110 | args = exc_info()[1].args 111 | for k, v in iteritems(args_dict): 112 | self.assertEqual(args[k], v) 113 | finally: 114 | cursor.close() 115 | 116 | 117 | if __name__ == "__main__": 118 | unittest.main() 119 | -------------------------------------------------------------------------------- /circleci/install-postgresql-generic.sh: -------------------------------------------------------------------------------- 1 | set -x 2 | set -e 3 | 4 | if [[ ! -e pgsql-${PG_VERSION}/bin/postgres ]]; then 5 | wget http://ftp.postgresql.org/pub/source/v${PG_VERSION}/postgresql-${PG_VERSION}.tar.bz2 6 | tar -jxf postgresql-${PG_VERSION}.tar.bz2 7 | cd postgresql-${PG_VERSION} 8 | ./configure --prefix=$BUILDROOT/pgsql-${PG_VERSION} --with-gssapi --with-openssl --with-ossp-uuid --with-libxml 9 | make world 10 | make install-world 11 | cd $BUILDROOT 12 | rm -rf postgresql-${PG_VERSION}.tar.bz2 postgresql-${PG_VERSION} # remove source tarball & build dir 13 | ./pgsql-${PG_VERSION}/bin/initdb -U postgres -E UTF8 `pwd`/pgsql-${PG_VERSION}/data 14 | 15 | sed -i -e "s/#port = 5432/port = ${PG_PORT}/" `pwd`/pgsql-${PG_VERSION}/data/postgresql.conf 16 | sed -i -e "s/#ssl = off/ssl = on/" `pwd`/pgsql-${PG_VERSION}/data/postgresql.conf 17 | 18 | cat > `pwd`/pgsql-${PG_VERSION}/data/pg_hba.conf < `pwd`/pgsql-${PG_VERSION}/data/server.crt < `pwd`/pgsql-${PG_VERSION}/data/server.key <`_; however, the arguments of the 50 | function are not defined by the specification. 51 | 52 | :param user: 53 | The username to connect to the PostgreSQL server with. 54 | 55 | If your server character encoding is not ``ascii`` or ``utf8``, then 56 | you need to provide ``user`` as bytes, eg. 57 | ``"my_name".encode('EUC-JP')``. 58 | 59 | :keyword host: 60 | The hostname of the PostgreSQL server to connect with. Providing this 61 | parameter is necessary for TCP/IP connections. One of either ``host`` 62 | or ``unix_sock`` must be provided. The default is ``localhost``. 63 | 64 | :keyword unix_sock: 65 | The path to the UNIX socket to access the database through, for 66 | example, ``'/tmp/.s.PGSQL.5432'``. One of either ``host`` or 67 | ``unix_sock`` must be provided. 68 | 69 | :keyword port: 70 | The TCP/IP port of the PostgreSQL server instance. This parameter 71 | defaults to ``5432``, the registered common port of PostgreSQL TCP/IP 72 | servers. 73 | 74 | :keyword database: 75 | The name of the database instance to connect with. This parameter is 76 | optional; if omitted, the PostgreSQL server will assume the database 77 | name is the same as the username. 78 | 79 | If your server character encoding is not ``ascii`` or ``utf8``, then 80 | you need to provide ``database`` as bytes, eg. 81 | ``"my_db".encode('EUC-JP')``. 82 | 83 | :keyword password: 84 | The user password to connect to the server with. This parameter is 85 | optional; if omitted and the database server requests password-based 86 | authentication, the connection will fail to open. If this parameter 87 | is provided but not requested by the server, no error will occur. 88 | 89 | If your server character encoding is not ``ascii`` or ``utf8``, then 90 | you need to provide ``user`` as bytes, eg. 91 | ``"my_password".encode('EUC-JP')``. 92 | 93 | :keyword application_name: 94 | The name will be displayed in the pg_stat_activity view. 95 | This parameter is optional. 96 | 97 | :keyword ssl: 98 | Use SSL encryption for TCP/IP sockets if ``True``. Defaults to 99 | ``False``. 100 | 101 | :keyword timeout: 102 | Only used with Python 3, this is the time in seconds before the 103 | connection to the database will time out. The default is ``None`` which 104 | means no timeout. 105 | 106 | :rtype: 107 | A :class:`Connection` object. 108 | """ 109 | return Connection( 110 | user, host, unix_sock, port, database, password, ssl, timeout, 111 | application_name) 112 | 113 | 114 | apilevel = "2.0" 115 | """The DBAPI level supported, currently "2.0". 116 | 117 | This property is part of the `DBAPI 2.0 specification 118 | `_. 119 | """ 120 | 121 | threadsafety = 1 122 | """Integer constant stating the level of thread safety the DBAPI interface 123 | supports. This DBAPI module supports sharing of the module only. Connections 124 | and cursors my not be shared between threads. This gives pg8000 a threadsafety 125 | value of 1. 126 | 127 | This property is part of the `DBAPI 2.0 specification 128 | `_. 129 | """ 130 | 131 | paramstyle = 'format' 132 | 133 | max_prepared_statements = 1000 134 | 135 | # I have no idea what this would be used for by a client app. Should it be 136 | # TEXT, VARCHAR, CHAR? It will only compare against row_description's 137 | # type_code if it is this one type. It is the varchar type oid for now, this 138 | # appears to match expectations in the DB API 2.0 compliance test suite. 139 | 140 | STRING = 1043 141 | """String type oid.""" 142 | 143 | 144 | NUMBER = 1700 145 | """Numeric type oid""" 146 | 147 | DATETIME = 1114 148 | """Timestamp type oid""" 149 | 150 | ROWID = 26 151 | """ROWID type oid""" 152 | 153 | __all__ = [ 154 | Warning, Bytea, DataError, DatabaseError, connect, InterfaceError, 155 | ProgrammingError, Error, OperationalError, IntegrityError, InternalError, 156 | NotSupportedError, ArrayContentNotHomogenousError, 157 | ArrayDimensionsNotConsistentError, ArrayContentNotSupportedError, utc, 158 | Connection, Cursor, Binary, Date, DateFromTicks, Time, TimeFromTicks, 159 | Timestamp, TimestampFromTicks, BINARY, Interval] 160 | 161 | """Version string for pg8000. 162 | 163 | .. versionadded:: 1.9.11 164 | """ 165 | -------------------------------------------------------------------------------- /tests/test_connection.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import pg8000 3 | from connection_settings import db_connect 4 | from six import PY2, u 5 | import sys 6 | from distutils.version import LooseVersion 7 | import socket 8 | import struct 9 | 10 | 11 | # Check if running in Jython 12 | if 'java' in sys.platform: 13 | from javax.net.ssl import TrustManager, X509TrustManager 14 | from jarray import array 15 | from javax.net.ssl import SSLContext 16 | 17 | class TrustAllX509TrustManager(X509TrustManager): 18 | '''Define a custom TrustManager which will blindly accept all 19 | certificates''' 20 | 21 | def checkClientTrusted(self, chain, auth): 22 | pass 23 | 24 | def checkServerTrusted(self, chain, auth): 25 | pass 26 | 27 | def getAcceptedIssuers(self): 28 | return None 29 | # Create a static reference to an SSLContext which will use 30 | # our custom TrustManager 31 | trust_managers = array([TrustAllX509TrustManager()], TrustManager) 32 | TRUST_ALL_CONTEXT = SSLContext.getInstance("SSL") 33 | TRUST_ALL_CONTEXT.init(None, trust_managers, None) 34 | # Keep a static reference to the JVM's default SSLContext for restoring 35 | # at a later time 36 | DEFAULT_CONTEXT = SSLContext.getDefault() 37 | 38 | 39 | def trust_all_certificates(f): 40 | '''Decorator function that will make it so the context of the decorated 41 | method will run with our TrustManager that accepts all certificates''' 42 | def wrapped(*args, **kwargs): 43 | # Only do this if running under Jython 44 | if 'java' in sys.platform: 45 | from javax.net.ssl import SSLContext 46 | SSLContext.setDefault(TRUST_ALL_CONTEXT) 47 | try: 48 | res = f(*args, **kwargs) 49 | return res 50 | finally: 51 | SSLContext.setDefault(DEFAULT_CONTEXT) 52 | else: 53 | return f(*args, **kwargs) 54 | return wrapped 55 | 56 | 57 | # Tests related to connecting to a database. 58 | class Tests(unittest.TestCase): 59 | def testSocketMissing(self): 60 | conn_params = { 61 | 'unix_sock': "/file-does-not-exist", 62 | 'user': "doesn't-matter"} 63 | self.assertRaises(pg8000.InterfaceError, pg8000.connect, **conn_params) 64 | 65 | def testDatabaseMissing(self): 66 | data = db_connect.copy() 67 | data["database"] = "missing-db" 68 | self.assertRaises(pg8000.ProgrammingError, pg8000.connect, **data) 69 | 70 | def testNotify(self): 71 | 72 | try: 73 | db = pg8000.connect(**db_connect) 74 | self.assertEqual(list(db.notifications), []) 75 | cursor = db.cursor() 76 | cursor.execute("LISTEN test") 77 | cursor.execute("NOTIFY test") 78 | db.commit() 79 | 80 | cursor.execute("VALUES (1, 2), (3, 4), (5, 6)") 81 | self.assertEqual(len(db.notifications), 1) 82 | self.assertEqual(db.notifications[0][1], "test") 83 | finally: 84 | cursor.close() 85 | db.close() 86 | 87 | # This requires a line in pg_hba.conf that requires md5 for the database 88 | # pg8000_md5 89 | 90 | def testMd5(self): 91 | data = db_connect.copy() 92 | data["database"] = "pg8000_md5" 93 | 94 | # Should only raise an exception saying db doesn't exist 95 | if PY2: 96 | self.assertRaises( 97 | pg8000.ProgrammingError, pg8000.connect, **data) 98 | else: 99 | self.assertRaisesRegex( 100 | pg8000.ProgrammingError, '3D000', pg8000.connect, **data) 101 | 102 | # This requires a line in pg_hba.conf that requires gss for the database 103 | # pg8000_gss 104 | 105 | def testGss(self): 106 | data = db_connect.copy() 107 | data["database"] = "pg8000_gss" 108 | 109 | # Should raise an exception saying gss isn't supported 110 | if PY2: 111 | self.assertRaises(pg8000.InterfaceError, pg8000.connect, **data) 112 | else: 113 | self.assertRaisesRegex( 114 | pg8000.InterfaceError, 115 | "Authentication method 7 not supported by pg8000.", 116 | pg8000.connect, **data) 117 | 118 | @trust_all_certificates 119 | def testSsl(self): 120 | data = db_connect.copy() 121 | data["ssl"] = True 122 | db = pg8000.connect(**data) 123 | db.close() 124 | 125 | # This requires a line in pg_hba.conf that requires 'password' for the 126 | # database pg8000_password 127 | 128 | def testPassword(self): 129 | data = db_connect.copy() 130 | data["database"] = "pg8000_password" 131 | 132 | # Should only raise an exception saying db doesn't exist 133 | if PY2: 134 | self.assertRaises( 135 | pg8000.ProgrammingError, pg8000.connect, **data) 136 | else: 137 | self.assertRaisesRegex( 138 | pg8000.ProgrammingError, '3D000', pg8000.connect, **data) 139 | 140 | def testUnicodeDatabaseName(self): 141 | data = db_connect.copy() 142 | data["database"] = "pg8000_sn\uFF6Fw" 143 | 144 | # Should only raise an exception saying db doesn't exist 145 | if PY2: 146 | self.assertRaises( 147 | pg8000.ProgrammingError, pg8000.connect, **data) 148 | else: 149 | self.assertRaisesRegex( 150 | pg8000.ProgrammingError, '3D000', pg8000.connect, **data) 151 | 152 | def testBytesDatabaseName(self): 153 | data = db_connect.copy() 154 | 155 | # Should only raise an exception saying db doesn't exist 156 | if PY2: 157 | data["database"] = "pg8000_sn\uFF6Fw" 158 | self.assertRaises( 159 | pg8000.ProgrammingError, pg8000.connect, **data) 160 | else: 161 | data["database"] = bytes("pg8000_sn\uFF6Fw", 'utf8') 162 | self.assertRaisesRegex( 163 | pg8000.ProgrammingError, '3D000', pg8000.connect, **data) 164 | 165 | def testBytesPassword(self): 166 | db = pg8000.connect(**db_connect) 167 | # Create user 168 | username = 'boltzmann' 169 | password = u('cha\uFF6Fs') 170 | cur = db.cursor() 171 | 172 | # Delete user if left over from previous run 173 | try: 174 | cur.execute("drop role " + username) 175 | except pg8000.ProgrammingError: 176 | cur.execute("rollback") 177 | 178 | cur.execute( 179 | "create user " + username + " with password '" + password + "';") 180 | cur.execute('commit;') 181 | db.close() 182 | 183 | data = db_connect.copy() 184 | data['user'] = username 185 | data['password'] = password.encode('utf8') 186 | data['database'] = 'pg8000_md5' 187 | if PY2: 188 | self.assertRaises( 189 | pg8000.ProgrammingError, pg8000.connect, **data) 190 | else: 191 | self.assertRaisesRegex( 192 | pg8000.ProgrammingError, '3D000', pg8000.connect, **data) 193 | 194 | db = pg8000.connect(**db_connect) 195 | cur = db.cursor() 196 | cur.execute("drop role " + username) 197 | cur.execute("commit;") 198 | db.close() 199 | 200 | def testBrokenPipe(self): 201 | db1 = pg8000.connect(**db_connect) 202 | db2 = pg8000.connect(**db_connect) 203 | 204 | try: 205 | cur1 = db1.cursor() 206 | cur2 = db2.cursor() 207 | 208 | cur1.execute("select pg_backend_pid()") 209 | pid1 = cur1.fetchone()[0] 210 | 211 | cur2.execute("select pg_terminate_backend(%s)", (pid1,)) 212 | try: 213 | cur1.execute("select 1") 214 | except Exception as e: 215 | self.assertTrue(isinstance(e, (socket.error, struct.error))) 216 | 217 | cur2.close() 218 | finally: 219 | db1.close() 220 | db2.close() 221 | 222 | def testApplicatioName(self): 223 | params = db_connect.copy() 224 | params['application_name'] = 'my test application name' 225 | db = pg8000.connect(**params) 226 | cur = db.cursor() 227 | 228 | if db._server_version >= LooseVersion('9.2'): 229 | cur.execute('select application_name from pg_stat_activity ' 230 | ' where pid = pg_backend_pid()') 231 | else: 232 | # for pg9.1 and earlier, procpod field rather than pid 233 | cur.execute('select application_name from pg_stat_activity ' 234 | ' where procpid = pg_backend_pid()') 235 | 236 | application_name = cur.fetchone()[0] 237 | self.assertEqual(application_name, 'my test application name') 238 | 239 | 240 | if __name__ == "__main__": 241 | unittest.main() 242 | -------------------------------------------------------------------------------- /tests/test_dbapi.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import time 4 | import pg8000 5 | import datetime 6 | from connection_settings import db_connect 7 | from sys import exc_info 8 | from six import b 9 | from distutils.version import LooseVersion 10 | 11 | 12 | # DBAPI compatible interface tests 13 | class Tests(unittest.TestCase): 14 | def setUp(self): 15 | self.db = pg8000.connect(**db_connect) 16 | 17 | # Neither Windows nor Jython 2.5.3 have a time.tzset() so skip 18 | if hasattr(time, 'tzset'): 19 | os.environ['TZ'] = "UTC" 20 | time.tzset() 21 | self.HAS_TZSET = True 22 | else: 23 | self.HAS_TZSET = False 24 | 25 | try: 26 | c = self.db.cursor() 27 | try: 28 | c = self.db.cursor() 29 | c.execute("DROP TABLE t1") 30 | except pg8000.DatabaseError: 31 | e = exc_info()[1] 32 | # the only acceptable error is: 33 | self.assertEqual(e.args[1], '42P01') # table does not exist 34 | self.db.rollback() 35 | c.execute( 36 | "CREATE TEMPORARY TABLE t1 " 37 | "(f1 int primary key, f2 int not null, f3 varchar(50) null)") 38 | c.execute( 39 | "INSERT INTO t1 (f1, f2, f3) VALUES (%s, %s, %s)", 40 | (1, 1, None)) 41 | c.execute( 42 | "INSERT INTO t1 (f1, f2, f3) VALUES (%s, %s, %s)", 43 | (2, 10, None)) 44 | c.execute( 45 | "INSERT INTO t1 (f1, f2, f3) VALUES (%s, %s, %s)", 46 | (3, 100, None)) 47 | c.execute( 48 | "INSERT INTO t1 (f1, f2, f3) VALUES (%s, %s, %s)", 49 | (4, 1000, None)) 50 | c.execute( 51 | "INSERT INTO t1 (f1, f2, f3) VALUES (%s, %s, %s)", 52 | (5, 10000, None)) 53 | self.db.commit() 54 | finally: 55 | c.close() 56 | 57 | def tearDown(self): 58 | self.db.close() 59 | 60 | def testParallelQueries(self): 61 | try: 62 | c1 = self.db.cursor() 63 | c2 = self.db.cursor() 64 | 65 | c1.execute("SELECT f1, f2, f3 FROM t1") 66 | while 1: 67 | row = c1.fetchone() 68 | if row is None: 69 | break 70 | f1, f2, f3 = row 71 | c2.execute("SELECT f1, f2, f3 FROM t1 WHERE f1 > %s", (f1,)) 72 | while 1: 73 | row = c2.fetchone() 74 | if row is None: 75 | break 76 | f1, f2, f3 = row 77 | finally: 78 | c1.close() 79 | c2.close() 80 | 81 | self.db.rollback() 82 | 83 | def testQmark(self): 84 | orig_paramstyle = pg8000.paramstyle 85 | try: 86 | pg8000.paramstyle = "qmark" 87 | c1 = self.db.cursor() 88 | c1.execute("SELECT f1, f2, f3 FROM t1 WHERE f1 > ?", (3,)) 89 | while 1: 90 | row = c1.fetchone() 91 | if row is None: 92 | break 93 | f1, f2, f3 = row 94 | self.db.rollback() 95 | finally: 96 | pg8000.paramstyle = orig_paramstyle 97 | c1.close() 98 | 99 | def testNumeric(self): 100 | orig_paramstyle = pg8000.paramstyle 101 | try: 102 | pg8000.paramstyle = "numeric" 103 | c1 = self.db.cursor() 104 | c1.execute("SELECT f1, f2, f3 FROM t1 WHERE f1 > :1", (3,)) 105 | while 1: 106 | row = c1.fetchone() 107 | if row is None: 108 | break 109 | f1, f2, f3 = row 110 | self.db.rollback() 111 | finally: 112 | pg8000.paramstyle = orig_paramstyle 113 | c1.close() 114 | 115 | def testNamed(self): 116 | orig_paramstyle = pg8000.paramstyle 117 | try: 118 | pg8000.paramstyle = "named" 119 | c1 = self.db.cursor() 120 | c1.execute( 121 | "SELECT f1, f2, f3 FROM t1 WHERE f1 > :f1", {"f1": 3}) 122 | while 1: 123 | row = c1.fetchone() 124 | if row is None: 125 | break 126 | f1, f2, f3 = row 127 | self.db.rollback() 128 | finally: 129 | pg8000.paramstyle = orig_paramstyle 130 | c1.close() 131 | 132 | def testFormat(self): 133 | orig_paramstyle = pg8000.paramstyle 134 | try: 135 | pg8000.paramstyle = "format" 136 | c1 = self.db.cursor() 137 | c1.execute("SELECT f1, f2, f3 FROM t1 WHERE f1 > %s", (3,)) 138 | while 1: 139 | row = c1.fetchone() 140 | if row is None: 141 | break 142 | f1, f2, f3 = row 143 | self.db.commit() 144 | finally: 145 | pg8000.paramstyle = orig_paramstyle 146 | c1.close() 147 | 148 | def testPyformat(self): 149 | orig_paramstyle = pg8000.paramstyle 150 | try: 151 | pg8000.paramstyle = "pyformat" 152 | c1 = self.db.cursor() 153 | c1.execute( 154 | "SELECT f1, f2, f3 FROM t1 WHERE f1 > %(f1)s", {"f1": 3}) 155 | while 1: 156 | row = c1.fetchone() 157 | if row is None: 158 | break 159 | f1, f2, f3 = row 160 | self.db.commit() 161 | finally: 162 | pg8000.paramstyle = orig_paramstyle 163 | c1.close() 164 | 165 | def testArraysize(self): 166 | try: 167 | c1 = self.db.cursor() 168 | c1.arraysize = 3 169 | c1.execute("SELECT * FROM t1") 170 | retval = c1.fetchmany() 171 | self.assertEqual(len(retval), c1.arraysize) 172 | finally: 173 | c1.close() 174 | self.db.commit() 175 | 176 | def testDate(self): 177 | val = pg8000.Date(2001, 2, 3) 178 | self.assertEqual(val, datetime.date(2001, 2, 3)) 179 | 180 | def testTime(self): 181 | val = pg8000.Time(4, 5, 6) 182 | self.assertEqual(val, datetime.time(4, 5, 6)) 183 | 184 | def testTimestamp(self): 185 | val = pg8000.Timestamp(2001, 2, 3, 4, 5, 6) 186 | self.assertEqual(val, datetime.datetime(2001, 2, 3, 4, 5, 6)) 187 | 188 | def testDateFromTicks(self): 189 | if self.HAS_TZSET: 190 | val = pg8000.DateFromTicks(1173804319) 191 | self.assertEqual(val, datetime.date(2007, 3, 13)) 192 | 193 | def testTimeFromTicks(self): 194 | if self.HAS_TZSET: 195 | val = pg8000.TimeFromTicks(1173804319) 196 | self.assertEqual(val, datetime.time(16, 45, 19)) 197 | 198 | def testTimestampFromTicks(self): 199 | if self.HAS_TZSET: 200 | val = pg8000.TimestampFromTicks(1173804319) 201 | self.assertEqual(val, datetime.datetime(2007, 3, 13, 16, 45, 19)) 202 | 203 | def testBinary(self): 204 | v = pg8000.Binary(b("\x00\x01\x02\x03\x02\x01\x00")) 205 | self.assertEqual(v, b("\x00\x01\x02\x03\x02\x01\x00")) 206 | self.assertTrue(isinstance(v, pg8000.BINARY)) 207 | 208 | def testRowCount(self): 209 | try: 210 | c1 = self.db.cursor() 211 | c1.execute("SELECT * FROM t1") 212 | 213 | # Before PostgreSQL 9 we don't know the row count for a select 214 | if self.db._server_version > LooseVersion('8.0.0'): 215 | self.assertEqual(5, c1.rowcount) 216 | 217 | c1.execute("UPDATE t1 SET f3 = %s WHERE f2 > 101", ("Hello!",)) 218 | self.assertEqual(2, c1.rowcount) 219 | 220 | c1.execute("DELETE FROM t1") 221 | self.assertEqual(5, c1.rowcount) 222 | finally: 223 | c1.close() 224 | self.db.commit() 225 | 226 | def testFetchMany(self): 227 | try: 228 | cursor = self.db.cursor() 229 | cursor.arraysize = 2 230 | cursor.execute("SELECT * FROM t1") 231 | self.assertEqual(2, len(cursor.fetchmany())) 232 | self.assertEqual(2, len(cursor.fetchmany())) 233 | self.assertEqual(1, len(cursor.fetchmany())) 234 | self.assertEqual(0, len(cursor.fetchmany())) 235 | finally: 236 | cursor.close() 237 | self.db.commit() 238 | 239 | def testIterator(self): 240 | from warnings import filterwarnings 241 | filterwarnings("ignore", "DB-API extension cursor.next()") 242 | filterwarnings("ignore", "DB-API extension cursor.__iter__()") 243 | 244 | try: 245 | cursor = self.db.cursor() 246 | cursor.execute("SELECT * FROM t1 ORDER BY f1") 247 | f1 = 0 248 | for row in cursor: 249 | next_f1 = row[0] 250 | assert next_f1 > f1 251 | f1 = next_f1 252 | except: 253 | cursor.close() 254 | 255 | self.db.commit() 256 | 257 | # Vacuum can't be run inside a transaction, so we need to turn 258 | # autocommit on. 259 | def testVacuum(self): 260 | self.db.autocommit = True 261 | try: 262 | cursor = self.db.cursor() 263 | cursor.execute("vacuum") 264 | finally: 265 | cursor.close() 266 | 267 | def testPreparedStatement(self): 268 | cursor = self.db.cursor() 269 | cursor.execute( 270 | 'PREPARE gen_series AS SELECT generate_series(1, 10);') 271 | cursor.execute('EXECUTE gen_series') 272 | 273 | 274 | if __name__ == "__main__": 275 | unittest.main() 276 | -------------------------------------------------------------------------------- /tests/test_query.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import pg8000 3 | from connection_settings import db_connect 4 | from six import u 5 | from sys import exc_info 6 | import datetime 7 | from distutils.version import LooseVersion 8 | 9 | from warnings import filterwarnings 10 | 11 | 12 | # Tests relating to the basic operation of the database driver, driven by the 13 | # pg8000 custom interface. 14 | class Tests(unittest.TestCase): 15 | def setUp(self): 16 | self.db = pg8000.connect(**db_connect) 17 | filterwarnings("ignore", "DB-API extension cursor.next()") 18 | filterwarnings("ignore", "DB-API extension cursor.__iter__()") 19 | self.db.paramstyle = 'format' 20 | try: 21 | cursor = self.db.cursor() 22 | try: 23 | cursor.execute("DROP TABLE t1") 24 | except pg8000.DatabaseError: 25 | e = exc_info()[1] 26 | # the only acceptable error is: 27 | self.assertEqual(e.args[1], '42P01') # table does not exist 28 | self.db.rollback() 29 | cursor.execute( 30 | "CREATE TEMPORARY TABLE t1 (f1 int primary key, " 31 | "f2 bigint not null, f3 varchar(50) null)") 32 | finally: 33 | cursor.close() 34 | 35 | self.db.commit() 36 | 37 | def tearDown(self): 38 | self.db.close() 39 | 40 | def testDatabaseError(self): 41 | try: 42 | cursor = self.db.cursor() 43 | self.assertRaises( 44 | pg8000.ProgrammingError, cursor.execute, 45 | "INSERT INTO t99 VALUES (1, 2, 3)") 46 | finally: 47 | cursor.close() 48 | 49 | self.db.rollback() 50 | 51 | def testParallelQueries(self): 52 | try: 53 | cursor = self.db.cursor() 54 | cursor.execute( 55 | "INSERT INTO t1 (f1, f2, f3) VALUES (%s, %s, %s)", 56 | (1, 1, None)) 57 | cursor.execute( 58 | "INSERT INTO t1 (f1, f2, f3) VALUES (%s, %s, %s)", 59 | (2, 10, None)) 60 | cursor.execute( 61 | "INSERT INTO t1 (f1, f2, f3) VALUES (%s, %s, %s)", 62 | (3, 100, None)) 63 | cursor.execute( 64 | "INSERT INTO t1 (f1, f2, f3) VALUES (%s, %s, %s)", 65 | (4, 1000, None)) 66 | cursor.execute( 67 | "INSERT INTO t1 (f1, f2, f3) VALUES (%s, %s, %s)", 68 | (5, 10000, None)) 69 | try: 70 | c1 = self.db.cursor() 71 | c2 = self.db.cursor() 72 | c1.execute("SELECT f1, f2, f3 FROM t1") 73 | for row in c1: 74 | f1, f2, f3 = row 75 | c2.execute( 76 | "SELECT f1, f2, f3 FROM t1 WHERE f1 > %s", (f1,)) 77 | for row in c2: 78 | f1, f2, f3 = row 79 | finally: 80 | c1.close() 81 | c2.close() 82 | finally: 83 | cursor.close() 84 | self.db.rollback() 85 | 86 | def testParallelOpenPortals(self): 87 | try: 88 | c1, c2 = self.db.cursor(), self.db.cursor() 89 | c1count, c2count = 0, 0 90 | q = "select * from generate_series(1, %s)" 91 | params = (100,) 92 | c1.execute(q, params) 93 | c2.execute(q, params) 94 | for c2row in c2: 95 | c2count += 1 96 | for c1row in c1: 97 | c1count += 1 98 | finally: 99 | c1.close() 100 | c2.close() 101 | self.db.rollback() 102 | 103 | self.assertEqual(c1count, c2count) 104 | 105 | # Run a query on a table, alter the structure of the table, then run the 106 | # original query again. 107 | 108 | def testAlter(self): 109 | try: 110 | cursor = self.db.cursor() 111 | cursor.execute("select * from t1") 112 | cursor.execute("alter table t1 drop column f3") 113 | cursor.execute("select * from t1") 114 | finally: 115 | cursor.close() 116 | self.db.rollback() 117 | 118 | # Run a query on a table, drop then re-create the table, then run the 119 | # original query again. 120 | 121 | def testCreate(self): 122 | try: 123 | cursor = self.db.cursor() 124 | cursor.execute("select * from t1") 125 | cursor.execute("drop table t1") 126 | cursor.execute("create temporary table t1 (f1 int primary key)") 127 | cursor.execute("select * from t1") 128 | finally: 129 | cursor.close() 130 | self.db.rollback() 131 | 132 | def testInsertReturning(self): 133 | try: 134 | cursor = self.db.cursor() 135 | cursor.execute("CREATE TABLE t2 (id serial, data text)") 136 | 137 | # Test INSERT ... RETURNING with one row... 138 | cursor.execute( 139 | "INSERT INTO t2 (data) VALUES (%s) RETURNING id", 140 | ("test1",)) 141 | row_id = cursor.fetchone()[0] 142 | cursor.execute("SELECT data FROM t2 WHERE id = %s", (row_id,)) 143 | self.assertEqual("test1", cursor.fetchone()[0]) 144 | 145 | # Before PostgreSQL 9 we don't know the row count for a select 146 | if self.db._server_version > LooseVersion('8.0.0'): 147 | self.assertEqual(cursor.rowcount, 1) 148 | 149 | # Test with multiple rows... 150 | cursor.execute( 151 | "INSERT INTO t2 (data) VALUES (%s), (%s), (%s) " 152 | "RETURNING id", ("test2", "test3", "test4")) 153 | self.assertEqual(cursor.rowcount, 3) 154 | ids = tuple([x[0] for x in cursor]) 155 | self.assertEqual(len(ids), 3) 156 | finally: 157 | cursor.close() 158 | self.db.rollback() 159 | 160 | def testRowCount(self): 161 | # Before PostgreSQL 9 we don't know the row count for a select 162 | if self.db._server_version > LooseVersion('8.0.0'): 163 | try: 164 | cursor = self.db.cursor() 165 | expected_count = 57 166 | cursor.executemany( 167 | "INSERT INTO t1 (f1, f2, f3) VALUES (%s, %s, %s)", 168 | tuple((i, i, None) for i in range(expected_count))) 169 | 170 | # Check rowcount after executemany 171 | self.assertEqual(expected_count, cursor.rowcount) 172 | self.db.commit() 173 | 174 | cursor.execute("SELECT * FROM t1") 175 | 176 | # Check row_count without doing any reading first... 177 | self.assertEqual(expected_count, cursor.rowcount) 178 | 179 | # Check rowcount after reading some rows, make sure it still 180 | # works... 181 | for i in range(expected_count // 2): 182 | cursor.fetchone() 183 | self.assertEqual(expected_count, cursor.rowcount) 184 | finally: 185 | cursor.close() 186 | self.db.commit() 187 | 188 | try: 189 | cursor = self.db.cursor() 190 | # Restart the cursor, read a few rows, and then check rowcount 191 | # again... 192 | cursor = self.db.cursor() 193 | cursor.execute("SELECT * FROM t1") 194 | for i in range(expected_count // 3): 195 | cursor.fetchone() 196 | self.assertEqual(expected_count, cursor.rowcount) 197 | self.db.rollback() 198 | 199 | # Should be -1 for a command with no results 200 | cursor.execute("DROP TABLE t1") 201 | self.assertEqual(-1, cursor.rowcount) 202 | finally: 203 | cursor.close() 204 | self.db.commit() 205 | 206 | def testRowCountUpdate(self): 207 | try: 208 | cursor = self.db.cursor() 209 | cursor.execute( 210 | "INSERT INTO t1 (f1, f2, f3) VALUES (%s, %s, %s)", 211 | (1, 1, None)) 212 | cursor.execute( 213 | "INSERT INTO t1 (f1, f2, f3) VALUES (%s, %s, %s)", 214 | (2, 10, None)) 215 | cursor.execute( 216 | "INSERT INTO t1 (f1, f2, f3) VALUES (%s, %s, %s)", 217 | (3, 100, None)) 218 | cursor.execute( 219 | "INSERT INTO t1 (f1, f2, f3) VALUES (%s, %s, %s)", 220 | (4, 1000, None)) 221 | cursor.execute( 222 | "INSERT INTO t1 (f1, f2, f3) VALUES (%s, %s, %s)", 223 | (5, 10000, None)) 224 | cursor.execute("UPDATE t1 SET f3 = %s WHERE f2 > 101", ("Hello!",)) 225 | self.assertEqual(cursor.rowcount, 2) 226 | finally: 227 | cursor.close() 228 | self.db.commit() 229 | 230 | def testIntOid(self): 231 | try: 232 | cursor = self.db.cursor() 233 | # https://bugs.launchpad.net/pg8000/+bug/230796 234 | cursor.execute( 235 | "SELECT typname FROM pg_type WHERE oid = %s", (100,)) 236 | finally: 237 | cursor.close() 238 | self.db.rollback() 239 | 240 | def testUnicodeQuery(self): 241 | try: 242 | cursor = self.db.cursor() 243 | cursor.execute( 244 | u( 245 | "CREATE TEMPORARY TABLE \u043c\u0435\u0441\u0442\u043e " 246 | "(\u0438\u043c\u044f VARCHAR(50), " 247 | "\u0430\u0434\u0440\u0435\u0441 VARCHAR(250))")) 248 | finally: 249 | cursor.close() 250 | self.db.commit() 251 | 252 | def testExecutemany(self): 253 | try: 254 | cursor = self.db.cursor() 255 | cursor.executemany( 256 | "INSERT INTO t1 (f1, f2, f3) VALUES (%s, %s, %s)", 257 | ((1, 1, 'Avast ye!'), (2, 1, None))) 258 | 259 | cursor.executemany( 260 | "select %s", 261 | ( 262 | (datetime.datetime(2014, 5, 7, tzinfo=pg8000.core.utc), ), 263 | (datetime.datetime(2014, 5, 7),))) 264 | finally: 265 | cursor.close() 266 | self.db.commit() 267 | 268 | # Check that autocommit stays off 269 | # We keep track of whether we're in a transaction or not by using the 270 | # READY_FOR_QUERY message. 271 | def testTransactions(self): 272 | try: 273 | cursor = self.db.cursor() 274 | cursor.execute("commit") 275 | cursor.execute( 276 | "INSERT INTO t1 (f1, f2, f3) VALUES (%s, %s, %s)", 277 | (1, 1, "Zombie")) 278 | cursor.execute("rollback") 279 | cursor.execute("select * from t1") 280 | 281 | # Before PostgreSQL 9 we don't know the row count for a select 282 | if self.db._server_version > LooseVersion('8.0.0'): 283 | self.assertEqual(cursor.rowcount, 0) 284 | finally: 285 | cursor.close() 286 | self.db.commit() 287 | 288 | def testIn(self): 289 | try: 290 | cursor = self.db.cursor() 291 | cursor.execute( 292 | "SELECT typname FROM pg_type WHERE oid = any(%s)", ([16, 23],)) 293 | ret = cursor.fetchall() 294 | self.assertEqual(ret[0][0], 'bool') 295 | finally: 296 | cursor.close() 297 | 298 | def test_no_previous_tpc(self): 299 | try: 300 | self.db.tpc_begin('Stacey') 301 | cursor = self.db.cursor() 302 | cursor.execute("SELECT * FROM pg_type") 303 | self.db.tpc_commit() 304 | finally: 305 | cursor.close() 306 | 307 | # Check that tpc_recover() doesn't start a transaction 308 | def test_tpc_recover(self): 309 | try: 310 | self.db.tpc_recover() 311 | cursor = self.db.cursor() 312 | self.db.autocommit = True 313 | 314 | # If tpc_recover() has started a transaction, this will fail 315 | cursor.execute("VACUUM") 316 | finally: 317 | cursor.close() 318 | 319 | # An empty query should raise a ProgrammingError 320 | def test_empty_query(self): 321 | try: 322 | cursor = self.db.cursor() 323 | self.assertRaises(pg8000.ProgrammingError, cursor.execute, "") 324 | finally: 325 | cursor.close() 326 | 327 | # rolling back when not in a transaction doesn't generate a warning 328 | def test_rollback_no_transaction(self): 329 | try: 330 | # Remove any existing notices 331 | self.db.notices.clear() 332 | 333 | cursor = self.db.cursor() 334 | 335 | # First, verify that a raw rollback does produce a notice 336 | self.db.execute(cursor, "rollback", None) 337 | 338 | self.assertEqual(1, len(self.db.notices)) 339 | # 25P01 is the code for no_active_sql_tronsaction. It has 340 | # a message and severity name, but those might be 341 | # localized/depend on the server version. 342 | self.assertEqual(self.db.notices.pop().get(b'C'), b'25P01') 343 | 344 | # Now going through the rollback method doesn't produce 345 | # any notices because it knows we're not in a transaction. 346 | self.db.rollback() 347 | 348 | self.assertEqual(0, len(self.db.notices)) 349 | 350 | finally: 351 | cursor.close() 352 | 353 | def test_context_manager_class(self): 354 | self.assertTrue('__enter__' in pg8000.core.Cursor.__dict__) 355 | self.assertTrue('__exit__' in pg8000.core.Cursor.__dict__) 356 | 357 | with self.db.cursor() as cursor: 358 | cursor.execute('select 1') 359 | 360 | def test_deallocate_prepared_statements(self): 361 | try: 362 | cursor = self.db.cursor() 363 | cursor.execute("select * from t1") 364 | cursor.execute("alter table t1 drop column f3") 365 | cursor.execute("select count(*) from pg_prepared_statements") 366 | res = cursor.fetchall() 367 | self.assertEqual(res[0][0], 1) 368 | finally: 369 | cursor.close() 370 | self.db.rollback() 371 | 372 | if __name__ == "__main__": 373 | unittest.main() 374 | -------------------------------------------------------------------------------- /pg8000/_version.py: -------------------------------------------------------------------------------- 1 | 2 | # This file helps to compute a version number in source trees obtained from 3 | # git-archive tarball (such as those provided by githubs download-from-tag 4 | # feature). Distribution tarballs (built by setup.py sdist) and build 5 | # directories (produced by setup.py build) will contain a much shorter file 6 | # that just contains the computed version number. 7 | 8 | # This file is released into the public domain. Generated by 9 | # versioneer-0.15 (https://github.com/warner/python-versioneer) 10 | 11 | import errno 12 | import os 13 | import re 14 | import subprocess 15 | import sys 16 | 17 | 18 | def get_keywords(): 19 | # these strings will be replaced by git during git-archive. 20 | # setup.py/versioneer.py will grep for the variable names, so they must 21 | # each be defined on a line of their own. _version.py will just call 22 | # get_keywords(). 23 | git_refnames = " (HEAD -> master)" 24 | git_full = "0132090f7e00f76738200433e9e9ea3da6a6839e" 25 | keywords = {"refnames": git_refnames, "full": git_full} 26 | return keywords 27 | 28 | 29 | class VersioneerConfig: 30 | pass 31 | 32 | 33 | def get_config(): 34 | # these strings are filled in when 'setup.py versioneer' creates 35 | # _version.py 36 | cfg = VersioneerConfig() 37 | cfg.VCS = "git" 38 | cfg.style = "pep440" 39 | cfg.tag_prefix = "" 40 | cfg.parentdir_prefix = "pg8000-" 41 | cfg.versionfile_source = "pg8000/_version.py" 42 | cfg.verbose = False 43 | return cfg 44 | 45 | 46 | class NotThisMethod(Exception): 47 | pass 48 | 49 | 50 | LONG_VERSION_PY = {} 51 | HANDLERS = {} 52 | 53 | 54 | def register_vcs_handler(vcs, method): # decorator 55 | def decorate(f): 56 | if vcs not in HANDLERS: 57 | HANDLERS[vcs] = {} 58 | HANDLERS[vcs][method] = f 59 | return f 60 | return decorate 61 | 62 | 63 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): 64 | assert isinstance(commands, list) 65 | p = None 66 | for c in commands: 67 | try: 68 | dispcmd = str([c] + args) 69 | # remember shell=False, so use git.cmd on windows, not just git 70 | p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, 71 | stderr=(subprocess.PIPE if hide_stderr 72 | else None)) 73 | break 74 | except EnvironmentError: 75 | e = sys.exc_info()[1] 76 | if e.errno == errno.ENOENT: 77 | continue 78 | if verbose: 79 | print("unable to run %s" % dispcmd) 80 | print(e) 81 | return None 82 | else: 83 | if verbose: 84 | print("unable to find command, tried %s" % (commands,)) 85 | return None 86 | stdout = p.communicate()[0].strip() 87 | if sys.version_info[0] >= 3: 88 | stdout = stdout.decode() 89 | if p.returncode != 0: 90 | if verbose: 91 | print("unable to run %s (error)" % dispcmd) 92 | return None 93 | return stdout 94 | 95 | 96 | def versions_from_parentdir(parentdir_prefix, root, verbose): 97 | # Source tarballs conventionally unpack into a directory that includes 98 | # both the project name and a version string. 99 | dirname = os.path.basename(root) 100 | if not dirname.startswith(parentdir_prefix): 101 | if verbose: 102 | print("guessing rootdir is '%s', but '%s' doesn't start with " 103 | "prefix '%s'" % (root, dirname, parentdir_prefix)) 104 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 105 | return {"version": dirname[len(parentdir_prefix):], 106 | "full-revisionid": None, 107 | "dirty": False, "error": None} 108 | 109 | 110 | @register_vcs_handler("git", "get_keywords") 111 | def git_get_keywords(versionfile_abs): 112 | # the code embedded in _version.py can just fetch the value of these 113 | # keywords. When used from setup.py, we don't want to import _version.py, 114 | # so we do it with a regexp instead. This function is not used from 115 | # _version.py. 116 | keywords = {} 117 | try: 118 | f = open(versionfile_abs, "r") 119 | for line in f.readlines(): 120 | if line.strip().startswith("git_refnames ="): 121 | mo = re.search(r'=\s*"(.*)"', line) 122 | if mo: 123 | keywords["refnames"] = mo.group(1) 124 | if line.strip().startswith("git_full ="): 125 | mo = re.search(r'=\s*"(.*)"', line) 126 | if mo: 127 | keywords["full"] = mo.group(1) 128 | f.close() 129 | except EnvironmentError: 130 | pass 131 | return keywords 132 | 133 | 134 | @register_vcs_handler("git", "keywords") 135 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 136 | if not keywords: 137 | raise NotThisMethod("no keywords at all, weird") 138 | refnames = keywords["refnames"].strip() 139 | if refnames.startswith("$Format"): 140 | if verbose: 141 | print("keywords are unexpanded, not using") 142 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 143 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 144 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 145 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 146 | TAG = "tag: " 147 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 148 | if not tags: 149 | # Either we're using git < 1.8.3, or there really are no tags. We use 150 | # a heuristic: assume all version tags have a digit. The old git %d 151 | # expansion behaves like git log --decorate=short and strips out the 152 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 153 | # between branches and tags. By ignoring refnames without digits, we 154 | # filter out many common branch names like "release" and 155 | # "stabilization", as well as "HEAD" and "master". 156 | tags = set([r for r in refs if re.search(r'\d', r)]) 157 | if verbose: 158 | print("discarding '%s', no digits" % ",".join(refs-tags)) 159 | if verbose: 160 | print("likely tags: %s" % ",".join(sorted(tags))) 161 | for ref in sorted(tags): 162 | # sorting will prefer e.g. "2.0" over "2.0rc1" 163 | if ref.startswith(tag_prefix): 164 | r = ref[len(tag_prefix):] 165 | if verbose: 166 | print("picking %s" % r) 167 | return {"version": r, 168 | "full-revisionid": keywords["full"].strip(), 169 | "dirty": False, "error": None 170 | } 171 | # no suitable tags, so version is "0+unknown", but full hex is still there 172 | if verbose: 173 | print("no suitable tags, using unknown + full revision id") 174 | return {"version": "0+unknown", 175 | "full-revisionid": keywords["full"].strip(), 176 | "dirty": False, "error": "no suitable tags"} 177 | 178 | 179 | @register_vcs_handler("git", "pieces_from_vcs") 180 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 181 | # this runs 'git' from the root of the source tree. This only gets called 182 | # if the git-archive 'subst' keywords were *not* expanded, and 183 | # _version.py hasn't already been rewritten with a short version string, 184 | # meaning we're inside a checked out source tree. 185 | 186 | if not os.path.exists(os.path.join(root, ".git")): 187 | if verbose: 188 | print("no .git in %s" % root) 189 | raise NotThisMethod("no .git directory") 190 | 191 | GITS = ["git"] 192 | if sys.platform == "win32": 193 | GITS = ["git.cmd", "git.exe"] 194 | # if there is a tag, this yields TAG-NUM-gHEX[-dirty] 195 | # if there are no tags, this yields HEX[-dirty] (no NUM) 196 | describe_out = run_command(GITS, ["describe", "--tags", "--dirty", 197 | "--always", "--long"], 198 | cwd=root) 199 | # --long was added in git-1.5.5 200 | if describe_out is None: 201 | raise NotThisMethod("'git describe' failed") 202 | describe_out = describe_out.strip() 203 | full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 204 | if full_out is None: 205 | raise NotThisMethod("'git rev-parse' failed") 206 | full_out = full_out.strip() 207 | 208 | pieces = {} 209 | pieces["long"] = full_out 210 | pieces["short"] = full_out[:7] # maybe improved later 211 | pieces["error"] = None 212 | 213 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 214 | # TAG might have hyphens. 215 | git_describe = describe_out 216 | 217 | # look for -dirty suffix 218 | dirty = git_describe.endswith("-dirty") 219 | pieces["dirty"] = dirty 220 | if dirty: 221 | git_describe = git_describe[:git_describe.rindex("-dirty")] 222 | 223 | # now we have TAG-NUM-gHEX or HEX 224 | 225 | if "-" in git_describe: 226 | # TAG-NUM-gHEX 227 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 228 | if not mo: 229 | # unparseable. Maybe git-describe is misbehaving? 230 | pieces["error"] = ("unable to parse git-describe output: '%s'" 231 | % describe_out) 232 | return pieces 233 | 234 | # tag 235 | full_tag = mo.group(1) 236 | if not full_tag.startswith(tag_prefix): 237 | if verbose: 238 | fmt = "tag '%s' doesn't start with prefix '%s'" 239 | print(fmt % (full_tag, tag_prefix)) 240 | pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" 241 | % (full_tag, tag_prefix)) 242 | return pieces 243 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 244 | 245 | # distance: number of commits since tag 246 | pieces["distance"] = int(mo.group(2)) 247 | 248 | # commit: short hex revision ID 249 | pieces["short"] = mo.group(3) 250 | 251 | else: 252 | # HEX: no tags 253 | pieces["closest-tag"] = None 254 | count_out = run_command(GITS, ["rev-list", "HEAD", "--count"], 255 | cwd=root) 256 | pieces["distance"] = int(count_out) # total number of commits 257 | 258 | return pieces 259 | 260 | 261 | def plus_or_dot(pieces): 262 | if "+" in pieces.get("closest-tag", ""): 263 | return "." 264 | return "+" 265 | 266 | 267 | def render_pep440(pieces): 268 | # now build up version string, with post-release "local version 269 | # identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 270 | # get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 271 | 272 | # exceptions: 273 | # 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 274 | 275 | if pieces["closest-tag"]: 276 | rendered = pieces["closest-tag"] 277 | if pieces["distance"] or pieces["dirty"]: 278 | rendered += plus_or_dot(pieces) 279 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 280 | if pieces["dirty"]: 281 | rendered += ".dirty" 282 | else: 283 | # exception #1 284 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], 285 | pieces["short"]) 286 | if pieces["dirty"]: 287 | rendered += ".dirty" 288 | return rendered 289 | 290 | 291 | def render_pep440_pre(pieces): 292 | # TAG[.post.devDISTANCE] . No -dirty 293 | 294 | # exceptions: 295 | # 1: no tags. 0.post.devDISTANCE 296 | 297 | if pieces["closest-tag"]: 298 | rendered = pieces["closest-tag"] 299 | if pieces["distance"]: 300 | rendered += ".post.dev%d" % pieces["distance"] 301 | else: 302 | # exception #1 303 | rendered = "0.post.dev%d" % pieces["distance"] 304 | return rendered 305 | 306 | 307 | def render_pep440_post(pieces): 308 | # TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that 309 | # .dev0 sorts backwards (a dirty tree will appear "older" than the 310 | # corresponding clean one), but you shouldn't be releasing software with 311 | # -dirty anyways. 312 | 313 | # exceptions: 314 | # 1: no tags. 0.postDISTANCE[.dev0] 315 | 316 | if pieces["closest-tag"]: 317 | rendered = pieces["closest-tag"] 318 | if pieces["distance"] or pieces["dirty"]: 319 | rendered += ".post%d" % pieces["distance"] 320 | if pieces["dirty"]: 321 | rendered += ".dev0" 322 | rendered += plus_or_dot(pieces) 323 | rendered += "g%s" % pieces["short"] 324 | else: 325 | # exception #1 326 | rendered = "0.post%d" % pieces["distance"] 327 | if pieces["dirty"]: 328 | rendered += ".dev0" 329 | rendered += "+g%s" % pieces["short"] 330 | return rendered 331 | 332 | 333 | def render_pep440_old(pieces): 334 | # TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. 335 | 336 | # exceptions: 337 | # 1: no tags. 0.postDISTANCE[.dev0] 338 | 339 | if pieces["closest-tag"]: 340 | rendered = pieces["closest-tag"] 341 | if pieces["distance"] or pieces["dirty"]: 342 | rendered += ".post%d" % pieces["distance"] 343 | if pieces["dirty"]: 344 | rendered += ".dev0" 345 | else: 346 | # exception #1 347 | rendered = "0.post%d" % pieces["distance"] 348 | if pieces["dirty"]: 349 | rendered += ".dev0" 350 | return rendered 351 | 352 | 353 | def render_git_describe(pieces): 354 | # TAG[-DISTANCE-gHEX][-dirty], like 'git describe --tags --dirty 355 | # --always' 356 | 357 | # exceptions: 358 | # 1: no tags. HEX[-dirty] (note: no 'g' prefix) 359 | 360 | if pieces["closest-tag"]: 361 | rendered = pieces["closest-tag"] 362 | if pieces["distance"]: 363 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 364 | else: 365 | # exception #1 366 | rendered = pieces["short"] 367 | if pieces["dirty"]: 368 | rendered += "-dirty" 369 | return rendered 370 | 371 | 372 | def render_git_describe_long(pieces): 373 | # TAG-DISTANCE-gHEX[-dirty], like 'git describe --tags --dirty 374 | # --always -long'. The distance/hash is unconditional. 375 | 376 | # exceptions: 377 | # 1: no tags. HEX[-dirty] (note: no 'g' prefix) 378 | 379 | if pieces["closest-tag"]: 380 | rendered = pieces["closest-tag"] 381 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 382 | else: 383 | # exception #1 384 | rendered = pieces["short"] 385 | if pieces["dirty"]: 386 | rendered += "-dirty" 387 | return rendered 388 | 389 | 390 | def render(pieces, style): 391 | if pieces["error"]: 392 | return {"version": "unknown", 393 | "full-revisionid": pieces.get("long"), 394 | "dirty": None, 395 | "error": pieces["error"]} 396 | 397 | if not style or style == "default": 398 | style = "pep440" # the default 399 | 400 | if style == "pep440": 401 | rendered = render_pep440(pieces) 402 | elif style == "pep440-pre": 403 | rendered = render_pep440_pre(pieces) 404 | elif style == "pep440-post": 405 | rendered = render_pep440_post(pieces) 406 | elif style == "pep440-old": 407 | rendered = render_pep440_old(pieces) 408 | elif style == "git-describe": 409 | rendered = render_git_describe(pieces) 410 | elif style == "git-describe-long": 411 | rendered = render_git_describe_long(pieces) 412 | else: 413 | raise ValueError("unknown style '%s'" % style) 414 | 415 | return {"version": rendered, "full-revisionid": pieces["long"], 416 | "dirty": pieces["dirty"], "error": None} 417 | 418 | 419 | def get_versions(): 420 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 421 | # __file__, we can work backwards from there to the root. Some 422 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 423 | # case we can only use expanded keywords. 424 | 425 | cfg = get_config() 426 | verbose = cfg.verbose 427 | 428 | try: 429 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, 430 | verbose) 431 | except NotThisMethod: 432 | pass 433 | 434 | try: 435 | root = os.path.realpath(__file__) 436 | # versionfile_source is the relative path from the top of the source 437 | # tree (where the .git directory might live) to this file. Invert 438 | # this to find the root from __file__. 439 | for i in cfg.versionfile_source.split('/'): 440 | root = os.path.dirname(root) 441 | except NameError: 442 | return {"version": "0+unknown", "full-revisionid": None, 443 | "dirty": None, 444 | "error": "unable to find root of source tree"} 445 | 446 | try: 447 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 448 | return render(pieces, cfg.style) 449 | except NotThisMethod: 450 | pass 451 | 452 | try: 453 | if cfg.parentdir_prefix: 454 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 455 | except NotThisMethod: 456 | pass 457 | 458 | return {"version": "0+unknown", "full-revisionid": None, 459 | "dirty": None, 460 | "error": "unable to compute version"} 461 | -------------------------------------------------------------------------------- /tests/test_typeconversion.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import pg8000 3 | import datetime 4 | import decimal 5 | import struct 6 | from connection_settings import db_connect 7 | from six import b, PY2, u 8 | import uuid 9 | import os 10 | import time 11 | from distutils.version import LooseVersion 12 | import sys 13 | import json 14 | import pytz 15 | 16 | IS_JYTHON = sys.platform.lower().count('java') > 0 17 | 18 | 19 | # Type conversion tests 20 | class Tests(unittest.TestCase): 21 | def setUp(self): 22 | self.db = pg8000.connect(**db_connect) 23 | self.cursor = self.db.cursor() 24 | 25 | def tearDown(self): 26 | self.cursor.close() 27 | self.cursor = None 28 | self.db.close() 29 | 30 | def testTimeRoundtrip(self): 31 | self.cursor.execute("SELECT %s as f1", (datetime.time(4, 5, 6),)) 32 | retval = self.cursor.fetchall() 33 | self.assertEqual(retval[0][0], datetime.time(4, 5, 6)) 34 | 35 | def testDateRoundtrip(self): 36 | v = datetime.date(2001, 2, 3) 37 | self.cursor.execute("SELECT %s as f1", (v,)) 38 | retval = self.cursor.fetchall() 39 | self.assertEqual(retval[0][0], v) 40 | 41 | def testBoolRoundtrip(self): 42 | self.cursor.execute("SELECT %s as f1", (True,)) 43 | retval = self.cursor.fetchall() 44 | self.assertEqual(retval[0][0], True) 45 | 46 | def testNullRoundtrip(self): 47 | # We can't just "SELECT %s" and set None as the parameter, since it has 48 | # no type. That would result in a PG error, "could not determine data 49 | # type of parameter %s". So we create a temporary table, insert null 50 | # values, and read them back. 51 | self.cursor.execute( 52 | "CREATE TEMPORARY TABLE TestNullWrite " 53 | "(f1 int4, f2 timestamp, f3 varchar)") 54 | self.cursor.execute( 55 | "INSERT INTO TestNullWrite VALUES (%s, %s, %s)", 56 | (None, None, None,)) 57 | self.cursor.execute("SELECT * FROM TestNullWrite") 58 | retval = self.cursor.fetchone() 59 | self.assertEqual(retval, [None, None, None]) 60 | 61 | def testNullSelectFailure(self): 62 | # See comment in TestNullRoundtrip. This test is here to ensure that 63 | # this behaviour is documented and doesn't mysteriously change. 64 | self.assertRaises( 65 | pg8000.ProgrammingError, self.cursor.execute, 66 | "SELECT %s as f1", (None,)) 67 | self.db.rollback() 68 | 69 | def testDecimalRoundtrip(self): 70 | values = ( 71 | "1.1", "-1.1", "10000", "20000", "-1000000000.123456789", "1.0", 72 | "12.44") 73 | for v in values: 74 | self.cursor.execute("SELECT %s as f1", (decimal.Decimal(v),)) 75 | retval = self.cursor.fetchall() 76 | self.assertEqual(str(retval[0][0]), v) 77 | 78 | def testFloatRoundtrip(self): 79 | # This test ensures that the binary float value doesn't change in a 80 | # roundtrip to the server. That could happen if the value was 81 | # converted to text and got rounded by a decimal place somewhere. 82 | val = 1.756e-12 83 | bin_orig = struct.pack("!d", val) 84 | self.cursor.execute("SELECT %s as f1", (val,)) 85 | retval = self.cursor.fetchall() 86 | bin_new = struct.pack("!d", retval[0][0]) 87 | self.assertEqual(bin_new, bin_orig) 88 | 89 | def test_float_plus_infinity_roundtrip(self): 90 | v = float('inf') 91 | self.cursor.execute("SELECT %s as f1", (v,)) 92 | retval = self.cursor.fetchall() 93 | self.assertEqual(retval[0][0], v) 94 | 95 | def testStrRoundtrip(self): 96 | v = "hello world" 97 | self.cursor.execute( 98 | "create temporary table test_str (f character varying(255))") 99 | self.cursor.execute("INSERT INTO test_str VALUES (%s)", (v,)) 100 | self.cursor.execute("SELECT * from test_str") 101 | retval = self.cursor.fetchall() 102 | self.assertEqual(retval[0][0], v) 103 | 104 | if PY2: 105 | v = "hello \xce\x94 world" 106 | self.cursor.execute("SELECT cast(%s as varchar) as f1", (v,)) 107 | retval = self.cursor.fetchall() 108 | self.assertEqual(retval[0][0], v.decode('utf8')) 109 | 110 | def test_str_then_int(self): 111 | v1 = "hello world" 112 | self.cursor.execute("SELECT cast(%s as varchar) as f1", (v1,)) 113 | retval = self.cursor.fetchall() 114 | self.assertEqual(retval[0][0], v1) 115 | 116 | v2 = 1 117 | self.cursor.execute("SELECT cast(%s as varchar) as f1", (v2,)) 118 | retval = self.cursor.fetchall() 119 | self.assertEqual(retval[0][0], str(v2)) 120 | 121 | def testUnicodeRoundtrip(self): 122 | v = u("hello \u0173 world") 123 | self.cursor.execute("SELECT cast(%s as varchar) as f1", (v,)) 124 | retval = self.cursor.fetchall() 125 | self.assertEqual(retval[0][0], v) 126 | 127 | def testLongRoundtrip(self): 128 | self.cursor.execute( 129 | "SELECT %s", (50000000000000,)) 130 | retval = self.cursor.fetchall() 131 | self.assertEqual(retval[0][0], 50000000000000) 132 | 133 | def testIntExecuteMany(self): 134 | self.cursor.executemany("SELECT %s", ((1,), (40000,))) 135 | self.cursor.fetchall() 136 | 137 | v = ([None], [4]) 138 | self.cursor.execute( 139 | "create temporary table test_int (f integer)") 140 | self.cursor.executemany("INSERT INTO test_int VALUES (%s)", v) 141 | self.cursor.execute("SELECT * from test_int") 142 | retval = self.cursor.fetchall() 143 | self.assertEqual(retval, v) 144 | 145 | def testIntRoundtrip(self): 146 | int2 = 21 147 | int4 = 23 148 | int8 = 20 149 | 150 | test_values = [ 151 | (0, int2), 152 | (-32767, int2), 153 | (-32768, int4), 154 | (+32767, int2), 155 | (+32768, int4), 156 | (-2147483647, int4), 157 | (-2147483648, int8), 158 | (+2147483647, int4), 159 | (+2147483648, int8), 160 | (-9223372036854775807, int8), 161 | (+9223372036854775807, int8)] 162 | 163 | for value, typoid in test_values: 164 | self.cursor.execute("SELECT %s", (value,)) 165 | retval = self.cursor.fetchall() 166 | self.assertEqual(retval[0][0], value) 167 | column_name, column_typeoid = self.cursor.description[0][0:2] 168 | self.assertEqual(column_typeoid, typoid) 169 | 170 | def testByteaRoundtrip(self): 171 | self.cursor.execute( 172 | "SELECT %s as f1", 173 | (pg8000.Binary(b("\x00\x01\x02\x03\x02\x01\x00")),)) 174 | retval = self.cursor.fetchall() 175 | self.assertEqual(retval[0][0], b("\x00\x01\x02\x03\x02\x01\x00")) 176 | 177 | def testTimestampRoundtrip(self): 178 | v = datetime.datetime(2001, 2, 3, 4, 5, 6, 170000) 179 | self.cursor.execute("SELECT %s as f1", (v,)) 180 | retval = self.cursor.fetchall() 181 | self.assertEqual(retval[0][0], v) 182 | 183 | # Test that time zone doesn't affect it 184 | # Jython 2.5.3 doesn't have a time.tzset() so skip 185 | if not IS_JYTHON: 186 | orig_tz = os.environ['TZ'] 187 | os.environ['TZ'] = "America/Edmonton" 188 | time.tzset() 189 | 190 | self.cursor.execute("SELECT %s as f1", (v,)) 191 | retval = self.cursor.fetchall() 192 | self.assertEqual(retval[0][0], v) 193 | 194 | os.environ['TZ'] = orig_tz 195 | time.tzset() 196 | 197 | def testIntervalRoundtrip(self): 198 | v = pg8000.Interval(microseconds=123456789, days=2, months=24) 199 | self.cursor.execute("SELECT %s as f1", (v,)) 200 | retval = self.cursor.fetchall() 201 | self.assertEqual(retval[0][0], v) 202 | 203 | v = datetime.timedelta(seconds=30) 204 | self.cursor.execute("SELECT %s as f1", (v,)) 205 | retval = self.cursor.fetchall() 206 | self.assertEqual(retval[0][0], v) 207 | 208 | def testEnumRoundtrip(self): 209 | try: 210 | self.cursor.execute( 211 | "create type lepton as enum ('electron', 'muon', 'tau')") 212 | except pg8000.ProgrammingError: 213 | self.db.rollback() 214 | 215 | v = 'muon' 216 | self.cursor.execute("SELECT cast(%s as lepton) as f1", (v,)) 217 | retval = self.cursor.fetchall() 218 | self.assertEqual(retval[0][0], v) 219 | self.cursor.execute( 220 | "CREATE TEMPORARY TABLE testenum " 221 | "(f1 lepton)") 222 | self.cursor.execute("INSERT INTO testenum VALUES (%s)", ('electron',)) 223 | self.cursor.execute("drop table testenum") 224 | self.cursor.execute("drop type lepton") 225 | self.db.commit() 226 | 227 | def testXmlRoundtrip(self): 228 | v = 'gatccgagtac' 229 | self.cursor.execute("select xmlparse(content %s) as f1", (v,)) 230 | retval = self.cursor.fetchall() 231 | self.assertEqual(retval[0][0], v) 232 | 233 | def testUuidRoundtrip(self): 234 | v = uuid.UUID('911460f2-1f43-fea2-3e2c-e01fd5b5069d') 235 | self.cursor.execute("select %s as f1", (v,)) 236 | retval = self.cursor.fetchall() 237 | self.assertEqual(retval[0][0], v) 238 | 239 | def testInetRoundtrip(self): 240 | try: 241 | import ipaddress 242 | 243 | v = ipaddress.ip_network('192.168.0.0/28') 244 | self.cursor.execute("select %s as f1", (v,)) 245 | retval = self.cursor.fetchall() 246 | self.assertEqual(retval[0][0], v) 247 | 248 | v = ipaddress.ip_address('192.168.0.1') 249 | self.cursor.execute("select %s as f1", (v,)) 250 | retval = self.cursor.fetchall() 251 | self.assertEqual(retval[0][0], v) 252 | 253 | except ImportError: 254 | for v in ('192.168.100.128/25', '192.168.0.1'): 255 | self.cursor.execute( 256 | "select cast(cast(%s as varchar) as inet) as f1", (v,)) 257 | retval = self.cursor.fetchall() 258 | self.assertEqual(retval[0][0], v) 259 | 260 | def testXidRoundtrip(self): 261 | v = 86722 262 | self.cursor.execute( 263 | "select cast(cast(%s as varchar) as xid) as f1", (v,)) 264 | retval = self.cursor.fetchall() 265 | self.assertEqual(retval[0][0], v) 266 | 267 | # Should complete without an exception 268 | self.cursor.execute( 269 | "select * from pg_locks where transactionid = %s", (97712,)) 270 | retval = self.cursor.fetchall() 271 | 272 | def testInt2VectorIn(self): 273 | self.cursor.execute("select cast('1 2' as int2vector) as f1") 274 | retval = self.cursor.fetchall() 275 | self.assertEqual(retval[0][0], [1, 2]) 276 | 277 | # Should complete without an exception 278 | self.cursor.execute("select indkey from pg_index") 279 | retval = self.cursor.fetchall() 280 | 281 | def testTimestampTzOut(self): 282 | self.cursor.execute( 283 | "SELECT '2001-02-03 04:05:06.17 America/Edmonton'" 284 | "::timestamp with time zone") 285 | retval = self.cursor.fetchall() 286 | dt = retval[0][0] 287 | self.assertEqual(dt.tzinfo is not None, True, "no tzinfo returned") 288 | self.assertEqual( 289 | dt.astimezone(pg8000.utc), 290 | datetime.datetime(2001, 2, 3, 11, 5, 6, 170000, pg8000.utc), 291 | "retrieved value match failed") 292 | 293 | def testTimestampTzRoundtrip(self): 294 | if not IS_JYTHON: 295 | mst = pytz.timezone("America/Edmonton") 296 | v1 = mst.localize(datetime.datetime(2001, 2, 3, 4, 5, 6, 170000)) 297 | self.cursor.execute("SELECT %s as f1", (v1,)) 298 | retval = self.cursor.fetchall() 299 | v2 = retval[0][0] 300 | self.assertNotEqual(v2.tzinfo, None) 301 | self.assertEqual(v1, v2) 302 | 303 | def testTimestampMismatch(self): 304 | if not IS_JYTHON: 305 | mst = pytz.timezone("America/Edmonton") 306 | self.cursor.execute("SET SESSION TIME ZONE 'America/Edmonton'") 307 | try: 308 | self.cursor.execute( 309 | "CREATE TEMPORARY TABLE TestTz " 310 | "(f1 timestamp with time zone, " 311 | "f2 timestamp without time zone)") 312 | self.cursor.execute( 313 | "INSERT INTO TestTz (f1, f2) VALUES (%s, %s)", ( 314 | # insert timestamp into timestamptz field (v1) 315 | datetime.datetime(2001, 2, 3, 4, 5, 6, 170000), 316 | # insert timestamptz into timestamp field (v2) 317 | mst.localize(datetime.datetime( 318 | 2001, 2, 3, 4, 5, 6, 170000)))) 319 | self.cursor.execute("SELECT f1, f2 FROM TestTz") 320 | retval = self.cursor.fetchall() 321 | 322 | # when inserting a timestamp into a timestamptz field, 323 | # postgresql assumes that it is in local time. So the value 324 | # that comes out will be the server's local time interpretation 325 | # of v1. We've set the server's TZ to MST, the time should 326 | # be... 327 | f1 = retval[0][0] 328 | self.assertEqual( 329 | f1, datetime.datetime( 330 | 2001, 2, 3, 11, 5, 6, 170000, pytz.utc)) 331 | 332 | # inserting the timestamptz into a timestamp field, pg8000 333 | # converts the value into UTC, and then the PG server converts 334 | # it into local time for insertion into the field. When we 335 | # query for it, we get the same time back, like the tz was 336 | # dropped. 337 | f2 = retval[0][1] 338 | self.assertEqual( 339 | f2, datetime.datetime(2001, 2, 3, 4, 5, 6, 170000)) 340 | finally: 341 | self.cursor.execute("SET SESSION TIME ZONE DEFAULT") 342 | 343 | def testNameOut(self): 344 | # select a field that is of "name" type: 345 | self.cursor.execute("SELECT usename FROM pg_user") 346 | self.cursor.fetchall() 347 | # It is sufficient that no errors were encountered. 348 | 349 | def testOidOut(self): 350 | self.cursor.execute("SELECT oid FROM pg_type") 351 | self.cursor.fetchall() 352 | # It is sufficient that no errors were encountered. 353 | 354 | def testBooleanOut(self): 355 | self.cursor.execute("SELECT cast('t' as bool)") 356 | retval = self.cursor.fetchall() 357 | self.assertTrue(retval[0][0]) 358 | 359 | def testNumericOut(self): 360 | for num in ('5000', '50.34'): 361 | self.cursor.execute("SELECT " + num + "::numeric") 362 | retval = self.cursor.fetchall() 363 | self.assertEqual(str(retval[0][0]), num) 364 | 365 | def testInt2Out(self): 366 | self.cursor.execute("SELECT 5000::smallint") 367 | retval = self.cursor.fetchall() 368 | self.assertEqual(retval[0][0], 5000) 369 | 370 | def testInt4Out(self): 371 | self.cursor.execute("SELECT 5000::integer") 372 | retval = self.cursor.fetchall() 373 | self.assertEqual(retval[0][0], 5000) 374 | 375 | def testInt8Out(self): 376 | self.cursor.execute("SELECT 50000000000000::bigint") 377 | retval = self.cursor.fetchall() 378 | self.assertEqual(retval[0][0], 50000000000000) 379 | 380 | def testFloat4Out(self): 381 | self.cursor.execute("SELECT 1.1::real") 382 | retval = self.cursor.fetchall() 383 | self.assertEqual(retval[0][0], 1.1000000238418579) 384 | 385 | def testFloat8Out(self): 386 | self.cursor.execute("SELECT 1.1::double precision") 387 | retval = self.cursor.fetchall() 388 | self.assertEqual(retval[0][0], 1.1000000000000001) 389 | 390 | def testVarcharOut(self): 391 | self.cursor.execute("SELECT 'hello'::varchar(20)") 392 | retval = self.cursor.fetchall() 393 | self.assertEqual(retval[0][0], "hello") 394 | 395 | def testCharOut(self): 396 | self.cursor.execute("SELECT 'hello'::char(20)") 397 | retval = self.cursor.fetchall() 398 | self.assertEqual(retval[0][0], "hello ") 399 | 400 | def testTextOut(self): 401 | self.cursor.execute("SELECT 'hello'::text") 402 | retval = self.cursor.fetchall() 403 | self.assertEqual(retval[0][0], "hello") 404 | 405 | def testIntervalOut(self): 406 | self.cursor.execute( 407 | "SELECT '1 month 16 days 12 hours 32 minutes 64 seconds'" 408 | "::interval") 409 | retval = self.cursor.fetchall() 410 | expected_value = pg8000.Interval( 411 | microseconds=(12 * 60 * 60 * 1000 * 1000) + 412 | (32 * 60 * 1000 * 1000) + (64 * 1000 * 1000), 413 | days=16, months=1) 414 | self.assertEqual(retval[0][0], expected_value) 415 | 416 | self.cursor.execute("select interval '30 seconds'") 417 | retval = self.cursor.fetchall() 418 | expected_value = datetime.timedelta(seconds=30) 419 | self.assertEqual(retval[0][0], expected_value) 420 | 421 | self.cursor.execute("select interval '12 days 30 seconds'") 422 | retval = self.cursor.fetchall() 423 | expected_value = datetime.timedelta(days=12, seconds=30) 424 | self.assertEqual(retval[0][0], expected_value) 425 | 426 | def testTimestampOut(self): 427 | self.cursor.execute("SELECT '2001-02-03 04:05:06.17'::timestamp") 428 | retval = self.cursor.fetchall() 429 | self.assertEqual( 430 | retval[0][0], datetime.datetime(2001, 2, 3, 4, 5, 6, 170000)) 431 | 432 | # confirms that pg8000's binary output methods have the same output for 433 | # a data type as the PG server 434 | def testBinaryOutputMethods(self): 435 | methods = ( 436 | ("float8send", 22.2), 437 | ("timestamp_send", datetime.datetime(2001, 2, 3, 4, 5, 6, 789)), 438 | ("byteasend", pg8000.Binary(b("\x01\x02"))), 439 | ("interval_send", pg8000.Interval(1234567, 123, 123)),) 440 | for method_out, value in methods: 441 | self.cursor.execute("SELECT %s(%%s) as f1" % method_out, (value,)) 442 | retval = self.cursor.fetchall() 443 | self.assertEqual( 444 | retval[0][0], self.db.make_params((value,))[0][2](value)) 445 | 446 | def testInt4ArrayOut(self): 447 | self.cursor.execute( 448 | "SELECT '{1,2,3,4}'::INT[] AS f1, " 449 | "'{{1,2,3},{4,5,6}}'::INT[][] AS f2, " 450 | "'{{{1,2},{3,4}},{{NULL,6},{7,8}}}'::INT[][][] AS f3") 451 | f1, f2, f3 = self.cursor.fetchone() 452 | self.assertEqual(f1, [1, 2, 3, 4]) 453 | self.assertEqual(f2, [[1, 2, 3], [4, 5, 6]]) 454 | self.assertEqual(f3, [[[1, 2], [3, 4]], [[None, 6], [7, 8]]]) 455 | 456 | def testInt2ArrayOut(self): 457 | self.cursor.execute( 458 | "SELECT '{1,2,3,4}'::INT2[] AS f1, " 459 | "'{{1,2,3},{4,5,6}}'::INT2[][] AS f2, " 460 | "'{{{1,2},{3,4}},{{NULL,6},{7,8}}}'::INT2[][][] AS f3") 461 | f1, f2, f3 = self.cursor.fetchone() 462 | self.assertEqual(f1, [1, 2, 3, 4]) 463 | self.assertEqual(f2, [[1, 2, 3], [4, 5, 6]]) 464 | self.assertEqual(f3, [[[1, 2], [3, 4]], [[None, 6], [7, 8]]]) 465 | 466 | def testInt8ArrayOut(self): 467 | self.cursor.execute( 468 | "SELECT '{1,2,3,4}'::INT8[] AS f1, " 469 | "'{{1,2,3},{4,5,6}}'::INT8[][] AS f2, " 470 | "'{{{1,2},{3,4}},{{NULL,6},{7,8}}}'::INT8[][][] AS f3") 471 | f1, f2, f3 = self.cursor.fetchone() 472 | self.assertEqual(f1, [1, 2, 3, 4]) 473 | self.assertEqual(f2, [[1, 2, 3], [4, 5, 6]]) 474 | self.assertEqual(f3, [[[1, 2], [3, 4]], [[None, 6], [7, 8]]]) 475 | 476 | def testBoolArrayOut(self): 477 | self.cursor.execute( 478 | "SELECT '{TRUE,FALSE,FALSE,TRUE}'::BOOL[] AS f1, " 479 | "'{{TRUE,FALSE,TRUE},{FALSE,TRUE,FALSE}}'::BOOL[][] AS f2, " 480 | "'{{{TRUE,FALSE},{FALSE,TRUE}},{{NULL,TRUE},{FALSE,FALSE}}}'" 481 | "::BOOL[][][] AS f3") 482 | f1, f2, f3 = self.cursor.fetchone() 483 | self.assertEqual(f1, [True, False, False, True]) 484 | self.assertEqual(f2, [[True, False, True], [False, True, False]]) 485 | self.assertEqual( 486 | f3, 487 | [[[True, False], [False, True]], [[None, True], [False, False]]]) 488 | 489 | def testFloat4ArrayOut(self): 490 | self.cursor.execute( 491 | "SELECT '{1,2,3,4}'::FLOAT4[] AS f1, " 492 | "'{{1,2,3},{4,5,6}}'::FLOAT4[][] AS f2, " 493 | "'{{{1,2},{3,4}},{{NULL,6},{7,8}}}'::FLOAT4[][][] AS f3") 494 | f1, f2, f3 = self.cursor.fetchone() 495 | self.assertEqual(f1, [1, 2, 3, 4]) 496 | self.assertEqual(f2, [[1, 2, 3], [4, 5, 6]]) 497 | self.assertEqual(f3, [[[1, 2], [3, 4]], [[None, 6], [7, 8]]]) 498 | 499 | def testFloat8ArrayOut(self): 500 | self.cursor.execute( 501 | "SELECT '{1,2,3,4}'::FLOAT8[] AS f1, " 502 | "'{{1,2,3},{4,5,6}}'::FLOAT8[][] AS f2, " 503 | "'{{{1,2},{3,4}},{{NULL,6},{7,8}}}'::FLOAT8[][][] AS f3") 504 | f1, f2, f3 = self.cursor.fetchone() 505 | self.assertEqual(f1, [1, 2, 3, 4]) 506 | self.assertEqual(f2, [[1, 2, 3], [4, 5, 6]]) 507 | self.assertEqual(f3, [[[1, 2], [3, 4]], [[None, 6], [7, 8]]]) 508 | 509 | def testIntArrayRoundtrip(self): 510 | # send small int array, should be sent as INT2[] 511 | self.cursor.execute("SELECT %s as f1", ([1, 2, 3],)) 512 | retval = self.cursor.fetchall() 513 | self.assertEqual(retval[0][0], [1, 2, 3]) 514 | column_name, column_typeoid = self.cursor.description[0][0:2] 515 | self.assertEqual(column_typeoid, 1005, "type should be INT2[]") 516 | 517 | # test multi-dimensional array, should be sent as INT2[] 518 | self.cursor.execute("SELECT %s as f1", ([[1, 2], [3, 4]],)) 519 | retval = self.cursor.fetchall() 520 | self.assertEqual(retval[0][0], [[1, 2], [3, 4]]) 521 | 522 | column_name, column_typeoid = self.cursor.description[0][0:2] 523 | self.assertEqual(column_typeoid, 1005, "type should be INT2[]") 524 | 525 | # a larger value should kick it up to INT4[]... 526 | self.cursor.execute("SELECT %s as f1 -- integer[]", ([70000, 2, 3],)) 527 | retval = self.cursor.fetchall() 528 | self.assertEqual(retval[0][0], [70000, 2, 3]) 529 | column_name, column_typeoid = self.cursor.description[0][0:2] 530 | self.assertEqual(column_typeoid, 1007, "type should be INT4[]") 531 | 532 | # a much larger value should kick it up to INT8[]... 533 | self.cursor.execute( 534 | "SELECT %s as f1 -- bigint[]", ([7000000000, 2, 3],)) 535 | retval = self.cursor.fetchall() 536 | self.assertEqual( 537 | retval[0][0], [7000000000, 2, 3], 538 | "retrieved value match failed") 539 | column_name, column_typeoid = self.cursor.description[0][0:2] 540 | self.assertEqual(column_typeoid, 1016, "type should be INT8[]") 541 | 542 | def testIntArrayWithNullRoundtrip(self): 543 | self.cursor.execute("SELECT %s as f1", ([1, None, 3],)) 544 | retval = self.cursor.fetchall() 545 | self.assertEqual(retval[0][0], [1, None, 3]) 546 | 547 | def testFloatArrayRoundtrip(self): 548 | self.cursor.execute("SELECT %s as f1", ([1.1, 2.2, 3.3],)) 549 | retval = self.cursor.fetchall() 550 | self.assertEqual(retval[0][0], [1.1, 2.2, 3.3]) 551 | 552 | def testBoolArrayRoundtrip(self): 553 | self.cursor.execute("SELECT %s as f1", ([True, False, None],)) 554 | retval = self.cursor.fetchall() 555 | self.assertEqual(retval[0][0], [True, False, None]) 556 | 557 | def testStringArrayOut(self): 558 | self.cursor.execute("SELECT '{a,b,c}'::TEXT[] AS f1") 559 | self.assertEqual(self.cursor.fetchone()[0], ["a", "b", "c"]) 560 | self.cursor.execute("SELECT '{a,b,c}'::CHAR[] AS f1") 561 | self.assertEqual(self.cursor.fetchone()[0], ["a", "b", "c"]) 562 | self.cursor.execute("SELECT '{a,b,c}'::VARCHAR[] AS f1") 563 | self.assertEqual(self.cursor.fetchone()[0], ["a", "b", "c"]) 564 | self.cursor.execute("SELECT '{a,b,c}'::CSTRING[] AS f1") 565 | self.assertEqual(self.cursor.fetchone()[0], ["a", "b", "c"]) 566 | self.cursor.execute("SELECT '{a,b,c}'::NAME[] AS f1") 567 | self.assertEqual(self.cursor.fetchone()[0], ["a", "b", "c"]) 568 | self.cursor.execute("SELECT '{}'::text[];") 569 | self.assertEqual(self.cursor.fetchone()[0], []) 570 | self.cursor.execute("SELECT '{NULL,\"NULL\",NULL,\"\"}'::text[];") 571 | self.assertEqual(self.cursor.fetchone()[0], [None, 'NULL', None, ""]) 572 | 573 | def testNumericArrayOut(self): 574 | self.cursor.execute("SELECT '{1.1,2.2,3.3}'::numeric[] AS f1") 575 | self.assertEqual( 576 | self.cursor.fetchone()[0], [ 577 | decimal.Decimal("1.1"), decimal.Decimal("2.2"), 578 | decimal.Decimal("3.3")]) 579 | 580 | def testNumericArrayRoundtrip(self): 581 | v = [decimal.Decimal("1.1"), None, decimal.Decimal("3.3")] 582 | self.cursor.execute("SELECT %s as f1", (v,)) 583 | retval = self.cursor.fetchall() 584 | self.assertEqual(retval[0][0], v) 585 | 586 | def testStringArrayRoundtrip(self): 587 | v = ["Hello!", "World!", "abcdefghijklmnopqrstuvwxyz", "", 588 | "A bunch of random characters:", 589 | " ~!@#$%^&*()_+`1234567890-=[]\\{}|{;':\",./<>?\t", 590 | None] 591 | self.cursor.execute("SELECT %s as f1", (v,)) 592 | retval = self.cursor.fetchall() 593 | self.assertEqual(retval[0][0], v) 594 | 595 | def testUnicodeArrayRoundtrip(self): 596 | if PY2: 597 | v = map(unicode, ("Second", "To", None)) # noqa 598 | self.cursor.execute("SELECT %s as f1", (v,)) 599 | retval = self.cursor.fetchall() 600 | self.assertEqual(retval[0][0], v) 601 | 602 | def testEmptyArray(self): 603 | v = [] 604 | self.cursor.execute("SELECT %s as f1", (v,)) 605 | retval = self.cursor.fetchall() 606 | self.assertEqual(retval[0][0], v) 607 | 608 | def testArrayContentNotSupported(self): 609 | class Kajigger(object): 610 | pass 611 | self.assertRaises( 612 | pg8000.ArrayContentNotSupportedError, 613 | self.db.array_inspect, [[Kajigger()], [None], [None]]) 614 | self.db.rollback() 615 | 616 | def testArrayDimensions(self): 617 | for arr in ( 618 | [1, [2]], [[1], [2], [3, 4]], 619 | [[[1]], [[2]], [[3, 4]]], 620 | [[[1]], [[2]], [[3, 4]]], 621 | [[[[1]]], [[[2]]], [[[3, 4]]]], 622 | [[1, 2, 3], [4, [5], 6]]): 623 | 624 | arr_send = self.db.array_inspect(arr)[2] 625 | self.assertRaises( 626 | pg8000.ArrayDimensionsNotConsistentError, arr_send, arr) 627 | self.db.rollback() 628 | 629 | def testArrayHomogenous(self): 630 | arr = [[[1]], [[2]], [[3.1]]] 631 | arr_send = self.db.array_inspect(arr)[2] 632 | self.assertRaises( 633 | pg8000.ArrayContentNotHomogenousError, arr_send, arr) 634 | self.db.rollback() 635 | 636 | def testArrayInspect(self): 637 | self.db.array_inspect([1, 2, 3]) 638 | self.db.array_inspect([[1], [2], [3]]) 639 | self.db.array_inspect([[[1]], [[2]], [[3]]]) 640 | 641 | def testMacaddr(self): 642 | self.cursor.execute("SELECT macaddr '08002b:010203'") 643 | retval = self.cursor.fetchall() 644 | self.assertEqual(retval[0][0], "08:00:2b:01:02:03") 645 | 646 | def testTsvectorRoundtrip(self): 647 | self.cursor.execute( 648 | "SELECT cast(%s as tsvector)", 649 | ('a fat cat sat on a mat and ate a fat rat',)) 650 | retval = self.cursor.fetchall() 651 | self.assertEqual( 652 | retval[0][0], "'a' 'and' 'ate' 'cat' 'fat' 'mat' 'on' 'rat' 'sat'") 653 | 654 | def testHstoreRoundtrip(self): 655 | val = '"a"=>"1"' 656 | self.cursor.execute("SELECT cast(%s as hstore)", (val,)) 657 | retval = self.cursor.fetchall() 658 | self.assertEqual(retval[0][0], val) 659 | 660 | def testJsonRoundtrip(self): 661 | if self.db._server_version >= LooseVersion('9.2'): 662 | val = {'name': 'Apollo 11 Cave', 'zebra': True, 'age': 26.003} 663 | self.cursor.execute( 664 | "SELECT cast(%s as json)", (json.dumps(val),)) 665 | retval = self.cursor.fetchall() 666 | self.assertEqual(retval[0][0], val) 667 | 668 | def testJsonbRoundtrip(self): 669 | if self.db._server_version >= LooseVersion('9.4'): 670 | val = {'name': 'Apollo 11 Cave', 'zebra': True, 'age': 26.003} 671 | self.cursor.execute( 672 | "SELECT cast(%s as jsonb)", (json.dumps(val),)) 673 | retval = self.cursor.fetchall() 674 | self.assertEqual(retval[0][0], val) 675 | 676 | def test_json_access_object(self): 677 | if self.db._server_version >= LooseVersion('9.4'): 678 | val = {'name': 'Apollo 11 Cave', 'zebra': True, 'age': 26.003} 679 | self.cursor.execute( 680 | "SELECT cast(%s as json) -> %s", (json.dumps(val), 'name')) 681 | retval = self.cursor.fetchall() 682 | self.assertEqual(retval[0][0], 'Apollo 11 Cave') 683 | 684 | def test_jsonb_access_object(self): 685 | if self.db._server_version >= LooseVersion('9.4'): 686 | val = {'name': 'Apollo 11 Cave', 'zebra': True, 'age': 26.003} 687 | self.cursor.execute( 688 | "SELECT cast(%s as jsonb) -> %s", (json.dumps(val), 'name')) 689 | retval = self.cursor.fetchall() 690 | self.assertEqual(retval[0][0], 'Apollo 11 Cave') 691 | 692 | def test_json_access_array(self): 693 | if self.db._server_version >= LooseVersion('9.4'): 694 | val = [-1, -2, -3, -4, -5] 695 | self.cursor.execute( 696 | "SELECT cast(%s as json) -> %s", (json.dumps(val), 2)) 697 | retval = self.cursor.fetchall() 698 | self.assertEqual(retval[0][0], -3) 699 | 700 | def testJsonbAccessArray(self): 701 | if self.db._server_version >= LooseVersion('9.4'): 702 | val = [-1, -2, -3, -4, -5] 703 | self.cursor.execute( 704 | "SELECT cast(%s as jsonb) -> %s", (json.dumps(val), 2)) 705 | retval = self.cursor.fetchall() 706 | self.assertEqual(retval[0][0], -3) 707 | 708 | def test_timestamp_send_float(self): 709 | assert b('A\xbe\x19\xcf\x80\x00\x00\x00') == \ 710 | pg8000.core.timestamp_send_float( 711 | datetime.datetime(2016, 1, 2, 0, 0)) 712 | 713 | def test_infinity_timestamp_roundtrip(self): 714 | v = 'infinity' 715 | self.cursor.execute("SELECT cast(%s as timestamp) as f1", (v,)) 716 | retval = self.cursor.fetchall() 717 | self.assertEqual(retval[0][0], v) 718 | 719 | 720 | if __name__ == "__main__": 721 | unittest.main() 722 | -------------------------------------------------------------------------------- /tests/dbapi20.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest 3 | import time 4 | import warnings 5 | from six import b 6 | ''' Python DB API 2.0 driver compliance unit test suite. 7 | 8 | This software is Public Domain and may be used without restrictions. 9 | 10 | "Now we have booze and barflies entering the discussion, plus rumours of 11 | DBAs on drugs... and I won't tell you what flashes through my mind each 12 | time I read the subject line with 'Anal Compliance' in it. All around 13 | this is turning out to be a thoroughly unwholesome unit test." 14 | 15 | -- Ian Bicking 16 | ''' 17 | 18 | __rcs_id__ = '$Id: dbapi20.py,v 1.10 2003/10/09 03:14:14 zenzen Exp $' 19 | __version__ = '$Revision: 1.10 $'[11:-2] 20 | __author__ = 'Stuart Bishop ' 21 | 22 | 23 | # $Log: dbapi20.py,v $ 24 | # Revision 1.10 2003/10/09 03:14:14 zenzen 25 | # Add test for DB API 2.0 optional extension, where database exceptions 26 | # are exposed as attributes on the Connection object. 27 | # 28 | # Revision 1.9 2003/08/13 01:16:36 zenzen 29 | # Minor tweak from Stefan Fleiter 30 | # 31 | # Revision 1.8 2003/04/10 00:13:25 zenzen 32 | # Changes, as per suggestions by M.-A. Lemburg 33 | # - Add a table prefix, to ensure namespace collisions can always be avoided 34 | # 35 | # Revision 1.7 2003/02/26 23:33:37 zenzen 36 | # Break out DDL into helper functions, as per request by David Rushby 37 | # 38 | # Revision 1.6 2003/02/21 03:04:33 zenzen 39 | # Stuff from Henrik Ekelund: 40 | # added test_None 41 | # added test_nextset & hooks 42 | # 43 | # Revision 1.5 2003/02/17 22:08:43 zenzen 44 | # Implement suggestions and code from Henrik Eklund - test that 45 | # cursor.arraysize defaults to 1 & generic cursor.callproc test added 46 | # 47 | # Revision 1.4 2003/02/15 00:16:33 zenzen 48 | # Changes, as per suggestions and bug reports by M.-A. Lemburg, 49 | # Matthew T. Kromer, Federico Di Gregorio and Daniel Dittmar 50 | # - Class renamed 51 | # - Now a subclass of TestCase, to avoid requiring the driver stub 52 | # to use multiple inheritance 53 | # - Reversed the polarity of buggy test in test_description 54 | # - Test exception heirarchy correctly 55 | # - self.populate is now self._populate(), so if a driver stub 56 | # overrides self.ddl1 this change propogates 57 | # - VARCHAR columns now have a width, which will hopefully make the 58 | # DDL even more portible (this will be reversed if it causes more problems) 59 | # - cursor.rowcount being checked after various execute and fetchXXX methods 60 | # - Check for fetchall and fetchmany returning empty lists after results 61 | # are exhausted (already checking for empty lists if select retrieved 62 | # nothing 63 | # - Fix bugs in test_setoutputsize_basic and test_setinputsizes 64 | # 65 | 66 | 67 | class DatabaseAPI20Test(unittest.TestCase): 68 | ''' Test a database self.driver for DB API 2.0 compatibility. 69 | This implementation tests Gadfly, but the TestCase 70 | is structured so that other self.drivers can subclass this 71 | test case to ensure compiliance with the DB-API. It is 72 | expected that this TestCase may be expanded in the future 73 | if ambiguities or edge conditions are discovered. 74 | 75 | The 'Optional Extensions' are not yet being tested. 76 | 77 | self.drivers should subclass this test, overriding setUp, tearDown, 78 | self.driver, connect_args and connect_kw_args. Class specification 79 | should be as follows: 80 | 81 | import dbapi20 82 | class mytest(dbapi20.DatabaseAPI20Test): 83 | [...] 84 | 85 | Don't 'import DatabaseAPI20Test from dbapi20', or you will 86 | confuse the unit tester - just 'import dbapi20'. 87 | ''' 88 | 89 | # The self.driver module. This should be the module where the 'connect' 90 | # method is to be found 91 | driver = None 92 | connect_args = () # List of arguments to pass to connect 93 | connect_kw_args = {} # Keyword arguments for connect 94 | table_prefix = 'dbapi20test_' # If you need to specify a prefix for tables 95 | 96 | ddl1 = 'create table %sbooze (name varchar(20))' % table_prefix 97 | ddl2 = 'create table %sbarflys (name varchar(20))' % table_prefix 98 | xddl1 = 'drop table %sbooze' % table_prefix 99 | xddl2 = 'drop table %sbarflys' % table_prefix 100 | 101 | # Name of stored procedure to convert 102 | # string->lowercase 103 | lowerfunc = 'lower' 104 | 105 | # Some drivers may need to override these helpers, for example adding 106 | # a 'commit' after the execute. 107 | def executeDDL1(self, cursor): 108 | cursor.execute(self.ddl1) 109 | 110 | def executeDDL2(self, cursor): 111 | cursor.execute(self.ddl2) 112 | 113 | def setUp(self): 114 | ''' self.drivers should override this method to perform required setup 115 | if any is necessary, such as creating the database. 116 | ''' 117 | pass 118 | 119 | def tearDown(self): 120 | ''' self.drivers should override this method to perform required 121 | cleanup if any is necessary, such as deleting the test database. 122 | The default drops the tables that may be created. 123 | ''' 124 | con = self._connect() 125 | try: 126 | cur = con.cursor() 127 | for ddl in (self.xddl1, self.xddl2): 128 | try: 129 | cur.execute(ddl) 130 | con.commit() 131 | except self.driver.Error: 132 | # Assume table didn't exist. Other tests will check if 133 | # execute is busted. 134 | pass 135 | finally: 136 | con.close() 137 | 138 | def _connect(self): 139 | try: 140 | return self.driver.connect( 141 | *self.connect_args, **self.connect_kw_args) 142 | except AttributeError: 143 | self.fail("No connect method found in self.driver module") 144 | 145 | def test_connect(self): 146 | con = self._connect() 147 | con.close() 148 | 149 | def test_apilevel(self): 150 | try: 151 | # Must exist 152 | apilevel = self.driver.apilevel 153 | # Must equal 2.0 154 | self.assertEqual(apilevel, '2.0') 155 | except AttributeError: 156 | self.fail("Driver doesn't define apilevel") 157 | 158 | def test_threadsafety(self): 159 | try: 160 | # Must exist 161 | threadsafety = self.driver.threadsafety 162 | # Must be a valid value 163 | self.assertEqual(threadsafety in (0, 1, 2, 3), True) 164 | except AttributeError: 165 | self.fail("Driver doesn't define threadsafety") 166 | 167 | def test_paramstyle(self): 168 | try: 169 | # Must exist 170 | paramstyle = self.driver.paramstyle 171 | # Must be a valid value 172 | self.assertEqual( 173 | paramstyle in ( 174 | 'qmark', 'numeric', 'named', 'format', 'pyformat'), True) 175 | except AttributeError: 176 | self.fail("Driver doesn't define paramstyle") 177 | 178 | def test_Exceptions(self): 179 | # Make sure required exceptions exist, and are in the 180 | # defined heirarchy. 181 | self.assertEqual(issubclass(self.driver.Warning, Exception), True) 182 | self.assertEqual(issubclass(self.driver.Error, Exception), True) 183 | self.assertEqual( 184 | issubclass(self.driver.InterfaceError, self.driver.Error), True) 185 | self.assertEqual( 186 | issubclass(self.driver.DatabaseError, self.driver.Error), True) 187 | self.assertEqual( 188 | issubclass(self.driver.OperationalError, self.driver.Error), True) 189 | self.assertEqual( 190 | issubclass(self.driver.IntegrityError, self.driver.Error), True) 191 | self.assertEqual( 192 | issubclass(self.driver.InternalError, self.driver.Error), True) 193 | self.assertEqual( 194 | issubclass(self.driver.ProgrammingError, self.driver.Error), True) 195 | self.assertEqual( 196 | issubclass(self.driver.NotSupportedError, self.driver.Error), True) 197 | 198 | def test_ExceptionsAsConnectionAttributes(self): 199 | # OPTIONAL EXTENSION 200 | # Test for the optional DB API 2.0 extension, where the exceptions 201 | # are exposed as attributes on the Connection object 202 | # I figure this optional extension will be implemented by any 203 | # driver author who is using this test suite, so it is enabled 204 | # by default. 205 | warnings.simplefilter("ignore") 206 | con = self._connect() 207 | drv = self.driver 208 | self.assertEqual(con.Warning is drv.Warning, True) 209 | self.assertEqual(con.Error is drv.Error, True) 210 | self.assertEqual(con.InterfaceError is drv.InterfaceError, True) 211 | self.assertEqual(con.DatabaseError is drv.DatabaseError, True) 212 | self.assertEqual(con.OperationalError is drv.OperationalError, True) 213 | self.assertEqual(con.IntegrityError is drv.IntegrityError, True) 214 | self.assertEqual(con.InternalError is drv.InternalError, True) 215 | self.assertEqual(con.ProgrammingError is drv.ProgrammingError, True) 216 | self.assertEqual(con.NotSupportedError is drv.NotSupportedError, True) 217 | warnings.resetwarnings() 218 | con.close() 219 | 220 | def test_commit(self): 221 | con = self._connect() 222 | try: 223 | # Commit must work, even if it doesn't do anything 224 | con.commit() 225 | finally: 226 | con.close() 227 | 228 | def test_rollback(self): 229 | con = self._connect() 230 | # If rollback is defined, it should either work or throw 231 | # the documented exception 232 | if hasattr(con, 'rollback'): 233 | try: 234 | con.rollback() 235 | except self.driver.NotSupportedError: 236 | pass 237 | con.close() 238 | 239 | def test_cursor(self): 240 | con = self._connect() 241 | try: 242 | con.cursor() 243 | finally: 244 | con.close() 245 | 246 | def test_cursor_isolation(self): 247 | con = self._connect() 248 | try: 249 | # Make sure cursors created from the same connection have 250 | # the documented transaction isolation level 251 | cur1 = con.cursor() 252 | cur2 = con.cursor() 253 | self.executeDDL1(cur1) 254 | cur1.execute( 255 | "insert into %sbooze values ('Victoria Bitter')" % ( 256 | self.table_prefix)) 257 | cur2.execute("select name from %sbooze" % self.table_prefix) 258 | booze = cur2.fetchall() 259 | self.assertEqual(len(booze), 1) 260 | self.assertEqual(len(booze[0]), 1) 261 | self.assertEqual(booze[0][0], 'Victoria Bitter') 262 | finally: 263 | con.close() 264 | 265 | def test_description(self): 266 | con = self._connect() 267 | try: 268 | cur = con.cursor() 269 | self.executeDDL1(cur) 270 | self.assertEqual( 271 | cur.description, None, 272 | 'cursor.description should be none after executing a ' 273 | 'statement that can return no rows (such as DDL)') 274 | cur.execute('select name from %sbooze' % self.table_prefix) 275 | self.assertEqual( 276 | len(cur.description), 1, 277 | 'cursor.description describes too many columns') 278 | self.assertEqual( 279 | len(cur.description[0]), 7, 280 | 'cursor.description[x] tuples must have 7 elements') 281 | self.assertEqual( 282 | cur.description[0][0].lower(), b('name'), 283 | 'cursor.description[x][0] must return column name') 284 | self.assertEqual( 285 | cur.description[0][1], self.driver.STRING, 286 | 'cursor.description[x][1] must return column type. Got %r' 287 | % cur.description[0][1]) 288 | 289 | # Make sure self.description gets reset 290 | self.executeDDL2(cur) 291 | self.assertEqual( 292 | cur.description, None, 293 | 'cursor.description not being set to None when executing ' 294 | 'no-result statements (eg. DDL)') 295 | finally: 296 | con.close() 297 | 298 | def test_rowcount(self): 299 | con = self._connect() 300 | try: 301 | cur = con.cursor() 302 | self.executeDDL1(cur) 303 | self.assertEqual( 304 | cur.rowcount, -1, 305 | 'cursor.rowcount should be -1 after executing no-result ' 306 | 'statements') 307 | cur.execute( 308 | "insert into %sbooze values ('Victoria Bitter')" % ( 309 | self.table_prefix)) 310 | self.assertEqual( 311 | cur.rowcount in (-1, 1), True, 312 | 'cursor.rowcount should == number or rows inserted, or ' 313 | 'set to -1 after executing an insert statement') 314 | cur.execute("select name from %sbooze" % self.table_prefix) 315 | self.assertEqual( 316 | cur.rowcount in (-1, 1), True, 317 | 'cursor.rowcount should == number of rows returned, or ' 318 | 'set to -1 after executing a select statement') 319 | self.executeDDL2(cur) 320 | self.assertEqual( 321 | cur.rowcount, -1, 322 | 'cursor.rowcount not being reset to -1 after executing ' 323 | 'no-result statements') 324 | finally: 325 | con.close() 326 | 327 | lower_func = 'lower' 328 | 329 | def test_callproc(self): 330 | con = self._connect() 331 | try: 332 | cur = con.cursor() 333 | if self.lower_func and hasattr(cur, 'callproc'): 334 | r = cur.callproc(self.lower_func, ('FOO',)) 335 | self.assertEqual(len(r), 1) 336 | self.assertEqual(r[0], 'FOO') 337 | r = cur.fetchall() 338 | self.assertEqual(len(r), 1, 'callproc produced no result set') 339 | self.assertEqual( 340 | len(r[0]), 1, 'callproc produced invalid result set') 341 | self.assertEqual( 342 | r[0][0], 'foo', 'callproc produced invalid results') 343 | finally: 344 | con.close() 345 | 346 | def test_close(self): 347 | con = self._connect() 348 | try: 349 | cur = con.cursor() 350 | finally: 351 | con.close() 352 | 353 | # cursor.execute should raise an Error if called after connection 354 | # closed 355 | self.assertRaises(self.driver.Error, self.executeDDL1, cur) 356 | 357 | # connection.commit should raise an Error if called after connection' 358 | # closed.' 359 | self.assertRaises(self.driver.Error, con.commit) 360 | 361 | # connection.close should raise an Error if called more than once 362 | self.assertRaises(self.driver.Error, con.close) 363 | 364 | def test_execute(self): 365 | con = self._connect() 366 | try: 367 | cur = con.cursor() 368 | self._paraminsert(cur) 369 | finally: 370 | con.close() 371 | 372 | def _paraminsert(self, cur): 373 | self.executeDDL1(cur) 374 | cur.execute("insert into %sbooze values ('Victoria Bitter')" % ( 375 | self.table_prefix)) 376 | self.assertEqual(cur.rowcount in (-1, 1), True) 377 | 378 | if self.driver.paramstyle == 'qmark': 379 | cur.execute( 380 | 'insert into %sbooze values (?)' % self.table_prefix, 381 | ("Cooper's",)) 382 | elif self.driver.paramstyle == 'numeric': 383 | cur.execute( 384 | 'insert into %sbooze values (:1)' % self.table_prefix, 385 | ("Cooper's",)) 386 | elif self.driver.paramstyle == 'named': 387 | cur.execute( 388 | 'insert into %sbooze values (:beer)' % self.table_prefix, 389 | {'beer': "Cooper's"}) 390 | elif self.driver.paramstyle == 'format': 391 | cur.execute( 392 | 'insert into %sbooze values (%%s)' % self.table_prefix, 393 | ("Cooper's",)) 394 | elif self.driver.paramstyle == 'pyformat': 395 | cur.execute( 396 | 'insert into %sbooze values (%%(beer)s)' % self.table_prefix, 397 | {'beer': "Cooper's"}) 398 | else: 399 | self.fail('Invalid paramstyle') 400 | self.assertEqual(cur.rowcount in (-1, 1), True) 401 | 402 | cur.execute('select name from %sbooze' % self.table_prefix) 403 | res = cur.fetchall() 404 | self.assertEqual( 405 | len(res), 2, 'cursor.fetchall returned too few rows') 406 | beers = [res[0][0], res[1][0]] 407 | beers.sort() 408 | self.assertEqual( 409 | beers[0], "Cooper's", 410 | 'cursor.fetchall retrieved incorrect data, or data inserted ' 411 | 'incorrectly') 412 | self.assertEqual( 413 | beers[1], "Victoria Bitter", 414 | 'cursor.fetchall retrieved incorrect data, or data inserted ' 415 | 'incorrectly') 416 | 417 | def test_executemany(self): 418 | con = self._connect() 419 | try: 420 | cur = con.cursor() 421 | self.executeDDL1(cur) 422 | largs = [("Cooper's",), ("Boag's",)] 423 | margs = [{'beer': "Cooper's"}, {'beer': "Boag's"}] 424 | if self.driver.paramstyle == 'qmark': 425 | cur.executemany( 426 | 'insert into %sbooze values (?)' % self.table_prefix, 427 | largs 428 | ) 429 | elif self.driver.paramstyle == 'numeric': 430 | cur.executemany( 431 | 'insert into %sbooze values (:1)' % self.table_prefix, 432 | largs 433 | ) 434 | elif self.driver.paramstyle == 'named': 435 | cur.executemany( 436 | 'insert into %sbooze values (:beer)' % self.table_prefix, 437 | margs 438 | ) 439 | elif self.driver.paramstyle == 'format': 440 | cur.executemany( 441 | 'insert into %sbooze values (%%s)' % self.table_prefix, 442 | largs 443 | ) 444 | elif self.driver.paramstyle == 'pyformat': 445 | cur.executemany( 446 | 'insert into %sbooze values (%%(beer)s)' % ( 447 | self.table_prefix), margs) 448 | else: 449 | self.fail('Unknown paramstyle') 450 | self.assertEqual( 451 | cur.rowcount in (-1, 2), True, 452 | 'insert using cursor.executemany set cursor.rowcount to ' 453 | 'incorrect value %r' % cur.rowcount) 454 | cur.execute('select name from %sbooze' % self.table_prefix) 455 | res = cur.fetchall() 456 | self.assertEqual( 457 | len(res), 2, 458 | 'cursor.fetchall retrieved incorrect number of rows') 459 | beers = [res[0][0], res[1][0]] 460 | beers.sort() 461 | self.assertEqual(beers[0], "Boag's", 'incorrect data retrieved') 462 | self.assertEqual(beers[1], "Cooper's", 'incorrect data retrieved') 463 | finally: 464 | con.close() 465 | 466 | def test_fetchone(self): 467 | con = self._connect() 468 | try: 469 | cur = con.cursor() 470 | 471 | # cursor.fetchone should raise an Error if called before 472 | # executing a select-type query 473 | self.assertRaises(self.driver.Error, cur.fetchone) 474 | 475 | # cursor.fetchone should raise an Error if called after 476 | # executing a query that cannnot return rows 477 | self.executeDDL1(cur) 478 | self.assertRaises(self.driver.Error, cur.fetchone) 479 | 480 | cur.execute('select name from %sbooze' % self.table_prefix) 481 | self.assertEqual( 482 | cur.fetchone(), None, 483 | 'cursor.fetchone should return None if a query retrieves ' 484 | 'no rows') 485 | self.assertEqual(cur.rowcount in (-1, 0), True) 486 | 487 | # cursor.fetchone should raise an Error if called after 488 | # executing a query that cannnot return rows 489 | cur.execute( 490 | "insert into %sbooze values ('Victoria Bitter')" % ( 491 | self.table_prefix)) 492 | self.assertRaises(self.driver.Error, cur.fetchone) 493 | 494 | cur.execute('select name from %sbooze' % self.table_prefix) 495 | r = cur.fetchone() 496 | self.assertEqual( 497 | len(r), 1, 498 | 'cursor.fetchone should have retrieved a single row') 499 | self.assertEqual( 500 | r[0], 'Victoria Bitter', 501 | 'cursor.fetchone retrieved incorrect data') 502 | self.assertEqual( 503 | cur.fetchone(), None, 504 | 'cursor.fetchone should return None if no more rows available') 505 | self.assertEqual(cur.rowcount in (-1, 1), True) 506 | finally: 507 | con.close() 508 | 509 | samples = [ 510 | 'Carlton Cold', 511 | 'Carlton Draft', 512 | 'Mountain Goat', 513 | 'Redback', 514 | 'Victoria Bitter', 515 | 'XXXX' 516 | ] 517 | 518 | def _populate(self): 519 | ''' Return a list of sql commands to setup the DB for the fetch 520 | tests. 521 | ''' 522 | populate = [ 523 | "insert into %sbooze values ('%s')" % (self.table_prefix, s) 524 | for s in self.samples] 525 | return populate 526 | 527 | def test_fetchmany(self): 528 | con = self._connect() 529 | try: 530 | cur = con.cursor() 531 | 532 | # cursor.fetchmany should raise an Error if called without 533 | # issuing a query 534 | self.assertRaises(self.driver.Error, cur.fetchmany, 4) 535 | 536 | self.executeDDL1(cur) 537 | for sql in self._populate(): 538 | cur.execute(sql) 539 | 540 | cur.execute('select name from %sbooze' % self.table_prefix) 541 | r = cur.fetchmany() 542 | self.assertEqual( 543 | len(r), 1, 544 | 'cursor.fetchmany retrieved incorrect number of rows, ' 545 | 'default of arraysize is one.') 546 | cur.arraysize = 10 547 | r = cur.fetchmany(3) # Should get 3 rows 548 | self.assertEqual( 549 | len(r), 3, 550 | 'cursor.fetchmany retrieved incorrect number of rows') 551 | r = cur.fetchmany(4) # Should get 2 more 552 | self.assertEqual( 553 | len(r), 2, 554 | 'cursor.fetchmany retrieved incorrect number of rows') 555 | r = cur.fetchmany(4) # Should be an empty sequence 556 | self.assertEqual( 557 | len(r), 0, 558 | 'cursor.fetchmany should return an empty sequence after ' 559 | 'results are exhausted') 560 | self.assertEqual(cur.rowcount in (-1, 6), True) 561 | 562 | # Same as above, using cursor.arraysize 563 | cur.arraysize = 4 564 | cur.execute('select name from %sbooze' % self.table_prefix) 565 | r = cur.fetchmany() # Should get 4 rows 566 | self.assertEqual( 567 | len(r), 4, 568 | 'cursor.arraysize not being honoured by fetchmany') 569 | r = cur.fetchmany() # Should get 2 more 570 | self.assertEqual(len(r), 2) 571 | r = cur.fetchmany() # Should be an empty sequence 572 | self.assertEqual(len(r), 0) 573 | self.assertEqual(cur.rowcount in (-1, 6), True) 574 | 575 | cur.arraysize = 6 576 | cur.execute('select name from %sbooze' % self.table_prefix) 577 | rows = cur.fetchmany() # Should get all rows 578 | self.assertEqual(cur.rowcount in (-1, 6), True) 579 | self.assertEqual(len(rows), 6) 580 | self.assertEqual(len(rows), 6) 581 | rows = [row[0] for row in rows] 582 | rows.sort() 583 | 584 | # Make sure we get the right data back out 585 | for i in range(0, 6): 586 | self.assertEqual( 587 | rows[i], self.samples[i], 588 | 'incorrect data retrieved by cursor.fetchmany') 589 | 590 | rows = cur.fetchmany() # Should return an empty list 591 | self.assertEqual( 592 | len(rows), 0, 593 | 'cursor.fetchmany should return an empty sequence if ' 594 | 'called after the whole result set has been fetched') 595 | self.assertEqual(cur.rowcount in (-1, 6), True) 596 | 597 | self.executeDDL2(cur) 598 | cur.execute('select name from %sbarflys' % self.table_prefix) 599 | r = cur.fetchmany() # Should get empty sequence 600 | self.assertEqual( 601 | len(r), 0, 602 | 'cursor.fetchmany should return an empty sequence if ' 603 | 'query retrieved no rows') 604 | self.assertEqual(cur.rowcount in (-1, 0), True) 605 | 606 | finally: 607 | con.close() 608 | 609 | def test_fetchall(self): 610 | con = self._connect() 611 | try: 612 | cur = con.cursor() 613 | # cursor.fetchall should raise an Error if called 614 | # without executing a query that may return rows (such 615 | # as a select) 616 | self.assertRaises(self.driver.Error, cur.fetchall) 617 | 618 | self.executeDDL1(cur) 619 | for sql in self._populate(): 620 | cur.execute(sql) 621 | 622 | # cursor.fetchall should raise an Error if called 623 | # after executing a a statement that cannot return rows 624 | self.assertRaises(self.driver.Error, cur.fetchall) 625 | 626 | cur.execute('select name from %sbooze' % self.table_prefix) 627 | rows = cur.fetchall() 628 | self.assertEqual(cur.rowcount in (-1, len(self.samples)), True) 629 | self.assertEqual( 630 | len(rows), len(self.samples), 631 | 'cursor.fetchall did not retrieve all rows') 632 | rows = [r[0] for r in rows] 633 | rows.sort() 634 | for i in range(0, len(self.samples)): 635 | self.assertEqual( 636 | rows[i], self.samples[i], 637 | 'cursor.fetchall retrieved incorrect rows') 638 | rows = cur.fetchall() 639 | self.assertEqual( 640 | len(rows), 0, 641 | 'cursor.fetchall should return an empty list if called ' 642 | 'after the whole result set has been fetched') 643 | self.assertEqual(cur.rowcount in (-1, len(self.samples)), True) 644 | 645 | self.executeDDL2(cur) 646 | cur.execute('select name from %sbarflys' % self.table_prefix) 647 | rows = cur.fetchall() 648 | self.assertEqual(cur.rowcount in (-1, 0), True) 649 | self.assertEqual( 650 | len(rows), 0, 651 | 'cursor.fetchall should return an empty list if ' 652 | 'a select query returns no rows') 653 | 654 | finally: 655 | con.close() 656 | 657 | def test_mixedfetch(self): 658 | con = self._connect() 659 | try: 660 | cur = con.cursor() 661 | self.executeDDL1(cur) 662 | for sql in self._populate(): 663 | cur.execute(sql) 664 | 665 | cur.execute('select name from %sbooze' % self.table_prefix) 666 | rows1 = cur.fetchone() 667 | rows23 = cur.fetchmany(2) 668 | rows4 = cur.fetchone() 669 | rows56 = cur.fetchall() 670 | self.assertEqual(cur.rowcount in (-1, 6), True) 671 | self.assertEqual( 672 | len(rows23), 2, 'fetchmany returned incorrect number of rows') 673 | self.assertEqual( 674 | len(rows56), 2, 'fetchall returned incorrect number of rows') 675 | 676 | rows = [rows1[0]] 677 | rows.extend([rows23[0][0], rows23[1][0]]) 678 | rows.append(rows4[0]) 679 | rows.extend([rows56[0][0], rows56[1][0]]) 680 | rows.sort() 681 | for i in range(0, len(self.samples)): 682 | self.assertEqual( 683 | rows[i], self.samples[i], 684 | 'incorrect data retrieved or inserted') 685 | finally: 686 | con.close() 687 | 688 | def help_nextset_setUp(self, cur): 689 | ''' Should create a procedure called deleteme 690 | that returns two result sets, first the 691 | number of rows in booze then "name from booze" 692 | ''' 693 | raise NotImplementedError('Helper not implemented') 694 | 695 | def help_nextset_tearDown(self, cur): 696 | 'If cleaning up is needed after nextSetTest' 697 | raise NotImplementedError('Helper not implemented') 698 | 699 | def test_nextset(self): 700 | con = self._connect() 701 | try: 702 | cur = con.cursor() 703 | if not hasattr(cur, 'nextset'): 704 | return 705 | 706 | try: 707 | self.executeDDL1(cur) 708 | sql = self._populate() 709 | for sql in self._populate(): 710 | cur.execute(sql) 711 | 712 | self.help_nextset_setUp(cur) 713 | 714 | cur.callproc('deleteme') 715 | numberofrows = cur.fetchone() 716 | assert numberofrows[0] == len(self.samples) 717 | assert cur.nextset() 718 | names = cur.fetchall() 719 | assert len(names) == len(self.samples) 720 | s = cur.nextset() 721 | assert s is None, 'No more return sets, should return None' 722 | finally: 723 | self.help_nextset_tearDown(cur) 724 | 725 | finally: 726 | con.close() 727 | 728 | ''' 729 | def test_nextset(self): 730 | raise NotImplementedError('Drivers need to override this test') 731 | ''' 732 | 733 | def test_arraysize(self): 734 | # Not much here - rest of the tests for this are in test_fetchmany 735 | con = self._connect() 736 | try: 737 | cur = con.cursor() 738 | self.assertEqual( 739 | hasattr(cur, 'arraysize'), True, 740 | 'cursor.arraysize must be defined') 741 | finally: 742 | con.close() 743 | 744 | def test_setinputsizes(self): 745 | con = self._connect() 746 | try: 747 | cur = con.cursor() 748 | cur.setinputsizes((25,)) 749 | self._paraminsert(cur) # Make sure cursor still works 750 | finally: 751 | con.close() 752 | 753 | def test_setoutputsize_basic(self): 754 | # Basic test is to make sure setoutputsize doesn't blow up 755 | con = self._connect() 756 | try: 757 | cur = con.cursor() 758 | cur.setoutputsize(1000) 759 | cur.setoutputsize(2000, 0) 760 | self._paraminsert(cur) # Make sure the cursor still works 761 | finally: 762 | con.close() 763 | 764 | def test_setoutputsize(self): 765 | # Real test for setoutputsize is driver dependant 766 | raise NotImplementedError('Driver need to override this test') 767 | 768 | def test_None(self): 769 | con = self._connect() 770 | try: 771 | cur = con.cursor() 772 | self.executeDDL1(cur) 773 | cur.execute( 774 | 'insert into %sbooze values (NULL)' % self.table_prefix) 775 | cur.execute('select name from %sbooze' % self.table_prefix) 776 | r = cur.fetchall() 777 | self.assertEqual(len(r), 1) 778 | self.assertEqual(len(r[0]), 1) 779 | self.assertEqual(r[0][0], None, 'NULL value not returned as None') 780 | finally: 781 | con.close() 782 | 783 | def test_Date(self): 784 | self.driver.Date(2002, 12, 25) 785 | self.driver.DateFromTicks( 786 | time.mktime((2002, 12, 25, 0, 0, 0, 0, 0, 0))) 787 | # Can we assume this? API doesn't specify, but it seems implied 788 | # self.assertEqual(str(d1),str(d2)) 789 | 790 | def test_Time(self): 791 | self.driver.Time(13, 45, 30) 792 | self.driver.TimeFromTicks( 793 | time.mktime((2001, 1, 1, 13, 45, 30, 0, 0, 0))) 794 | # Can we assume this? API doesn't specify, but it seems implied 795 | # self.assertEqual(str(t1),str(t2)) 796 | 797 | def test_Timestamp(self): 798 | self.driver.Timestamp(2002, 12, 25, 13, 45, 30) 799 | self.driver.TimestampFromTicks( 800 | time.mktime((2002, 12, 25, 13, 45, 30, 0, 0, 0))) 801 | # Can we assume this? API doesn't specify, but it seems implied 802 | # self.assertEqual(str(t1),str(t2)) 803 | 804 | def test_Binary(self): 805 | self.driver.Binary(b('Something')) 806 | self.driver.Binary(b('')) 807 | 808 | def test_STRING(self): 809 | self.assertEqual( 810 | hasattr(self.driver, 'STRING'), True, 811 | 'module.STRING must be defined') 812 | 813 | def test_BINARY(self): 814 | self.assertEqual( 815 | hasattr(self.driver, 'BINARY'), True, 816 | 'module.BINARY must be defined.') 817 | 818 | def test_NUMBER(self): 819 | self.assertTrue( 820 | hasattr(self.driver, 'NUMBER'), 'module.NUMBER must be defined.') 821 | 822 | def test_DATETIME(self): 823 | self.assertEqual( 824 | hasattr(self.driver, 'DATETIME'), True, 825 | 'module.DATETIME must be defined.') 826 | 827 | def test_ROWID(self): 828 | self.assertEqual( 829 | hasattr(self.driver, 'ROWID'), True, 830 | 'module.ROWID must be defined.') 831 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = pg8000 2 | :toc: preamble 3 | 4 | pg8000 is a pure-link:http://www.python.org/[Python] 5 | http://www.postgresql.org/[PostgreSQL] driver that complies with 6 | http://www.python.org/dev/peps/pep-0249/[DB-API 2.0]. It runs on Python versions 2.7+ and 3.3+, on CPython, Jython and PyPy. Pg8000's name comes from 7 | the belief that it is probably about the 8000th PostgreSQL interface for 8 | Python. pg8000 is distributed under the terms of the BSD 3-clause license. 9 | 10 | All bug reports, feature requests and contributions are welcome at 11 | http://github.com/mfenniak/pg8000/. 12 | 13 | If you're a user of pg8000 we'd love to hear who you are and what you or your 14 | organization is using pg8000 for. Just log an issue on GitHub. The idea is to 15 | put together a list of users. 16 | 17 | image::https://circleci.com/gh/mfenniak/pg8000.svg?style=shield&circle-token=2737fb9ccf95410c088deb53f1212a7bc2314482[Build Status] 18 | 19 | 20 | == Key Points 21 | 22 | * Although it's possible for threads to share cursors and connections, for 23 | performance reasons it's best to use one thread per connection. 24 | * Internally, all queries use prepared statements. pg8000 remembers that a 25 | prepared statement has been created, and uses it on subsequent queries. 26 | 27 | 28 | == Installation 29 | 30 | To install pg8000 using `pip` type: 31 | 32 | `pip install pg8000` 33 | 34 | 35 | == Interactive Example 36 | 37 | Import pg8000, connect to the database, create a table, add some rows and then 38 | query the table: 39 | 40 | [source,python] 41 | ---- 42 | >>> import pg8000 43 | >>> conn = pg8000.connect(user="postgres", password="C.P.Snow") 44 | >>> cursor = conn.cursor() 45 | >>> cursor.execute("CREATE TEMPORARY TABLE book (id SERIAL, title TEXT)") 46 | >>> cursor.execute( 47 | ... "INSERT INTO book (title) VALUES (%s), (%s) RETURNING id, title", 48 | ... ("Ender's Game", "Speaker for the Dead")) 49 | >>> results = cursor.fetchall() 50 | >>> for row in results: 51 | ... id, title = row 52 | ... print("id = %s, title = %s" % (id, title)) 53 | id = 1, title = Ender's Game 54 | id = 2, title = Speaker for the Dead 55 | >>> conn.commit() 56 | 57 | ---- 58 | 59 | Another query, using some PostgreSQL functions: 60 | 61 | [source,python] 62 | ---- 63 | >>> cursor.execute("SELECT extract(millennium from now())") 64 | >>> cursor.fetchone() 65 | [3.0] 66 | 67 | ---- 68 | 69 | A query that returns the PostgreSQL interval type: 70 | 71 | [source,python] 72 | ---- 73 | >>> import datetime 74 | >>> cursor.execute("SELECT timestamp '2013-12-01 16:06' - %s", 75 | ... (datetime.date(1980, 4, 27),)) 76 | >>> cursor.fetchone() 77 | [datetime.timedelta(12271, 57960)] 78 | 79 | ---- 80 | 81 | pg8000 supports all the DB-API parameter styles. Here's an example of using 82 | the 'numeric' parameter style: 83 | 84 | [source,python] 85 | ---- 86 | >>> pg8000.paramstyle = "numeric" 87 | >>> cursor.execute("SELECT array_prepend(:1, :2)", ( 500, [1, 2, 3, 4], )) 88 | >>> cursor.fetchone() 89 | [[500, 1, 2, 3, 4]] 90 | >>> pg8000.paramstyle = "format" 91 | >>> conn.rollback() 92 | 93 | ---- 94 | 95 | Following the DB-API specification, autocommit is off by default. It can be 96 | turned on by using the autocommit property of the connection. 97 | 98 | [source,python] 99 | ---- 100 | >>> conn.autocommit = True 101 | >>> cur = conn.cursor() 102 | >>> cur.execute("vacuum") 103 | >>> conn.autocommit = False 104 | >>> cursor.close() 105 | 106 | ---- 107 | 108 | When communicating with the server, pg8000 uses the character set that the 109 | server asks it to use (the client encoding). By default the client encoding is 110 | the database's character set (chosen when the database is created), but the 111 | client encoding can be changed in a number of ways (eg. setting 112 | CLIENT_ENCODING in postgresql.conf). Another way of changing the client 113 | encoding is by using an SQL command. For example: 114 | 115 | [source,python] 116 | ---- 117 | >>> cur = conn.cursor() 118 | >>> cur.execute("SET CLIENT_ENCODING TO 'UTF8'") 119 | >>> cur.execute("SHOW CLIENT_ENCODING") 120 | >>> cur.fetchone() 121 | ['UTF8'] 122 | >>> cur.close() 123 | 124 | ---- 125 | 126 | JSON is sent to the server serialized, and returned de-serialized. Here's an 127 | example: 128 | 129 | [source,python] 130 | ---- 131 | >>> import json 132 | >>> cur = conn.cursor() 133 | >>> val = ['Apollo 11 Cave', True, 26.003] 134 | >>> cur.execute("SELECT cast(%s as json)", (json.dumps(val),)) 135 | >>> cur.fetchone() 136 | [['Apollo 11 Cave', True, 26.003]] 137 | >>> cur.close() 138 | 139 | ---- 140 | 141 | 142 | PostgreSQL https://www.postgresql.org/docs/current/static/plpgsql-errors-and-messages.html[notices] 143 | are stored in a deque called `Connection.notices` and added using the 144 | `append()` method. Similarly there are `Connection.notifications` for 145 | https://www.postgresql.org/docs/current/static/sql-notify.html[notifications] 146 | and `Connection.parameter_statuses` for changes to the server configuration. 147 | Here's an example: 148 | 149 | [source,python] 150 | ---- 151 | >>> cur = conn.cursor() 152 | >>> cur.execute("LISTEN aliens_landed") 153 | >>> cur.execute("NOTIFY aliens_landed") 154 | >>> conn.commit() 155 | >>> conn.notifications[0][1] 156 | 'aliens_landed' 157 | 158 | ---- 159 | 160 | 161 | == Type Mapping 162 | 163 | The following table shows the mapping between Python types and PostgreSQL 164 | types, and vice versa. 165 | 166 | If pg8000 doesn't recognize a type that it receives from PostgreSQL, it will 167 | return it as a `str` type. This is how pg8000 handles PostgreSQL `enum` and 168 | XML types. 169 | 170 | .Python to PostgreSQL Type Mapping 171 | |=== 172 | | Python Type | PostgreSQL Type | Notes 173 | 174 | | bool 175 | | bool 176 | | 177 | 178 | | int 179 | | int4 180 | | 181 | 182 | | long 183 | | numeric 184 | | Python 2 only. 185 | 186 | | str 187 | | text (Python 3), bytea (Python 2) 188 | | 189 | 190 | | unicode 191 | | text 192 | | Python 2 only. 193 | 194 | | float 195 | | float8 196 | | 197 | 198 | | decimal.Decimal 199 | | numeric 200 | | 201 | 202 | | pg8000.Bytea 203 | | bytea 204 | | Python 2 only. 205 | 206 | | bytes 207 | | bytea 208 | | Python 3 only. 209 | 210 | | datetime.datetime (without tzinfo) 211 | | timestamp without timezone 212 | | See note below. 213 | 214 | | datetime.datetime (with tzinfo) 215 | | timestamp with timezone 216 | | See note below. 217 | 218 | | datetime.date 219 | | date 220 | | See note below. 221 | 222 | | datetime.time 223 | | time without time zone 224 | | 225 | 226 | | datetime.timedelta 227 | | interval 228 | | datetime.timedelta is used unless the interval has months, in which case 229 | pg8000.Interval is used 230 | 231 | | None 232 | | NULL 233 | | 234 | 235 | | uuid.UUID 236 | | uuid 237 | | 238 | 239 | | ipaddress.IPv4Address 240 | | inet 241 | | Python 3.3 onwards 242 | 243 | | ipaddress.IPv6Address 244 | | inet 245 | | Python 3.3 onwards 246 | 247 | | ipaddress.IPv4Network 248 | | inet 249 | | Python 3.3 onwards 250 | 251 | | ipaddress.IPv6Network 252 | | inet 253 | | Python 3.3 onwards 254 | 255 | | int 256 | | xid 257 | | 258 | 259 | | list of int 260 | | INT4[] 261 | | 262 | 263 | | list of float 264 | | FLOAT8[] 265 | | 266 | 267 | | list of bool 268 | | BOOL[] 269 | | 270 | 271 | | list of str 272 | | TEXT[] 273 | | 274 | 275 | | list of unicode 276 | | TEXT[] 277 | | Python 2 only. 278 | 279 | | int 280 | | int2vector 281 | | Only from PostgreSQL to Python 282 | 283 | | JSON 284 | | json, jsonb 285 | | JSON string as an SQL parameter. Results returned as de-serialized JSON. 286 | |=== 287 | 288 | 289 | [[_theory_of_operation]] 290 | == Theory Of Operation 291 | 292 | 293 | Pg8000 communicates with the database using the 294 | http://www.postgresql.org/docs/current/static/protocol.html[PostgreSQL 295 | Frontend/Backend Protocol] (FEBE). Every query made with pg8000 uses prepared 296 | statements. It uses the Extended Query feature of the FEBE. So the steps are: 297 | 298 | . Query comes in. 299 | . If pg8000 hasn't seen it before, send a PARSE message to the server to create 300 | a prepared statement. 301 | . Send a BIND message to run the query using the prepared statement, resulting 302 | in an unnamed portal on the server. 303 | . Send an EXECUTE message to read all the results from the portal. 304 | 305 | There are a lot of PostgreSQL data types, but few primitive data types in 306 | Python. A PostgreSQL data type has to be assigned to each query parameter, 307 | which is impossible to work out in all cases because. pg8000's solution to this 308 | problem is to send some parameters with the PostgreSQL type `unknown`. This 309 | forces the server to make a guess about the type. It's fairly good at doing 310 | this, so most queries '`just work`'. However, sometimes you may need to do an 311 | explicit cast in the SQL query to get things to work. 312 | 313 | In the FEBE protocol, each query parameter can be sent to the server either as 314 | binary or text according to the format code (FC). In pg8000 the FC depends 315 | on the type of the value. Here is a table of some common types and their FC: 316 | 317 | .Python Type to FC Mapping 318 | |=== 319 | | Python Type | FC 320 | 321 | | None | binary 322 | | bool | binary 323 | | int | binary 324 | | float | binary 325 | | datetime.date | text 326 | | datetime.time | text 327 | | datetime.datetime (naive) | binary 328 | | datetime.datetime (with timezone) | binary 329 | | datetime.timedelta | binary 330 | | decimal.Decimal | text 331 | | uuid | binary 332 | |=== 333 | 334 | * Since pg8000 uses prepared statements implicitly, there's nothing to be 335 | gained by using them explicitly with the SQL PREPARE, EXECUTE and DEALLOCATE 336 | keywords. In fact in some cases pg8000 won't work for parameterized EXECUTE 337 | statements, because the server is unable to infer the types of the 338 | parameters for an EXECUTE statement. 339 | 340 | * PostgreSQL has +/-infinity values for dates and timestamps, but Python does 341 | not. Pg8000 handles this by returning +/-infinity strings in results, and in 342 | parameters the strings +/- infinity can be used. 343 | 344 | * PostgreSQL dates/timestamps can have values outside the range of Python 345 | datetimes. These are handled using the underlying PostgreSQL storage method. 346 | I don't know of any users of pg8000 that use this feature, so get in touch if 347 | it affects you. 348 | 349 | * Pg8000 can't handle a change of `search_path`, so statements like `set schema 350 | 'value';` may cause subsequent statements to fail. This is because pg8000 351 | will use a prepared statement for a previously executed query, and this 352 | prepared statement won't be aware of any change in `search_path`. 353 | 354 | 355 | == API Docs 356 | 357 | 358 | === Properties 359 | 360 | 361 | ==== pg8000.apilevel 362 | 363 | The DBAPI level supported, currently "2.0". 364 | 365 | This property is part of the 366 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 367 | 368 | 369 | ==== pg8000.threadsafety 370 | 371 | Integer constant stating the level of thread safety the DBAPI interface 372 | supports. This DBAPI module supports sharing the module, connections, and 373 | cursors, resulting in a threadsafety value of 3. 374 | 375 | This property is part of the 376 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 377 | 378 | ==== pg8000.paramstyle 379 | 380 | String property stating the type of parameter marker formatting expected by 381 | the interface. This value defaults to "format", in which parameters are 382 | marked in this format: "WHERE name=%s". 383 | 384 | This property is part of the 385 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 386 | 387 | As an extension to the DBAPI specification, this value is not constant; it 388 | can be changed to any of the following values: 389 | 390 | qmark:: 391 | Question mark style, eg. `WHERE name=?` 392 | 393 | numeric:: 394 | Numeric positional style, eg. `WHERE name=:1` 395 | 396 | named:: 397 | Named style, eg. `WHERE name=:paramname` 398 | 399 | format:: 400 | printf format codes, eg. `WHERE name=%s` 401 | 402 | pyformat:: 403 | Python format codes, eg. `WHERE name=%(paramname)s` 404 | 405 | 406 | ==== pg8000.STRING 407 | 408 | String type oid. 409 | 410 | ==== pg8000.BINARY 411 | 412 | 413 | ==== pg8000.NUMBER 414 | 415 | Numeric type oid. 416 | 417 | 418 | ==== pg8000.DATETIME 419 | 420 | Timestamp type oid 421 | 422 | 423 | ==== pg8000.ROWID 424 | 425 | ROWID type oid 426 | 427 | 428 | === Functions 429 | 430 | 431 | ==== pg8000.connect(user, host='localhost', unix_sock=None, port=5432, database=None, password=None, ssl=False, timeout=None, application_name=None) 432 | 433 | Creates a connection to a PostgreSQL database. 434 | 435 | This property is part of the 436 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 437 | 438 | user:: 439 | The username to connect to the PostgreSQL server with. If your server 440 | character encoding is not '`ascii`' or '`utf8`', then you need to provide 441 | `user` as bytes, eg. `'my_name'.encode(\'EUC-JP\')`. 442 | 443 | host:: 444 | The hostname of the PostgreSQL server to connect with. Providing this 445 | parameter is necessary for TCP/IP connections. One of either `host` or 446 | `unix_sock` must be provided. The default is `localhost`. 447 | 448 | unix_sock:: 449 | The path to the UNIX socket to access the database through, for example, 450 | `'/tmp/.s.PGSQL.5432'`. One of either `host` or `unix_sock` must be provided. 451 | 452 | port:: 453 | The TCP/IP port of the PostgreSQL server instance. This parameter defaults 454 | to `5432`, the registered common port of PostgreSQL TCP/IP servers. 455 | 456 | database:: 457 | The name of the database instance to connect with. This parameter is 458 | optional; if omitted, the PostgreSQL server will assume the database name is 459 | the same as the username. + 460 | + 461 | If your server character encoding is not '`ascii`' or '`utf8`', then 462 | you need to provide `database` as bytes, eg. 463 | `'my_db'.encode('EUC-JP')`. 464 | 465 | password:: 466 | The user password to connect to the server with. This parameter is optional; 467 | if omitted and the database server requests password-based authentication, 468 | the connection will fail to open. If this parameter is provided but not 469 | requested by the server, no error will occur. + 470 | + 471 | If your server character encoding is not '`ascii`' or '`utf8`', then 472 | you need to provide `password` as bytes, eg. 473 | `'my_password'.encode('EUC-JP')`. 474 | 475 | ssl:: 476 | Use SSL encryption for TCP/IP sockets if `True`. Defaults to `False`. 477 | 478 | timeout:: 479 | Only used with Python 3, this is the time in seconds before the connection to 480 | the database will time out. The default is `None` which means no timeout. 481 | 482 | 483 | ==== pg8000.Date(year, month, day) 484 | 485 | Constuct an object holding a date value. 486 | 487 | This function is part of the 488 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 489 | 490 | Returns: `datetime.date` 491 | 492 | 493 | ==== pg8000.Time(hour, minute, second) 494 | 495 | Construct an object holding a time value. 496 | 497 | This function is part of the 498 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 499 | 500 | Returns: `datetime.time` 501 | 502 | 503 | ==== pg8000.Timestamp(year, month, day, hour, minute, second) 504 | 505 | Construct an object holding a timestamp value. 506 | 507 | This function is part of the 508 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 509 | 510 | Returns: `datetime.datetime` 511 | 512 | 513 | ==== pg8000.DateFromTicks(ticks) 514 | 515 | Construct an object holding a date value from the given ticks value (number of 516 | seconds since the epoch). 517 | 518 | This function is part of the 519 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 520 | 521 | Returns: `datetime.datetime` 522 | 523 | 524 | ==== pg8000.TimeFromTicks(ticks) 525 | 526 | Construct an objet holding a time value from the given ticks value (number of 527 | seconds since the epoch). 528 | 529 | This function is part of the 530 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 531 | 532 | Returns: `datetime.time` 533 | 534 | 535 | ==== pg8000.TimestampFromTicks(ticks) 536 | 537 | Construct an object holding a timestamp value from the given ticks value 538 | (number of seconds since the epoch). 539 | 540 | 541 | This function is part of the 542 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 543 | 544 | Returns: `datetime.datetime` 545 | 546 | 547 | ==== pg8000.Binary(value) 548 | 549 | Construct an object holding binary data. 550 | 551 | This function is part of the 552 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 553 | 554 | Returns: `pg8000.types.Bytea` for Python 2, otherwise `bytes`. 555 | 556 | 557 | === Generic Exceptions 558 | 559 | Pg8000 uses the standard DBAPI 2.0 exception tree as "generic" exceptions. 560 | Generally, more specific exception types are raised; these specific exception 561 | types are derived from the generic exceptions. 562 | 563 | ==== pg8000.Warning 564 | 565 | Generic exception raised for important database warnings like data truncations. 566 | This exception is not currently used by pg8000. 567 | 568 | This exception is part of the 569 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 570 | 571 | ==== pg8000.Error 572 | 573 | Generic exception that is the base exception of all other error exceptions. 574 | 575 | This exception is part of the 576 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 577 | 578 | 579 | ==== pg8000.InterfaceError 580 | 581 | Generic exception raised for errors that are related to the database interface 582 | rather than the database itself. For example, if the interface attempts to use 583 | an SSL connection but the server refuses, an InterfaceError will be raised. 584 | 585 | This exception is part of the 586 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 587 | 588 | 589 | ==== pg8000.DatabaseError 590 | 591 | Generic exception raised for errors that are related to the database. This 592 | exception is currently never raised by pg8000. 593 | 594 | This exception is part of the 595 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 596 | 597 | 598 | ==== pg8000.DataError 599 | 600 | Generic exception raised for errors that are due to problems with the processed 601 | data. This exception is not currently raised by pg8000. 602 | 603 | This exception is part of the 604 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 605 | 606 | 607 | ==== pg8000.OperationalError 608 | 609 | Generic exception raised for errors that are related to the database's 610 | operation and not necessarily under the control of the programmer. This 611 | exception is currently never raised by pg8000. 612 | 613 | This exception is part of the 614 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 615 | 616 | 617 | ==== pg8000.IntegrityError 618 | 619 | Generic exception raised when the relational integrity of the database is 620 | affected. This exception is not currently raised by pg8000. 621 | 622 | This exception is part of the 623 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 624 | 625 | 626 | ==== pg8000.InternalError 627 | 628 | Generic exception raised when the database encounters an internal error. This 629 | is currently only raised when unexpected state occurs in the pg8000 interface 630 | itself, and is typically the result of a interface bug. 631 | 632 | This exception is part of the 633 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 634 | 635 | 636 | ==== pg8000.ProgrammingError 637 | 638 | Generic exception raised for programming errors. For example, this exception 639 | is raised if more parameter fields are in a query string than there are 640 | available parameters. 641 | 642 | This exception is part of the 643 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 644 | 645 | 646 | ==== pg8000.NotSupportedError 647 | 648 | Generic exception raised in case a method or database API was used which is not 649 | supported by the database. 650 | 651 | This exception is part of the 652 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 653 | 654 | 655 | === Specific Exceptions 656 | 657 | Exceptions that are subclassed from the standard DB-API 2.0 exceptions above. 658 | 659 | 660 | ==== pg8000.ArrayContentNotSupportedError 661 | 662 | Raised when attempting to transmit an array where the base type is not 663 | supported for binary data transfer by the interface. 664 | 665 | 666 | ==== pg8000.ArrayContentNotHomogenousError 667 | 668 | Raised when attempting to transmit an array that doesn’t contain only a single 669 | type of object. 670 | 671 | 672 | ==== pg8000.ArrayDimensionsNotConsistentError 673 | 674 | Raised when attempting to transmit an array that has inconsistent 675 | multi-dimension sizes. 676 | 677 | 678 | === Classes 679 | 680 | 681 | ==== pg8000.Connection 682 | 683 | A connection object is returned by the `pg8000.connect()` function. It 684 | represents a single physical connection to a PostgreSQL database. 685 | 686 | ===== pg8000.Connection.notifications 687 | 688 | A deque of server-side notifications received by this database connection (via 689 | the LISTEN/NOTIFY PostgreSQL commands). Each list element is a two-element 690 | tuple containing the PostgreSQL backend PID that issued the notify, and the 691 | notification name. 692 | 693 | 694 | This attribute is not part of the DBAPI standard; it is a pg8000 extension. 695 | 696 | 697 | ===== pg8000.Connection.notices 698 | 699 | A deque of server-side notices received by this database connection. 700 | 701 | This attribute is not part of the DBAPI standard; it is a pg8000 extension. 702 | 703 | 704 | ===== pg8000.Connection.parameter_statuses 705 | 706 | A deque of server-side parameter statuses received by this database connection. 707 | 708 | This attribute is not part of the DBAPI standard; it is a pg8000 extension. 709 | 710 | 711 | ===== pg8000.Connection.autocommit 712 | 713 | Following the DB-API specification, autocommit is off by default. It can be 714 | turned on by setting this boolean pg8000-specific autocommit property to True. 715 | 716 | New in version 1.9. 717 | 718 | 719 | ===== pg8000.Connection.max_prepared_statements 720 | 721 | The maximum number of prepared statements that pg8000 keeps track of. If this 722 | number is exceeded, they'll all be closed. The default is 1000. 723 | 724 | 725 | ===== pg8000.Connection.close() 726 | 727 | Closes the database connection. 728 | 729 | This function is part of the 730 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 731 | 732 | 733 | ===== pg8000.Connection.cursor() 734 | 735 | Creates a `pg8000.Cursor` object bound to this connection. 736 | 737 | This function is part of the 738 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 739 | 740 | 741 | ===== pg8000.Connection.rollback() 742 | 743 | Rolls back the current database transaction. 744 | 745 | This function is part of the 746 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 747 | 748 | 749 | ===== pg8000.Connection.tpc_begin(xid) 750 | 751 | Begins a TPC transaction with the given transaction ID xid. This method should 752 | be called outside of a transaction (i.e. nothing may have executed since the 753 | last `commit()` or `rollback()`. Furthermore, it is an error to call 754 | `commit()` or `rollback()` within the TPC transaction. A `ProgrammingError` is 755 | raised, if the application calls `commit()` or `rollback()` during an active 756 | TPC transaction. 757 | 758 | This function is part of the 759 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 760 | 761 | 762 | ===== pg8000.Connection.tpc_commit(xid=None) 763 | 764 | When called with no arguments, `tpc_commit()` commits a TPC transaction 765 | previously prepared with `tpc_prepare()`. If `tpc_commit()` is called prior to 766 | `tpc_prepare()`, a single phase commit is performed. A transaction manager may 767 | choose to do this if only a single resource is participating in the global 768 | transaction. 769 | 770 | When called with a transaction ID `xid`, the database commits the given 771 | transaction. If an invalid transaction ID is provided, a 772 | ProgrammingError will be raised. This form should be called outside of 773 | a transaction, and is intended for use in recovery. 774 | 775 | On return, the TPC transaction is ended. 776 | 777 | This function is part of the 778 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 779 | 780 | 781 | ===== pg8000.Connection.tpc_prepare() 782 | 783 | Performs the first phase of a transaction started with .tpc_begin(). A 784 | ProgrammingError is be raised if this method is called outside of a TPC 785 | transaction. 786 | 787 | After calling `tpc_prepare()`, no statements can be executed until 788 | `tpc_commit()` or `tpc_rollback()` have been called. 789 | 790 | This function is part of the 791 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 792 | 793 | 794 | ===== pg8000.Connection.tpc_recover() 795 | 796 | Returns a list of pending transaction IDs suitable for use with 797 | `tpc_commit(xid)` or `tpc_rollback(xid)` 798 | 799 | This function is part of the 800 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 801 | 802 | 803 | ===== pg8000.Connection.tpc_rollback(xid=None) 804 | 805 | When called with no arguments, `tpc_rollback()` rolls back a TPC transaction. 806 | It may be called before or after `tpc_prepare()`. 807 | 808 | When called with a transaction ID xid, it rolls back the given transaction. If 809 | an invalid transaction ID is provided, a `ProgrammingError` is raised. This 810 | form should be called outside of a transaction, and is intended for use in 811 | recovery. 812 | 813 | On return, the TPC transaction is ended. 814 | 815 | This function is part of the 816 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 817 | 818 | ===== pg8000.Connection.xid(format_id, global_transaction_id, branch_qualifier) 819 | 820 | Create a Transaction IDs (only global_transaction_id is used in pg) format_id 821 | and branch_qualifier are not used in postgres global_transaction_id may be any 822 | string identifier supported by postgres returns a tuple (format_id, 823 | global_transaction_id, branch_qualifier) 824 | 825 | 826 | ==== pg8000.Cursor 827 | 828 | A cursor object is returned by the `pg8000.Connection.cursor()` method of a 829 | connection. It has the following attributes and methods: 830 | 831 | ===== pg8000.Cursor.arraysize 832 | 833 | This read/write attribute specifies the number of rows to fetch at a time with 834 | `pg8000.Cursor.fetchmany()`. It defaults to 1. 835 | 836 | 837 | ===== pg8000.Cursor.connection 838 | 839 | This read-only attribute contains a reference to the connection object 840 | (an instance of `pg8000.Connection`) on which the cursor was created. 841 | 842 | This attribute is part of the 843 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 844 | 845 | 846 | ===== pg8000.Cursor.rowcount 847 | 848 | This read-only attribute contains the number of rows that the last 849 | `execute()` or `executemany()` method produced (for query statements like 850 | `SELECT`) or affected (for modification statements like `UPDATE`. 851 | 852 | The value is -1 if: 853 | 854 | * No `execute()` or `executemany()` method has been performed yet on the 855 | cursor. 856 | * There was no rowcount associated with the last `execute()`. 857 | * At least one of the statements executed as part of an `executemany()` had no 858 | row count associated with it. 859 | * Using a `SELECT` query statement on a PostgreSQL server older than version 860 | 9. 861 | * Using a `COPY` query statement on PostgreSQL server version 8.1 or older. 862 | 863 | This attribute is part of the 864 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 865 | 866 | 867 | ===== pg8000.Cursor.description"> 868 | 869 | This read-only attribute is a sequence of 7-item sequences. Each value contains 870 | information describing one result column. The 7 items returned for each column 871 | are (name, type_code, display_size, internal_size, precision, scale, null_ok). 872 | Only the first two values are provided by the current implementation. 873 | 874 | This attribute is part of the 875 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 876 | 877 | 878 | ===== pg8000.Cursor.close() 879 | 880 | Closes the cursor. 881 | 882 | This method is part of the 883 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 884 | 885 | 886 | ===== pg8000.Cursor.execute(operation, args=None, stream=None) 887 | 888 | Executes a database operation. Parameters may be provided as a sequence, or as 889 | a mapping, depending upon the value of `pg8000.paramstyle`. 890 | 891 | This method is part of the 892 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 893 | 894 | 895 | operation:: 896 | The SQL statement to execute. 897 | 898 | args:: 899 | If `pg8000.paramstyle` is `qmark`, `numeric`, or `format`, this argument 900 | should be an array of parameters to bind into the statement. If 901 | `pg8000.paramstyle` is `named`, the argument should be a `dict` mapping of 902 | parameters. If `pg8000.paramstyle' is `pyformat`, the argument value may be 903 | either an array or a mapping. 904 | 905 | stream:: 906 | This is a pg8000 extension for use with the PostgreSQL 907 | http://www.postgresql.org/docs/current/static/sql-copy.html[COPY] command. For 908 | a `COPY FROM` the parameter must be a readable file-like object, and for 909 | `COPY TO` it must be writable. 910 | 911 | New in version 1.9.11. 912 | 913 | 914 | ===== pg8000.Cursor.executemany(operation, param_sets) 915 | 916 | Prepare a database operation, and then execute it against all parameter 917 | sequences or mappings provided. 918 | 919 | This method is part of the 920 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 921 | 922 | operation:: 923 | The SQL statement to execute. 924 | parameter_sets:: 925 | A sequence of parameters to execute the statement with. The values in the 926 | sequence should be sequences or mappings of parameters, the same as the args 927 | argument of the `pg8000.Cursor.execute()` method. 928 | 929 | 930 | ===== pg8000.Cursor.fetchall() 931 | 932 | Fetches all remaining rows of a query result. 933 | 934 | This method is part of the 935 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 936 | 937 | Returns: A sequence, each entry of which is a sequence of field values making 938 | up a row. 939 | 940 | 941 | ===== pg8000.Cursor.fetchmany(size=None) 942 | 943 | Fetches the next set of rows of a query result. 944 | 945 | This method is part of the 946 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 947 | 948 | size:: 949 | The number of rows to fetch when called. If not provided, the 950 | `pg8000.Cursor.arraysize` attribute value is used instead. 951 | 952 | Returns: A sequence, each entry of which is a sequence of field values making 953 | up a row. If no more rows are available, an empty sequence will be returned. 954 | 955 | 956 | ===== pg8000.Cursor.fetchone() 957 | 958 | Fetch the next row of a query result set. 959 | 960 | This method is part of the 961 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification]. 962 | 963 | Returns: A row as a sequence of field values, or `None` if no more rows are 964 | available. 965 | 966 | 967 | ===== pg8000.Cursor.setinputsizes 968 | 969 | This method is part of the 970 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification], however, it 971 | is not implemented by pg8000. 972 | 973 | 974 | ===== pg8000.Cursor.setoutputsize(size, column=None) 975 | 976 | This method is part of the 977 | http://www.python.org/dev/peps/pep-0249/[DBAPI 2.0 specification], however, it 978 | is not implemented by pg8000. 979 | 980 | 981 | ==== pg8000.Bytea 982 | 983 | Bytea is a str-derived class that is mapped to a PostgreSQL byte array. This 984 | class is only used in Python 2, the built-in `bytes` type is used in Python 3. 985 | 986 | 987 | ==== pg8000.Interval 988 | 989 | An Interval represents a measurement of time. In PostgreSQL, an interval is 990 | defined in the measure of months, days, and microseconds; as such, the pg8000 991 | interval type represents the same information. 992 | 993 | Note that values of the `pg8000.Interval.microseconds`, `pg8000.Interval.days`, 994 | and `pg8000.Interval.months` properties are independently measured and cannot 995 | be converted to each other. A month may be 28, 29, 30, or 31 days, and a day 996 | may occasionally be lengthened slightly by a leap second. 997 | 998 | 999 | ===== pg8000.Interval.microseconds 1000 | 1001 | Measure of microseconds in the interval. 1002 | 1003 | The microseconds value is constrained to fit into a signed 64-bit integer. Any 1004 | attempt to set a value too large or too small will result in an OverflowError 1005 | being raised. 1006 | 1007 | 1008 | ===== pg8000.Interval.days 1009 | 1010 | Measure of days in the interval. 1011 | 1012 | The days value is constrained to fit into a signed 32-bit integer. Any attempt 1013 | to set a value too large or too small will result in an OverflowError being 1014 | raised. 1015 | 1016 | 1017 | ===== pg8000.Interval.months 1018 | 1019 | Measure of months in the interval. 1020 | 1021 | The months value is constrained to fit into a signed 32-bit integer. Any 1022 | attempt to set a value too large or too small will result in an OverflowError 1023 | being raised. 1024 | 1025 | 1026 | == Regression Tests 1027 | 1028 | The easiest way to run the regression tests is using a prebuilt Docker image 1029 | that contains all of the supported versions of Python, Jython, PyPy, and 1030 | PostgreSQL, ready-to-run. To use this, just mount your source directory 1031 | (where you checked out pg8000 to) onto the image in 1032 | `/home/postgres/pg8000-src`, and then run the `mfenniak/pg8000-test-env` image. 1033 | Here's a simple command-line: 1034 | 1035 | docker run -v `pwd`:/home/postgres/pg8000-src mfenniak/pg8000-test-env 1036 | 1037 | If you don't have Docker, or want to run the tests in a different environment, 1038 | install http://testrun.org/tox/latest/[tox]: 1039 | 1040 | pip install tox 1041 | 1042 | then install all the supported Python versions (using the 1043 | https://launchpad.net/~fkrull/+archive/ubuntu/deadsnakes[APT Repository] if 1044 | you're using Ubuntu). Install all the currently supported versions of PostgreSQL 1045 | (using the http://wiki.postgresql.org/wiki/Apt[APT Repository] if you're 1046 | using Ubuntu). Then for each of them, enable the hstore extension by running the 1047 | SQL command: 1048 | 1049 | create extension hstore; 1050 | 1051 | and add a line to pg_hba.conf for the various authentication options, eg. 1052 | 1053 | .... 1054 | host pg8000_md5 all 127.0.0.1/32 md5 1055 | host pg8000_gss all 127.0.0.1/32 gss 1056 | host pg8000_password all 127.0.0.1/32 password 1057 | host all all 127.0.0.1/32 trust 1058 | .... 1059 | 1060 | Set the following environment variables for the databases, for example: 1061 | 1062 | .... 1063 | export PG8000_TEST_NAME="PG8000_TEST_9_5" 1064 | export PG8000_TEST_9_1="{'user': 'postgres', 'password': 'pw', 'port': 5435}" 1065 | export PG8000_TEST_9_2="{'user': 'postgres', 'password': 'pw', 'port': 5434}" 1066 | export PG8000_TEST_9_3="{'user': 'postgres', 'password': 'pw', 'port': 5433}" 1067 | export PG8000_TEST_9_4="{'user': 'postgres', 'password': 'pw', 'port': 5432}" 1068 | export PG8000_TEST_9_5="{'user': 'postgres', 'password': 'pw', 'port': 5431}" 1069 | .... 1070 | 1071 | then run `tox` from the `pg8000` directory: 1072 | 1073 | `tox` 1074 | 1075 | 1076 | == Performance Tests 1077 | 1078 | To run the performance tests from the `pg8000` directory: 1079 | 1080 | python -m pg8000.tests.performance 1081 | 1082 | 1083 | == Stress Test 1084 | 1085 | There's a stress test that is run by doing: 1086 | 1087 | python ./multi 1088 | 1089 | The idea is to set `shared_buffers` in `postgresql.conf` to 128kB, and then 1090 | run the stress test, and you should get `no unpinned buffers` errors. 1091 | 1092 | 1093 | == Doing A Release Of pg8000 1094 | 1095 | Run `tox` to make sure all tests pass, then update `doc/release_notes.rst` then 1096 | do: 1097 | 1098 | .... 1099 | git tag -a x.y.z -m "version x.y.z" 1100 | python setup.py register sdist bdist_wheel upload --sign 1101 | .... 1102 | 1103 | 1104 | == Release Notes 1105 | 1106 | 1107 | === Version 1.11.0, 2017-08-16 1108 | 1109 | Note that this version is not backward compatible with previous versions. 1110 | 1111 | * The Python `int` type was sent as an `unknown` type, but now it's sent as the 1112 | nearest matching PostgreSQL type. Thanks to Patrick Hayes. 1113 | 1114 | * Prepared statements are now closed on the server when pg8000 clears them from 1115 | its cache. 1116 | 1117 | * Previously a `%` within an SQL literal had to be escaped, but this is no 1118 | longer the case. 1119 | 1120 | * Notifications, notices and parameter statuses are now handled by simple 1121 | `dequeue` buffers. See docs for more details. 1122 | 1123 | * Connections and cursors are no longer threadsafe. So to be clear, neither 1124 | connections or cursors should be shared between threads. One thread per 1125 | connection is mandatory now. This has been done for performance reasons, and 1126 | to simplify the code. 1127 | 1128 | * Rather than reading results from the server in batches, pg8000 now always 1129 | downloads them in one go. This avoids `portal closed` errors and makes things 1130 | a bit quicker, but now one has to avoid downloading too many rows in a single 1131 | query. 1132 | 1133 | * Attempts to return something informative if the returned PostgreSQL timestamp 1134 | value is outside the range of the Python datetime. 1135 | 1136 | * Allow empty arrays as parameters, assume they're of string type. 1137 | 1138 | * The cursor now has a context manager, so it can be used with the `with` 1139 | keyword. Thanks to Ildar Musin. 1140 | 1141 | * Add support for `application_name` parameter when connecting to database, 1142 | issue https://github.com/mfenniak/pg8000/pull/106[#106]. Thanks to 1143 | https://github.com/vadv[@vadv] for the contribution. 1144 | 1145 | * Fix warnings from PostgreSQL "not in a transaction", when calling 1146 | ``.rollback()`` while not in a transaction, issue 1147 | https://github.com/mfenniak/pg8000/issues/113[#113]. Thanks to 1148 | https://github.com/jamadden[@jamadden] for the contribution. 1149 | 1150 | * Errors from the server are now always passed through in full. 1151 | 1152 | 1153 | === Version 1.10.6, 2016-06-10 1154 | 1155 | * Fixed a problem where we weren't handling the password connection parameter 1156 | correctly. Now it's handled in the same way as the 'user' and 'database' 1157 | parameters, ie. if the password is bytes, then pass it straight through to the 1158 | database, if it's a string then encode it with utf8. 1159 | 1160 | * It used to be that if the 'user' parameter to the connection function was 1161 | 'None', then pg8000 would try and look at environment variables to find a 1162 | username. Now we just go by the 'user' parameter only, and give an error if 1163 | it's None. 1164 | 1165 | 1166 | === Version 1.10.5, 2016-03-04 1167 | 1168 | - Include LICENCE text and sources for docs in the source distribution (the 1169 | tarball). 1170 | 1171 | 1172 | === Version 1.10.4, 2016-02-27 1173 | 1174 | * Fixed bug where if a str is sent as a query parameter, and then with the same 1175 | cursor an int is sent instead of a string, for the same query, then it fails. 1176 | 1177 | * Under Python 2, a str type is now sent 'as is', ie. as a byte string rather 1178 | than trying to decode and send according to the client encoding. Under Python 1179 | 2 it's recommended to send text as unicode() objects. 1180 | 1181 | * Dropped and added support for Python versions. Now pg8000 supports 1182 | Python 2.7+ and Python 3.3+. 1183 | 1184 | * Dropped and added support for PostgreSQL versions. Now pg8000 supports 1185 | PostgreSQL 9.1+. 1186 | 1187 | * pg8000 uses the 'six' library for making the same code run on both Python 2 1188 | and Python 3. We used to include it as a file in the pg8000 source code. Now 1189 | we have it as a separate dependency that's installed with 'pip install'. The 1190 | reason for doing this is that package maintainers for OS distributions 1191 | prefer unbundled libaries. 1192 | 1193 | 1194 | === Version 1.10.3, 2016-01-07 1195 | 1196 | * Removed testing for PostgreSQL 9.0 as it's not longer supported by the 1197 | PostgreSQL Global Development Group. 1198 | * Fixed bug where pg8000 would fail with datetimes if PostgreSQL was compiled 1199 | with the integer_datetimes option set to 'off'. The bug was in the 1200 | timestamp_send_float function. 1201 | 1202 | 1203 | === Version 1.10.2, 2015-03-17 1204 | 1205 | * If there's a socket exception thrown when communicating with the database, 1206 | it is now wrapped in an OperationalError exception, to conform to the DB-API 1207 | spec. 1208 | 1209 | * Previously, pg8000 didn't recognize the EmptyQueryResponse (that the server 1210 | sends back if the SQL query is an empty string) now we raise a 1211 | ProgrammingError exception. 1212 | 1213 | * Added socket timeout option for Python 3. 1214 | 1215 | * If the server returns an error, we used to initialize the ProgramerException 1216 | with just the first three fields of the error. Now we initialize the 1217 | ProgrammerException with all the fields. 1218 | 1219 | * Use relative imports inside package. 1220 | 1221 | * User and database names given as bytes. The user and database parameters of 1222 | the connect() function are now passed directly as bytes to the server. If the 1223 | type of the parameter is unicode, pg8000 converts it to bytes using the uft8 1224 | encoding. 1225 | 1226 | * Added support for JSON and JSONB Postgres types. We take the approach of 1227 | taking serialized JSON (str) as an SQL parameter, but returning results as 1228 | de-serialized JSON (Python objects). See the example in the Quickstart. 1229 | 1230 | * Added CircleCI continuous integration. 1231 | 1232 | * String support in arrays now allow letters like "u", braces and whitespace. 1233 | 1234 | 1235 | === Version 1.10.1, 2014-09-15 1236 | 1237 | * Add support for the Wheel package format. 1238 | 1239 | * Remove option to set a connection timeout. For communicating with the server, 1240 | pg8000 uses a file-like object using socket.makefile() but you can't use this 1241 | if the underlying socket has a timeout. 1242 | 1243 | 1244 | === Version 1.10.0, 2014-08-30 1245 | 1246 | * Remove the old ``pg8000.dbapi`` and ``pg8000.DBAPI`` namespaces. For example, 1247 | now only ``pg8000.connect()`` will work, and ``pg8000.dbapi.connect()`` 1248 | won't work any more. 1249 | 1250 | * Parse server version string with LooseVersion. This should solve the problems 1251 | that people have been having when using versions of PostgreSQL such as 1252 | ``9.4beta2``. 1253 | 1254 | * Message if portal suspended in autocommit. Give a proper error message if the 1255 | portal is suspended while in autocommit mode. The error is that the portal is 1256 | closed when the transaction is closed, and so in autocommit mode the portal 1257 | will be immediately closed. The bottom line is, don't use autocommit mode if 1258 | there's a chance of retrieving more rows than the cache holds (currently 100). 1259 | 1260 | 1261 | === Version 1.9.14, 2014-08-02 1262 | 1263 | * Make ``executemany()`` set ``rowcount``. Previously, ``executemany()`` would 1264 | always set ``rowcount`` to -1. Now we set it to a meaningful value if 1265 | possible. If any of the statements have a -1 ``rowcount`` then then the 1266 | ``rowcount`` for the ``executemany()`` is -1, otherwise the ``executemany()`` 1267 | ``rowcount`` is the sum of the rowcounts of the individual statements. 1268 | 1269 | * Support for password authentication. pg8000 didn't support plain text 1270 | authentication, now it does. 1271 | 1272 | 1273 | === Version 1.9.13, 2014-07-27 1274 | 1275 | * Reverted to using the string ``connection is closed`` as the message of the 1276 | exception that's thrown if a connection is closed. For a few versions we were 1277 | using a slightly different one with capitalization and punctuation, but we've 1278 | reverted to the original because it's easier for users of the library to 1279 | consume. 1280 | 1281 | * Previously, ``tpc_recover()`` would start a transaction if one was not already 1282 | in progress. Now it won't. 1283 | 1284 | 1285 | === Version 1.9.12, 2014-07-22 1286 | 1287 | * Fixed bug in ``tpc_commit()`` where a single phase commit failed. 1288 | 1289 | 1290 | === Version 1.9.11, 2014-07-20 1291 | 1292 | * Add support for two-phase commit DBAPI extension. Thanks to Mariano Reingart's 1293 | TPC code on the Google Code version: 1294 | 1295 | https://code.google.com/p/pg8000/source/detail?r=c8609701b348b1812c418e2c7 1296 | 1297 | on which the code for this commit is based. 1298 | 1299 | * Deprecate ``copy_from()`` and ``copy_to()`` The methods ``copy_from()`` and 1300 | ``copy_to()`` of the ``Cursor`` object are deprecated because it's simpler and 1301 | more flexible to use the ``execute()`` method with a ``fileobj`` parameter. 1302 | 1303 | * Fixed bug in reporting unsupported authentication codes. Thanks to 1304 | https://github.com/hackgnar for reporting this and providing the fix. 1305 | 1306 | * Have a default for the ``user`` paramater of the ``connect()`` function. If 1307 | the ``user`` parameter of the ``connect()`` function isn't provided, look 1308 | first for the ``PGUSER`` then the ``USER`` environment variables. Thanks to 1309 | Alex Gaynor https://github.com/alex for this suggestion. 1310 | 1311 | * Before PostgreSQL 8.2, ``COPY`` didn't give row count. Until PostgreSQL 8.2 1312 | (which includes Amazon Redshift which forked at 8.0) the ``COPY`` command 1313 | didn't return a row count, but pg8000 thought it did. That's fixed now. 1314 | 1315 | 1316 | === Version 1.9.10, 2014-06-08 1317 | 1318 | * Remember prepared statements. Now prepared statements are never closed, and 1319 | pg8000 remembers which ones are on the server, and uses them when a query is 1320 | repeated. This gives an increase in performance, because on subsequent 1321 | queries the prepared statement doesn't need to be created each time. 1322 | 1323 | * For performance reasons, pg8000 never closed portals explicitly, it just 1324 | let the server close them at the end of the transaction. However, this can 1325 | cause memory problems for long running transactions, so now pg800 always 1326 | closes a portal after it's exhausted. 1327 | 1328 | * Fixed bug where unicode arrays failed under Python 2. Thanks to 1329 | https://github.com/jdkx for reporting this. 1330 | 1331 | * A FLUSH message is now sent after every message (except SYNC). This is in 1332 | accordance with the protocol docs, and ensures the server sends back its 1333 | responses straight away. 1334 | 1335 | 1336 | === Version 1.9.9, 2014-05-12 1337 | 1338 | * The PostgreSQL interval type is now mapped to datetime.timedelta where 1339 | possible. Previously the PostgreSQL interval type was always mapped to the 1340 | pg8000.Interval type. However, to support the datetime.timedelta type we 1341 | now use it whenever possible. Unfortunately it's not always possible because 1342 | timedelta doesn't support months. If months are needed then the fall-back 1343 | is the pg8000.Interval type. This approach means we handle timedelta in a 1344 | similar way to other Python PostgreSQL drivers, and it makes pg8000 1345 | compatible with popular ORMs like SQLAlchemy. 1346 | 1347 | * Fixed bug in executemany() where a new prepared statement should be created 1348 | for each variation in the oids of the parameter sets. 1349 | 1350 | 1351 | === Version 1.9.8, 2014-05-05 1352 | 1353 | * We used to ask the server for a description of the statement, and then ask 1354 | for a description of each subsequent portal. We now only ask for a 1355 | description of the statement. This results in a significant performance 1356 | improvement, especially for executemany() calls and when using the 1357 | 'use_cache' option of the connect() function. 1358 | 1359 | * Fixed warning in Python 3.4 which was saying that a socket hadn't been 1360 | closed. It seems that closing a socket file doesn't close the underlying 1361 | socket. 1362 | 1363 | * Now should cope with PostgreSQL 8 versions before 8.4. This includes Amazon 1364 | Redshift. 1365 | 1366 | * Added 'unicode' alias for 'utf-8', which is needed for Amazon Redshift. 1367 | 1368 | * Various other bug fixes. 1369 | 1370 | 1371 | === Version 1.9.7, 2014-03-26 1372 | 1373 | * Caching of prepared statements. There's now a 'use_cache' boolean parameter 1374 | for the connect() function, which causes all prepared statements to be cached 1375 | by pg8000, keyed on the SQL query string. This should speed things up 1376 | significantly in most cases. 1377 | 1378 | * Added support for the PostgreSQL inet type. It maps to the Python types 1379 | IPv*Address and IPv*Network. 1380 | 1381 | * Added support for PostgreSQL +/- infinity date and timestamp values. Now the 1382 | Python value datetime.datetime.max maps to the PostgreSQL value 'infinity' 1383 | and datetime.datetime.min maps to '-infinity', and the same for 1384 | datetime.date. 1385 | 1386 | * Added support for the PostgreSQL types int2vector and xid, which are mostly 1387 | used internally by PostgreSQL. 1388 | 1389 | 1390 | === Version 1.9.6, 2014-02-26 1391 | 1392 | * Fixed a bug where 'portal does not exist' errors were being generated. Some 1393 | queries that should have been run in a transaction were run in autocommit 1394 | mode and so any that suspended a portal had the portal immediately closed, 1395 | because a portal can only exist within a transaction. This has been solved by 1396 | determining the transaction status from the READY_FOR_QUERY message. 1397 | 1398 | 1399 | === Version 1.9.5, 2014-02-15 1400 | 1401 | * Removed warn() calls for __next__() and __iter__(). Removing the warn() in 1402 | __next__() improves the performance tests by ~20%. 1403 | 1404 | * Increased performance of timestamp by ~20%. Should also improve timestamptz. 1405 | 1406 | * Moved statement_number and portal_number from module to Connection. This 1407 | should reduce lock contention for cases where there's a single module and 1408 | lots of connections. 1409 | 1410 | * Make decimal_out/in and time_in use client_encoding. These functions used to 1411 | assume ascii, and I can't think of a case where that wouldn't work. 1412 | Nonetheless, that theoretical bug is now fixed. 1413 | 1414 | * Fixed a bug in cursor.executemany(), where a non-None parameter in a sequence 1415 | of parameters, is None in a subsequent sequence of parameters. 1416 | 1417 | 1418 | === Version 1.9.4, 2014-01-18 1419 | 1420 | * Fixed a bug where with Python 2, a parameter with the value Decimal('12.44'), 1421 | (and probably other numbers) isn't sent correctly to PostgreSQL, and so the 1422 | command fails. This has been fixed by sending decimal types as text rather 1423 | than binary. I'd imagine it's slightly faster too. 1424 | 1425 | 1426 | === Version 1.9.3, 2014-01-16 1427 | 1428 | * Fixed bug where there were missing trailing zeros after the decimal point in 1429 | the NUMERIC type. For example, the NUMERIC value 1.0 was returned as 1 (with 1430 | no zero after the decimal point). 1431 | 1432 | This is fixed this by making pg8000 use the text rather than binary 1433 | representation for the numeric type. This actually doubles the speed of 1434 | numeric queries. 1435 | 1436 | 1437 | === Version 1.9.2, 2013-12-17 1438 | 1439 | * Fixed incompatibility with PostgreSQL 8.4. In 8.4, the CommandComplete 1440 | message doesn't return a row count if the command is SELECT. We now look at 1441 | the server version and don't look for a row count for a SELECT with version 1442 | 8.4. 1443 | 1444 | 1445 | === Version 1.9.1, 2013-12-15 1446 | 1447 | * Fixed bug where the Python 2 'unicode' type wasn't recognized in a query 1448 | parameter. 1449 | 1450 | 1451 | === Version 1.9.0, 2013-12-01 1452 | 1453 | * For Python 3, the :class:`bytes` type replaces the :class:`pg8000.Bytea` 1454 | type. For backward compatibility the :class:`pg8000.Bytea` still works under 1455 | Python 3, but its use is deprecated. 1456 | 1457 | * A single codebase for Python 2 and 3. 1458 | 1459 | * Everything (functions, properties, classes) is now available under the 1460 | ``pg8000`` namespace. So for example: 1461 | 1462 | * pg8000.DBAPI.connect() -> pg8000.connect() 1463 | * pg8000.DBAPI.apilevel -> pg8000.apilevel 1464 | * pg8000.DBAPI.threadsafety -> pg8000.threadsafety 1465 | * pg8000.DBAPI.paramstyle -> pg8000.paramstyle 1466 | * pg8000.types.Bytea -> pg8000.Bytea 1467 | * pg8000.types.Interval -> pg8000.Interval 1468 | * pg8000.errors.Warning -> pg8000.Warning 1469 | * pg8000.errors.Error -> pg8000.Error 1470 | * pg8000.errors.InterfaceError -> pg8000.InterfaceError 1471 | * pg8000.errors.DatabaseError -> pg8000.DatabaseError 1472 | 1473 | The old locations are deprecated, but still work for backward compatibility. 1474 | 1475 | * Lots of performance improvements. 1476 | 1477 | * Faster receiving of ``numeric`` types. 1478 | * Query only parsed when PreparedStatement is created. 1479 | * PreparedStatement re-used in executemany() 1480 | * Use ``collections.deque`` rather than ``list`` for the row cache. We're 1481 | adding to one end and removing from the other. This is O(n) for a list but 1482 | O(1) for a deque. 1483 | * Find the conversion function and do the format code check in the 1484 | ROW_DESCRIPTION handler, rather than every time in the ROW_DATA handler. 1485 | * Use the 'unpack_from' form of struct, when unpacking the data row, so we 1486 | don't have to slice the data. 1487 | * Return row as a list for better performance. At the moment result rows are 1488 | turned into a tuple before being returned. Returning the rows directly as a 1489 | list speeds up the performance tests about 5%. 1490 | * Simplify the event loop. Now the main event loop just continues until a 1491 | READY_FOR_QUERY message is received. This follows the suggestion in the 1492 | Postgres protocol docs. There's not much of a difference in speed, but the 1493 | code is a bit simpler, and it should make things more robust. 1494 | * Re-arrange the code as a state machine to give > 30% speedup. 1495 | * Using pre-compiled struct objects. Pre-compiled struct objects are a bit 1496 | faster than using the struct functions directly. It also hopefully adds to 1497 | the readability of the code. 1498 | * Speeded up _send. Before calling the socket 'write' method, we were 1499 | checking that the 'data' type implements the 'buffer' interface (bytes or 1500 | bytearray), but the check isn't needed because 'write' raises an exception 1501 | if data is of the wrong type. 1502 | 1503 | 1504 | * Add facility for turning auto-commit on. This follows the suggestion of 1505 | funkybob to fix the problem of not be able to execute a command such as 1506 | 'create database' that must be executed outside a transaction. Now you can do 1507 | conn.autocommit = True and then execute 'create database'. 1508 | 1509 | * Add support for the PostgreSQL ``uid`` type. Thanks to Rad Cirskis. 1510 | 1511 | * Add support for the PostgreSQL XML type. 1512 | 1513 | * Add support for the PostgreSQL ``enum`` user defined types. 1514 | 1515 | * Fix a socket leak, where a problem opening a connection could leave a socket 1516 | open. 1517 | 1518 | * Fix empty array issue. https://github.com/mfenniak/pg8000/issues/10 1519 | 1520 | * Fix scale on ``numeric`` types. https://github.com/mfenniak/pg8000/pull/13 1521 | 1522 | * Fix numeric_send. Thanks to Christian Hofstaedtler. 1523 | 1524 | 1525 | === Version 1.08, 2010-06-08 1526 | 1527 | * Removed usage of deprecated :mod:`md5` module, replaced with :mod:`hashlib`. 1528 | Thanks to Gavin Sherry for the patch. 1529 | 1530 | * Start transactions on execute or executemany, rather than immediately at the 1531 | end of previous transaction. Thanks to Ben Moran for the patch. 1532 | 1533 | * Add encoding lookups where needed, to address usage of SQL_ASCII encoding. 1534 | Thanks to Benjamin Schweizer for the patch. 1535 | 1536 | * Remove record type cache SQL query on every new pg8000 connection. 1537 | 1538 | * Fix and test SSL connections. 1539 | 1540 | * Handle out-of-band messages during authentication. 1541 | 1542 | 1543 | === Version 1.07, 2009-01-06 1544 | 1545 | * Added support for :meth:`~pg8000.dbapi.CursorWrapper.copy_to` and 1546 | :meth:`~pg8000.dbapi.CursorWrapper.copy_from` methods on cursor objects, to 1547 | allow the usage of the PostgreSQL COPY queries. Thanks to Bob Ippolito for 1548 | the original patch. 1549 | 1550 | * Added the :attr:`~pg8000.dbapi.ConnectionWrapper.notifies` and 1551 | :attr:`~pg8000.dbapi.ConnectionWrapper.notifies_lock` attributes to DBAPI 1552 | connection objects to provide access to server-side event notifications. 1553 | Thanks again to Bob Ippolito for the original patch. 1554 | 1555 | * Improved performance using buffered socket I/O. 1556 | 1557 | * Added valid range checks for :class:`~pg8000.types.Interval` attributes. 1558 | 1559 | * Added binary transmission of :class:`~decimal.Decimal` values. This permits 1560 | full support for NUMERIC[] types, both send and receive. 1561 | 1562 | * New `Sphinx `_-based website and documentation. 1563 | 1564 | 1565 | === Version 1.06, 2008-12-09 1566 | 1567 | * pg8000-py3: a branch of pg8000 fully supporting Python 3.0. 1568 | 1569 | * New Sphinx-based documentation. 1570 | 1571 | * Support for PostgreSQL array types -- INT2[], INT4[], INT8[], FLOAT[], 1572 | DOUBLE[], BOOL[], and TEXT[]. New support permits both sending and 1573 | receiving these values. 1574 | 1575 | * Limited support for receiving RECORD types. If a record type is received, 1576 | it will be translated into a Python dict object. 1577 | 1578 | * Fixed potential threading bug where the socket lock could be lost during 1579 | error handling. 1580 | 1581 | 1582 | === Version 1.05, 2008-09-03 1583 | 1584 | * Proper support for timestamptz field type: 1585 | 1586 | * Reading a timestamptz field results in a datetime.datetime instance that 1587 | has a valid tzinfo property. tzinfo is always UTC. 1588 | 1589 | * Sending a datetime.datetime instance with a tzinfo value will be 1590 | sent as a timestamptz type, with the appropriate tz conversions done. 1591 | 1592 | * Map postgres < -- > python text encodings correctly. 1593 | 1594 | * Fix bug where underscores were not permitted in pyformat names. 1595 | 1596 | * Support "%s" in a pyformat strin. 1597 | 1598 | * Add cursor.connection DB-API extension. 1599 | 1600 | * Add cursor.next and cursor.__iter__ DB-API extensions. 1601 | 1602 | * DBAPI documentation improvements. 1603 | 1604 | * Don't attempt rollback in cursor.execute if a ConnectionClosedError occurs. 1605 | 1606 | * Add warning for accessing exceptions as attributes on the connection object, 1607 | as per DB-API spec. 1608 | 1609 | * Fix up open connection when an unexpected connection occurs, rather than 1610 | leaving the connection in an unusable state. 1611 | 1612 | * Use setuptools/egg package format. 1613 | 1614 | 1615 | === Version 1.04, 2008-05-12 1616 | 1617 | * DBAPI 2.0 compatibility: 1618 | 1619 | * rowcount returns rows affected when appropriate (eg. UPDATE, DELETE) 1620 | 1621 | * Fix CursorWrapper.description to return a 7 element tuple, as per spec. 1622 | 1623 | * Fix CursorWrapper.rowcount when using executemany. 1624 | 1625 | * Fix CursorWrapper.fetchmany to return an empty sequence when no more 1626 | results are available. 1627 | 1628 | * Add access to DBAPI exceptions through connection properties. 1629 | 1630 | * Raise exception on closing a closed connection. 1631 | 1632 | * Change DBAPI.STRING to varchar type. 1633 | 1634 | * rowcount returns -1 when appropriate. 1635 | 1636 | * DBAPI implementation now passes Stuart Bishop's Python DB API 2.0 Anal 1637 | Compliance Unit Test. 1638 | 1639 | * Make interface.Cursor class use unnamed prepared statement that binds to 1640 | parameter value types. This change increases the accuracy of PG's query 1641 | plans by including parameter information, hence increasing performance in 1642 | some scenarios. 1643 | 1644 | * Raise exception when reading from a cursor without a result set. 1645 | 1646 | * Fix bug where a parse error may have rendered a connection unusable. 1647 | 1648 | 1649 | === Version 1.03, 2008-05-09 1650 | 1651 | * Separate pg8000.py into multiple python modules within the pg8000 package. 1652 | There should be no need for a client to change how pg8000 is imported. 1653 | 1654 | * Fix bug in row_description property when query has not been completed. 1655 | 1656 | * Fix bug in fetchmany dbapi method that did not properly deal with the end of 1657 | result sets. 1658 | 1659 | * Add close methods to DB connections. 1660 | 1661 | * Add callback event handlers for server notices, notifications, and runtime 1662 | configuration changes. 1663 | 1664 | * Add boolean type output. 1665 | 1666 | * Add date, time, and timestamp types in/out. 1667 | 1668 | * Add recognition of "SQL_ASCII" client encoding, which maps to Python's 1669 | "ascii" encoding. 1670 | 1671 | * Add types.Interval class to represent PostgreSQL's interval data type, and 1672 | appropriate wire send/receive methods. 1673 | 1674 | * Remove unused type conversion methods. 1675 | 1676 | 1677 | === Version 1.02, 2007-03-13 1678 | 1679 | * Add complete DB-API 2.0 interface. 1680 | 1681 | * Add basic SSL support via ssl connect bool. 1682 | 1683 | * Rewrite pg8000_test.py to use Python's unittest library. 1684 | 1685 | * Add bytea type support. 1686 | 1687 | * Add support for parameter output types: NULL value, timestamp value, python 1688 | long value. 1689 | 1690 | * Add support for input parameter type oid. 1691 | 1692 | 1693 | === Version 1.01, 2007-03-09 1694 | 1695 | * Add support for writing floats and decimal objs up to PG backend. 1696 | 1697 | * Add new error handling code and tests to make sure connection can recover 1698 | from a database error. 1699 | 1700 | * Fixed bug where timestamp types were not always returned in the same binary 1701 | format from the PG backend. Text format is now being used to send 1702 | timestamps. 1703 | 1704 | * Fixed bug where large packets from the server were not being read fully, due 1705 | to socket.read not always returning full read size requested. It was a 1706 | lazy-coding bug. 1707 | 1708 | * Added locks to make most of the library thread-safe. 1709 | 1710 | * Added UNIX socket support. 1711 | 1712 | 1713 | === Version 1.00, 2007-03-08 1714 | 1715 | * First public release. Although fully functional, this release is mostly 1716 | lacking in production testing and in type support. 1717 | --------------------------------------------------------------------------------