diff -Nru psycopg2-2.8.5/debian/changelog psycopg2-2.8.6/debian/changelog --- psycopg2-2.8.5/debian/changelog 2020-12-07 17:20:26.000000000 +0000 +++ psycopg2-2.8.6/debian/changelog 2021-01-19 15:35:54.000000000 +0000 @@ -1,14 +1,26 @@ -psycopg2 (2.8.5-1build2) hirsute; urgency=medium +psycopg2 (2.8.6-2) unstable; urgency=medium - * No-change rebuild to drop python3.8 extensions. + * Fix arch-indep build. - -- Matthias Klose Mon, 07 Dec 2020 18:20:26 +0100 + -- Christoph Berg Tue, 19 Jan 2021 16:35:54 +0100 -psycopg2 (2.8.5-1build1) hirsute; urgency=medium +psycopg2 (2.8.6-1) unstable; urgency=medium - * No-change rebuild to build with python3.9 as supported. + [ Ondřej Nový ] + * d/control: Update Maintainer field with new Debian Python Team + contact address. + * d/control: Update Vcs-* fields with new Debian Python Team Salsa + layout. - -- Matthias Klose Sat, 24 Oct 2020 10:53:30 +0200 + [ Debian Janitor ] + * Apply multi-arch hints. + + python3-psycopg2-dbg: Add Multi-Arch: same. + + [ Christoph Berg ] + * New upstream version 2.8.6. + * DH 13. + + -- Christoph Berg Tue, 19 Jan 2021 13:52:54 +0100 psycopg2 (2.8.5-1) unstable; urgency=medium diff -Nru psycopg2-2.8.5/debian/control psycopg2-2.8.6/debian/control --- psycopg2-2.8.5/debian/control 2020-04-08 04:43:35.000000000 +0000 +++ psycopg2-2.8.6/debian/control 2021-01-19 13:15:33.000000000 +0000 @@ -2,7 +2,7 @@ Section: python Priority: optional Build-Depends: - debhelper-compat (= 9), + debhelper-compat (= 13), dh-python, libpq-dev, postgresql, @@ -11,14 +11,15 @@ python3-setuptools, python3-sphinx (>= 1.0.7+dfsg-1~), Build-Depends-Indep: python3-doc -Maintainer: Debian Python Modules Team +Maintainer: Debian Python Team Uploaders: Fabio Tranchitella , Scott Kitterman , Christoph Berg , Standards-Version: 4.5.0 -Vcs-Git: https://salsa.debian.org/python-team/modules/psycopg2.git -Vcs-Browser: https://salsa.debian.org/python-team/modules/psycopg2 +Rules-Requires-Root: no +Vcs-Git: https://salsa.debian.org/python-team/packages/psycopg2.git +Vcs-Browser: https://salsa.debian.org/python-team/packages/psycopg2 Homepage: http://initd.org/projects/psycopg Package: python3-psycopg2 @@ -50,6 +51,7 @@ python3-psycopg2 (= ${binary:Version}), ${misc:Depends}, ${shlibs:Depends}, +Multi-Arch: same Description: Python 3 module for PostgreSQL (debug extension) psycopg is a PostgreSQL database adapter for the Python3 programming language (just like pygresql and popy.) This is version 2, a complete rewrite of the diff -Nru psycopg2-2.8.5/debian/patches/0007-Use-default-sphinx-theme.patch psycopg2-2.8.6/debian/patches/0007-Use-default-sphinx-theme.patch --- psycopg2-2.8.5/debian/patches/0007-Use-default-sphinx-theme.patch 2020-04-08 04:54:00.000000000 +0000 +++ psycopg2-2.8.6/debian/patches/0007-Use-default-sphinx-theme.patch 2021-01-19 12:50:54.000000000 +0000 @@ -28,8 +28,18 @@ # The stylesheet to use with HTML output: this will include the original one # adding a few classes. -@@ -156,7 +156,7 @@ html_theme_options = { - } +@@ -150,13 +150,13 @@ html_show_sphinx = False + # Theme options are theme-specific and customize the look and feel of a theme + # further. For a list of options available for each theme, see the + # documentation. +-html_theme_options = { +- 'linktotheme': False, +- 'cssfiles': ['_static/psycopg.css'], +-} ++#html_theme_options = { ++# 'linktotheme': False, ++# 'cssfiles': ['_static/psycopg.css'], ++#} # Add any paths that contain custom themes here, relative to this directory. -html_theme_path = [better_theme_path] diff -Nru psycopg2-2.8.5/debian/python-psycopg2-doc.examples psycopg2-2.8.6/debian/python-psycopg2-doc.examples --- psycopg2-2.8.5/debian/python-psycopg2-doc.examples 2020-04-08 04:43:35.000000000 +0000 +++ psycopg2-2.8.6/debian/python-psycopg2-doc.examples 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -examples/* diff -Nru psycopg2-2.8.5/debian/rules psycopg2-2.8.6/debian/rules --- psycopg2-2.8.5/debian/rules 2020-04-08 04:43:35.000000000 +0000 +++ psycopg2-2.8.6/debian/rules 2021-01-19 15:35:54.000000000 +0000 @@ -10,11 +10,8 @@ %: dh $@ --with python3,sphinxdoc --buildsystem=pybuild -override_dh_auto_build: - dh_auto_build -a - -build-indep: - $(DEFAULT_PYTHON3) setup.py build +override_dh_auto_build-indep: + dh_auto_build -i set -e; export PYTHONPATH=$(shell pybuild --print build_dir --interpreter python3) PYTHON_VERSION=3; \ cd $(CURDIR)/doc; make src/sqlstate_errors.rst; \ cd $(CURDIR)/doc/src; make html; \ @@ -28,7 +25,7 @@ override_dh_strip: dh_strip --no-automatic-dbgsym -override_dh_auto_test: +override_dh_auto_test-arch: ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS))) ifneq ($(DEB_HOST_ARCH),hurd-i386) LANG=C.UTF-8 LC_ALL=C.UTF-8 \ @@ -38,3 +35,5 @@ pg_virtualenv dh_auto_test endif endif + +override_dh_auto_test-indep: diff -Nru psycopg2-2.8.5/doc/src/advanced.rst psycopg2-2.8.6/doc/src/advanced.rst --- psycopg2-2.8.5/doc/src/advanced.rst 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/doc/src/advanced.rst 2020-09-05 22:39:27.000000000 +0000 @@ -490,7 +490,7 @@ .. _Eventlet: https://eventlet.net/ .. _gevent: http://www.gevent.org/ .. _SQLAlchemy: https://www.sqlalchemy.org/ -.. _psycogreen: http://bitbucket.org/dvarrazzo/psycogreen/ +.. _psycogreen: https://github.com/psycopg/psycogreen/ .. __: https://www.postgresql.org/docs/current/static/libpq-async.html .. warning:: diff -Nru psycopg2-2.8.5/doc/src/connection.rst psycopg2-2.8.6/doc/src/connection.rst --- psycopg2-2.8.5/doc/src/connection.rst 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/doc/src/connection.rst 2020-09-05 22:39:27.000000000 +0000 @@ -21,6 +21,28 @@ Connections are thread safe and can be shared among many threads. See :ref:`thread-safety` for details. + Connections can be used as context managers. Note that a context wraps a + transaction: if the context exits with success the transaction is + committed, if it exits with an exception the transaction is rolled back. + Note that the connection is not closed by the context and it can be used + for several contexts. + + .. code:: python + + conn = psycopg2.connect(DSN) + + with conn: + with conn.cursor() as curs: + curs.execute(SQL1) + + with conn: + with conn.cursor() as curs: + curs.execute(SQL2) + + # leaving contexts doesn't close the connection + conn.close() + + .. method:: cursor(name=None, cursor_factory=None, scrollable=None, withhold=False) Return a new `cursor` object using the connection. @@ -724,6 +746,7 @@ raw connection structure to C functions, e.g. via `ctypes`:: >>> import ctypes + >>> import ctypes.util >>> libpq = ctypes.pydll.LoadLibrary(ctypes.util.find_library('pq')) >>> libpq.PQserverVersion.argtypes = [ctypes.c_void_p] >>> libpq.PQserverVersion.restype = ctypes.c_int diff -Nru psycopg2-2.8.5/doc/src/cursor.rst psycopg2-2.8.6/doc/src/cursor.rst --- psycopg2-2.8.5/doc/src/cursor.rst 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/doc/src/cursor.rst 2020-09-05 22:39:27.000000000 +0000 @@ -34,6 +34,16 @@ many cursors from the same connection and should use each cursor from a single thread. See :ref:`thread-safety` for details. + Cursors can be used as context managers: leaving the context will close + the cursor. + + .. code:: python + + with conn.cursor() as curs: + curs.execute(SQL) + + # the cursor is now closed + .. attribute:: description diff -Nru psycopg2-2.8.5/doc/src/errorcodes.rst psycopg2-2.8.6/doc/src/errorcodes.rst --- psycopg2-2.8.5/doc/src/errorcodes.rst 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/doc/src/errorcodes.rst 2020-09-05 22:39:27.000000000 +0000 @@ -50,7 +50,7 @@ '42P01' Constants representing all the error values defined by PostgreSQL versions -between 8.1 and 12 are included in the module. +between 8.1 and 13 are included in the module. .. autofunction:: lookup(code) diff -Nru psycopg2-2.8.5/doc/src/errors.rst psycopg2-2.8.6/doc/src/errors.rst --- psycopg2-2.8.5/doc/src/errors.rst 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/doc/src/errors.rst 2020-09-05 22:39:27.000000000 +0000 @@ -12,11 +12,13 @@ .. versionchanged:: 2.8.4 added errors introduced in PostgreSQL 12 +.. versionchanged:: 2.8.6 added errors introduced in PostgreSQL 13 + This module exposes the classes psycopg raises upon receiving an error from the database with a :sql:`SQLSTATE` value attached (available in the `~psycopg2.Error.pgcode` attribute). The content of the module is generated from the PostgreSQL source code and includes classes for every error defined -by PostgreSQL in versions between 9.1 and 12. +by PostgreSQL in versions between 9.1 and 13. Every class in the module is named after what referred as "condition name" `in the documentation`__, converted to CamelCase: e.g. the error 22012, diff -Nru psycopg2-2.8.5/doc/src/extras.rst psycopg2-2.8.6/doc/src/extras.rst --- psycopg2-2.8.5/doc/src/extras.rst 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/doc/src/extras.rst 2020-09-05 22:39:27.000000000 +0000 @@ -41,8 +41,8 @@ Dictionary-like cursor ^^^^^^^^^^^^^^^^^^^^^^ -The dict cursors allow to access to the retrieved records using an interface -similar to the Python dictionaries instead of the tuples. +The dict cursors allow to access to the attributes of retrieved records +using an interface similar to the Python dictionaries instead of the tuples. >>> dict_cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) >>> dict_cur.execute("INSERT INTO test (num, data) VALUES(%s, %s)", diff -Nru psycopg2-2.8.5/doc/src/faq.rst psycopg2-2.8.6/doc/src/faq.rst --- psycopg2-2.8.5/doc/src/faq.rst 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/doc/src/faq.rst 2020-09-05 22:39:27.000000000 +0000 @@ -328,7 +328,7 @@ I can't compile `!psycopg2`: the compiler says *error: Python.h: No such file or directory*. What am I missing? You need to install a Python development package: it is usually called - ``python-dev``. + ``python-dev`` or ``python3-dev`` according to your Python version. .. _faq-libpq-fe-h: diff -Nru psycopg2-2.8.5/doc/src/install.rst psycopg2-2.8.6/doc/src/install.rst --- psycopg2-2.8.5/doc/src/install.rst 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/doc/src/install.rst 2020-09-05 22:39:27.000000000 +0000 @@ -8,26 +8,116 @@ Psycopg is a PostgreSQL_ adapter for the Python_ programming language. It is a wrapper for the libpq_, the official PostgreSQL client library. -The `psycopg2` package is the current mature implementation of the adapter: it -is a C extension and as such it is only compatible with CPython_. If you want -to use Psycopg on a different Python implementation (PyPy, Jython, IronPython) -there is a couple of alternative: +.. _PostgreSQL: https://www.postgresql.org/ +.. _Python: https://www.python.org/ -- a `Ctypes port`__, but it is not as mature as the C implementation yet - and it is not as feature-complete; -- a `CFFI port`__ which is currently more used and reported more efficient on - PyPy, but plese be careful to its version numbers because they are not - aligned to the official psycopg2 ones and some features may differ. +.. index:: + single: Install; from PyPI + single: Install; wheel + single: Wheel -.. _PostgreSQL: https://www.postgresql.org/ -.. _Python: https://www.python.org/ -.. _libpq: https://www.postgresql.org/docs/current/static/libpq.html -.. _CPython: https://en.wikipedia.org/wiki/CPython -.. _Ctypes: https://docs.python.org/library/ctypes.html -.. __: https://github.com/mvantellingen/psycopg2-ctypes -.. __: https://github.com/chtd/psycopg2cffi +.. _binary-packages: + +Quick Install +------------- + +For most operating systems, the quickest way to install Psycopg is using the +wheel_ package available on PyPI_: + +.. code-block:: console + + $ pip install psycopg2-binary + +This will install a pre-compiled binary version of the module which does not +require the build or runtime prerequisites described below. Make sure to use +an up-date-date version of :program:`pip` (you can upgrade it using something +like ``pip install -U pip``). + +You may then import the ``psycopg`` package, as usual: + +.. code-block:: python + + import psycopg + + # Connect to your postgres DB + conn = psycopg.connect("dbname=test user=postgres") + + # Open a cursor to perform database operations + cur = conn.cursor() + # Execute a query + cur.execute("SELECT * FROM my_data"); + + # Retrieve query results + records = cur.fetchall() + +.. _PyPI: https://pypi.org/project/psycopg2-binary/ +.. _wheel: https://pythonwheels.com/ + + +psycopg vs psycopg-binary +^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``psycopg2-binary`` package is meant for beginners to start playing +with Python and PostgreSQL without the need to meet the build +requirements. + +If you are the maintainer of a published package depending on `!psycopg2` +you shouldn't use ``psycopg2-binary`` as a module dependency. **For +production use you are advised to use the source distribution.** + +The binary packages come with their own versions of a few C libraries, +among which ``libpq`` and ``libssl``, which will be used regardless of other +libraries available on the client: upgrading the system libraries will not +upgrade the libraries used by `!psycopg2`. Please build `!psycopg2` from +source if you want to maintain binary upgradeability. + +.. warning:: + + The `!psycopg2` wheel package comes packaged, among the others, with its + own ``libssl`` binary. This may create conflicts with other extension + modules binding with ``libssl`` as well, for instance with the Python + `ssl` module: in some cases, under concurrency, the interaction between + the two libraries may result in a segfault. In case of doubts you are + advised to use a package built from source. + + +.. index:: + single: Install; disable wheel + single: Wheel; disable + +.. _disable-wheel: + +Change in binary packages between Psycopg 2.7 and 2.8 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In version 2.7.x, :command:`pip install psycopg2` would have tried to install +automatically the binary package of Psycopg. Because of concurrency problems +binary packages have displayed, ``psycopg2-binary`` has become a separate +package, and from 2.8 it has become the only way to install the binary +package. + +If you are using Psycopg 2.7 and you want to disable the use of wheel binary +packages, relying on the system libraries available on your client, you +can use the :command:`pip` |--no-binary option|__, e.g.: + +.. code-block:: console + + $ pip install --no-binary :all: psycopg2 + +.. |--no-binary option| replace:: ``--no-binary`` option +.. __: https://pip.pypa.io/en/stable/reference/pip_install/#install-no-binary + +which can be specified in your :file:`requirements.txt` files too, e.g. use: + +.. code-block:: none + + psycopg2>=2.7,<2.8 --no-binary psycopg2 + +to use the last bugfix release of the `!psycopg2` 2.7 package, specifying to +always compile it from source. Of course in this case you will have to meet +the :ref:`build prerequisites `. .. index:: @@ -64,8 +154,9 @@ - A C compiler. - The Python header files. They are usually installed in a package such as - **python-dev**. A message such as *error: Python.h: No such file or - directory* is an indication that the Python headers are missing. + **python-dev** or **python3-dev**. A message such as *error: Python.h: No + such file or directory* is an indication that the Python headers are + missing. - The libpq header files. They are usually installed in a package such as **libpq-dev**. If you get an *error: libpq-fe.h: No such file or directory* @@ -129,101 +220,6 @@ to connect to. - -.. index:: - single: Install; from PyPI - single: Install; wheel - single: Wheel - -.. _binary-packages: - -Binary install from PyPI ------------------------- - -`!psycopg2` is also `available on PyPI`__ in the form of wheel_ packages for -the most common platform (Linux, OSX, Windows): this should make you able to -install a binary version of the module, not requiring the above build or -runtime prerequisites. - -.. note:: - - The ``psycopg2-binary`` package is meant for beginners to start playing - with Python and PostgreSQL without the need to meet the build - requirements. - - If you are the maintainer of a publish package depending on `!psycopg2` - **you shouldn't use 'psycopg2-binary' as a module dependency**. For - production use you are advised to use the source distribution. - - -Make sure to use an up-to-date version of :program:`pip` (you can upgrade it -using something like ``pip install -U pip``), then you can run: - -.. code-block:: console - - $ pip install psycopg2-binary - -.. __: PyPI-binary_ -.. _PyPI-binary: https://pypi.org/project/psycopg2-binary/ -.. _wheel: https://pythonwheels.com/ - -.. note:: - - The binary packages come with their own versions of a few C libraries, - among which ``libpq`` and ``libssl``, which will be used regardless of other - libraries available on the client: upgrading the system libraries will not - upgrade the libraries used by `!psycopg2`. Please build `!psycopg2` from - source if you want to maintain binary upgradeability. - -.. warning:: - - The `!psycopg2` wheel package comes packaged, among the others, with its - own ``libssl`` binary. This may create conflicts with other extension - modules binding with ``libssl`` as well, for instance with the Python - `ssl` module: in some cases, under concurrency, the interaction between - the two libraries may result in a segfault. In case of doubts you are - advised to use a package built from source. - - - -.. index:: - single: Install; disable wheel - single: Wheel; disable - -.. _disable-wheel: - -Change in binary packages between Psycopg 2.7 and 2.8 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -In version 2.7.x, :command:`pip install psycopg2` would have tried to install -automatically the binary package of Psycopg. Because of concurrency problems -binary packages have displayed, ``psycopg2-binary`` has become a separate -package, and from 2.8 it has become the only way to install the binary -package. - -If you are using Psycopg 2.7 and you want to disable the use of wheel binary -packages, relying on the system libraries available on your client, you -can use the :command:`pip` |--no-binary option|__, e.g.: - -.. code-block:: console - - $ pip install --no-binary :all: psycopg2 - -.. |--no-binary option| replace:: ``--no-binary`` option -.. __: https://pip.pypa.io/en/stable/reference/pip_install/#install-no-binary - -which can be specified in your :file:`requirements.txt` files too, e.g. use: - -.. code-block:: none - - psycopg2>=2.7,<2.8 --no-binary psycopg2 - -to use the last bugfix release of the `!psycopg2` 2.7 package, specifying to -always compile it from source. Of course in this case you will have to meet -the :ref:`build prerequisites `. - - - .. index:: single: setup.py single: setup.cfg @@ -285,6 +281,29 @@ .. __: https://pypi.org/project/psycopg2/#files +Non-standard Python Implementation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The `psycopg2` package is the current mature implementation of the adapter: it +is a C extension and as such it is only compatible with CPython_. If you want +to use Psycopg on a different Python implementation (PyPy, Jython, IronPython) +there is a couple of alternative: + +- a `Ctypes port`__, but it is not as mature as the C implementation yet + and it is not as feature-complete; + +- a `CFFI port`__ which is currently more used and reported more efficient on + PyPy, but please be careful of its version numbers because they are not + aligned to the official psycopg2 ones and some features may differ. + +.. _PostgreSQL: https://www.postgresql.org/ +.. _Python: https://www.python.org/ +.. _libpq: https://www.postgresql.org/docs/current/static/libpq.html +.. _CPython: https://en.wikipedia.org/wiki/CPython +.. _Ctypes: https://docs.python.org/library/ctypes.html +.. __: https://github.com/mvantellingen/psycopg2-ctypes +.. __: https://github.com/chtd/psycopg2cffi + .. index:: single: tests @@ -313,7 +332,6 @@ The database should already exist before running the tests. - .. _other-problems: If you still have problems diff -Nru psycopg2-2.8.5/doc/src/usage.rst psycopg2-2.8.6/doc/src/usage.rst --- psycopg2-2.8.5/doc/src/usage.rst 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/doc/src/usage.rst 2020-09-05 22:39:27.000000000 +0000 @@ -750,18 +750,25 @@ The connection is responsible for terminating its transaction, calling either the `~connection.commit()` or `~connection.rollback()` method. Committed -changes are immediately made persistent into the database. Closing the -connection using the `~connection.close()` method or destroying the -connection object (using `!del` or letting it fall out of scope) -will result in an implicit rollback. +changes are immediately made persistent into the database. If he connection +is closed (using the `~connection.close()` method) or destroyed (using `!del` +or letting it falling out of scope) while a transaction is in progress, the +server will discard the transaction. However doing so is not adviceable: +middleware such as PgBouncer_ may see the connection closed uncleanly and +dispose of it. + +.. _PgBouncer: http://www.pgbouncer.org/ It is possible to set the connection in *autocommit* mode: this way all the commands executed will be immediately committed and no rollback is possible. A -few commands (e.g. :sql:`CREATE DATABASE`, :sql:`VACUUM`...) require to be run +few commands (e.g. :sql:`CREATE DATABASE`, :sql:`VACUUM`, :sql:`CALL` on +`stored procedures`__ using transaction control...) require to be run outside any transaction: in order to be able to run these commands from Psycopg, the connection must be in autocommit mode: you can use the `~connection.autocommit` property. +.. __: https://www.postgresql.org/docs/current/xproc.html + .. warning:: By default even a simple :sql:`SELECT` will start a transaction: in @@ -1023,7 +1030,7 @@ .. _lo_export: https://www.postgresql.org/docs/current/static/lo-interfaces.html#LO-EXPORT .. versionchanged:: 2.6 - added support for large objects greated than 2GB. Note that the support is + added support for large objects greater than 2GB. Note that the support is enabled only if all the following conditions are verified: - the Python build is 64 bits; diff -Nru psycopg2-2.8.5/lib/errorcodes.py psycopg2-2.8.6/lib/errorcodes.py --- psycopg2-2.8.5/lib/errorcodes.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/lib/errorcodes.py 2020-09-05 22:39:27.000000000 +0000 @@ -43,7 +43,8 @@ tmp = {} for k, v in globals().items(): if isinstance(v, str) and len(v) in (2, 5): - tmp[v] = k + # Strip trailing underscore used to disambiguate duplicate values + tmp[v] = k.rstrip("_") assert tmp @@ -106,7 +107,7 @@ # Class 01 - Warning WARNING = '01000' NULL_VALUE_ELIMINATED_IN_SET_FUNCTION = '01003' -STRING_DATA_RIGHT_TRUNCATION = '01004' +STRING_DATA_RIGHT_TRUNCATION_ = '01004' PRIVILEGE_NOT_REVOKED = '01006' PRIVILEGE_NOT_GRANTED = '01007' IMPLICIT_ZERO_BIT_PADDING = '01008' @@ -164,7 +165,7 @@ STRING_DATA_RIGHT_TRUNCATION = '22001' NULL_VALUE_NO_INDICATOR_PARAMETER = '22002' NUMERIC_VALUE_OUT_OF_RANGE = '22003' -NULL_VALUE_NOT_ALLOWED = '22004' +NULL_VALUE_NOT_ALLOWED_ = '22004' ERROR_IN_ASSIGNMENT = '22005' INVALID_DATETIME_FORMAT = '22007' DATETIME_FIELD_OVERFLOW = '22008' @@ -207,6 +208,7 @@ INVALID_TABLESAMPLE_REPEAT = '2202G' INVALID_TABLESAMPLE_ARGUMENT = '2202H' DUPLICATE_JSON_OBJECT_KEY_VALUE = '22030' +INVALID_ARGUMENT_FOR_SQL_JSON_DATETIME_FUNCTION = '22031' INVALID_JSON_TEXT = '22032' INVALID_SQL_JSON_SUBSCRIPT = '22033' MORE_THAN_ONE_SQL_JSON_ITEM = '22034' @@ -273,9 +275,9 @@ # Class 2F - SQL Routine Exception SQL_ROUTINE_EXCEPTION = '2F000' -MODIFYING_SQL_DATA_NOT_PERMITTED = '2F002' -PROHIBITED_SQL_STATEMENT_ATTEMPTED = '2F003' -READING_SQL_DATA_NOT_PERMITTED = '2F004' +MODIFYING_SQL_DATA_NOT_PERMITTED_ = '2F002' +PROHIBITED_SQL_STATEMENT_ATTEMPTED_ = '2F003' +READING_SQL_DATA_NOT_PERMITTED_ = '2F004' FUNCTION_EXECUTED_NO_RETURN_STATEMENT = '2F005' # Class 34 - Invalid Cursor Name diff -Nru psycopg2-2.8.5/lib/extras.py psycopg2-2.8.6/lib/extras.py --- psycopg2-2.8.5/lib/extras.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/lib/extras.py 2020-09-05 22:39:27.000000000 +0000 @@ -130,7 +130,10 @@ class DictCursor(DictCursorBase): - """A cursor that keeps a list of column name -> index mappings.""" + """A cursor that keeps a list of column name -> index mappings__. + + .. __: https://docs.python.org/glossary.html#term-mapping + """ def __init__(self, *args, **kwargs): kwargs['row_factory'] = DictRow diff -Nru psycopg2-2.8.5/NEWS psycopg2-2.8.6/NEWS --- psycopg2-2.8.5/NEWS 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/NEWS 2020-09-05 22:39:27.000000000 +0000 @@ -1,6 +1,21 @@ Current release --------------- +What's new in psycopg 2.8.6 +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- Fixed memory leak changing connection encoding to the current one + (:ticket:`#1101`). +- Fixed search of mxDateTime headers in virtualenvs (:ticket:`#996`). +- Added missing values from errorcodes (:ticket:`#1133`). +- `cursor.query` reports the query of the last :sql:`COPY` opearation too + (:ticket:`#1141`). +- `~psycopg2.errorcodes` map and `~psycopg2.errors` classes updated to + PostgreSQL 13. +- Added wheel packages for ARM architecture (:ticket:`#1125`). +- Wheel package compiled against OpenSSL 1.1.1g. + + What's new in psycopg 2.8.5 ^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff -Nru psycopg2-2.8.5/PKG-INFO psycopg2-2.8.6/PKG-INFO --- psycopg2-2.8.5/PKG-INFO 2020-04-06 05:35:16.000000000 +0000 +++ psycopg2-2.8.6/PKG-INFO 2020-09-05 22:39:33.000000000 +0000 @@ -1,12 +1,18 @@ Metadata-Version: 1.2 Name: psycopg2 -Version: 2.8.5 +Version: 2.8.6 Summary: psycopg2 - Python-PostgreSQL Database Adapter Home-page: https://psycopg.org/ -Author: Daniele Varrazzo -Author-email: daniele.varrazzo@gmail.org +Author: Federico Di Gregorio +Author-email: fog@initd.org +Maintainer: Daniele Varrazzo +Maintainer-email: daniele.varrazzo@gmail.org License: LGPL with exceptions -Description-Content-Type: UNKNOWN +Project-URL: Code, https://github.com/psycopg/psycopg2 +Project-URL: Issue Tracker, https://github.com/psycopg/psycopg2/issues +Project-URL: Documentation, https://www.psycopg.org/docs/ +Project-URL: Homepage, https://psycopg.org/ +Project-URL: Download, https://pypi.org/project/psycopg2/ Description: Psycopg is the most popular PostgreSQL database adapter for the Python programming language. Its main features are the complete implementation of the Python DB API 2.0 specification and the thread safety (several threads can @@ -82,7 +88,6 @@ Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) -Classifier: License :: OSI Approved :: Zope Public License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 diff -Nru psycopg2-2.8.5/psycopg/connection_int.c psycopg2-2.8.6/psycopg/connection_int.c --- psycopg2-2.8.5/psycopg/connection_int.c 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/psycopg/connection_int.c 2020-09-05 22:39:27.000000000 +0000 @@ -1389,7 +1389,10 @@ /* If the current encoding is equal to the requested one we don't issue any query to the backend */ - if (strcmp(self->encoding, clean_enc) == 0) return 0; + if (strcmp(self->encoding, clean_enc) == 0) { + res = 0; + goto exit; + } Py_BEGIN_ALLOW_THREADS; pthread_mutex_lock(&self->lock); diff -Nru psycopg2-2.8.5/psycopg/cursor_type.c psycopg2-2.8.6/psycopg/cursor_type.c --- psycopg2-2.8.5/psycopg/cursor_type.c 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/psycopg/cursor_type.c 2020-09-05 22:39:27.000000000 +0000 @@ -1446,6 +1446,11 @@ Dprintf("curs_copy_from: query = %s", query); + Py_CLEAR(self->query); + if (!(self->query = Bytes_FromString(query))) { + goto exit; + } + /* This routine stores a borrowed reference. Although it is only held * for the duration of curs_copy_from, nested invocations of * Py_BEGIN_ALLOW_THREADS could surrender control to another thread, @@ -1538,6 +1543,11 @@ Dprintf("curs_copy_to: query = %s", query); + Py_CLEAR(self->query); + if (!(self->query = Bytes_FromString(query))) { + goto exit; + } + self->copysize = 0; Py_INCREF(file); self->copyfile = file; @@ -1615,6 +1625,10 @@ Py_INCREF(file); self->copyfile = file; + Py_CLEAR(self->query); + Py_INCREF(sql); + self->query = sql; + /* At this point, the SQL statement must be str, not unicode */ if (pq_execute(self, Bytes_AS_STRING(sql), 0, 0, 0) >= 0) { res = Py_None; diff -Nru psycopg2-2.8.5/psycopg/psycopgmodule.c psycopg2-2.8.6/psycopg/psycopgmodule.c --- psycopg2-2.8.5/psycopg/psycopgmodule.c 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/psycopg/psycopgmodule.c 2020-09-05 22:39:27.000000000 +0000 @@ -918,7 +918,7 @@ for (i = 0; typetable[i].name; i++) { PyObject *type = (PyObject *)typetable[i].type; - Py_TYPE(typetable[i].type) = &PyType_Type; + Py_SET_TYPE(typetable[i].type, &PyType_Type); if (0 > PyType_Ready(typetable[i].type)) { return -1; } Py_INCREF(type); @@ -950,7 +950,7 @@ if (0 > repl_curs_datetime_init()) { return -1; } if (0 > replmsg_datetime_init()) { return -1; } - Py_TYPE(&pydatetimeType) = &PyType_Type; + Py_SET_TYPE(&pydatetimeType, &PyType_Type); if (0 > PyType_Ready(&pydatetimeType)) { return -1; } return 0; @@ -962,7 +962,7 @@ Dprintf("psycopgmodule: initializing mx.DateTime module"); #ifdef HAVE_MXDATETIME - Py_TYPE(&mxdatetimeType) = &PyType_Type; + Py_SET_TYPE(&mxdatetimeType, &PyType_Type); if (0 > PyType_Ready(&mxdatetimeType)) { return -1; } if (mxDateTime_ImportModuleAndAPI()) { @@ -1082,13 +1082,13 @@ libcrypto_threads_init(); /* initialize types and objects not exposed to the module */ - Py_TYPE(&typecastType) = &PyType_Type; + Py_SET_TYPE(&typecastType, &PyType_Type); if (0 > PyType_Ready(&typecastType)) { goto exit; } - Py_TYPE(&chunkType) = &PyType_Type; + Py_SET_TYPE(&chunkType, &PyType_Type); if (0 > PyType_Ready(&chunkType)) { goto exit; } - Py_TYPE(&errorType) = &PyType_Type; + Py_SET_TYPE(&errorType, &PyType_Type); errorType.tp_base = (PyTypeObject *)PyExc_StandardError; if (0 > PyType_Ready(&errorType)) { goto exit; } diff -Nru psycopg2-2.8.5/psycopg/python.h psycopg2-2.8.6/psycopg/python.h --- psycopg2-2.8.5/psycopg/python.h 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/psycopg/python.h 2020-09-05 22:39:27.000000000 +0000 @@ -52,6 +52,14 @@ typedef unsigned long Py_uhash_t; #endif +/* Since Py_TYPE() is changed to the inline static function, + * Py_TYPE(obj) = new_type must be replaced with Py_SET_TYPE(obj, new_type) + * https://docs.python.org/3.10/whatsnew/3.10.html#id2 + */ +#if PY_VERSION_HEX < 0x030900A4 + #define Py_SET_TYPE(obj, type) ((Py_TYPE(obj) = (type)), (void)0) +#endif + /* FORMAT_CODE_PY_SSIZE_T is for Py_ssize_t: */ #define FORMAT_CODE_PY_SSIZE_T "%" PY_FORMAT_SIZE_T "d" diff -Nru psycopg2-2.8.5/psycopg/sqlstate_errors.h psycopg2-2.8.6/psycopg/sqlstate_errors.h --- psycopg2-2.8.5/psycopg/sqlstate_errors.h 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/psycopg/sqlstate_errors.h 2020-09-05 22:39:27.000000000 +0000 @@ -96,6 +96,7 @@ {"2202G", "InvalidTablesampleRepeat"}, {"2202H", "InvalidTablesampleArgument"}, {"22030", "DuplicateJsonObjectKeyValue"}, +{"22031", "InvalidArgumentForSqlJsonDatetimeFunction"}, {"22032", "InvalidJsonText"}, {"22033", "InvalidSqlJsonSubscript"}, {"22034", "MoreThanOneSqlJsonItem"}, diff -Nru psycopg2-2.8.5/psycopg2.egg-info/PKG-INFO psycopg2-2.8.6/psycopg2.egg-info/PKG-INFO --- psycopg2-2.8.5/psycopg2.egg-info/PKG-INFO 2020-04-06 05:35:16.000000000 +0000 +++ psycopg2-2.8.6/psycopg2.egg-info/PKG-INFO 2020-09-05 22:39:33.000000000 +0000 @@ -1,12 +1,18 @@ Metadata-Version: 1.2 Name: psycopg2 -Version: 2.8.5 +Version: 2.8.6 Summary: psycopg2 - Python-PostgreSQL Database Adapter Home-page: https://psycopg.org/ -Author: Daniele Varrazzo -Author-email: daniele.varrazzo@gmail.org +Author: Federico Di Gregorio +Author-email: fog@initd.org +Maintainer: Daniele Varrazzo +Maintainer-email: daniele.varrazzo@gmail.org License: LGPL with exceptions -Description-Content-Type: UNKNOWN +Project-URL: Code, https://github.com/psycopg/psycopg2 +Project-URL: Issue Tracker, https://github.com/psycopg/psycopg2/issues +Project-URL: Documentation, https://www.psycopg.org/docs/ +Project-URL: Homepage, https://psycopg.org/ +Project-URL: Download, https://pypi.org/project/psycopg2/ Description: Psycopg is the most popular PostgreSQL database adapter for the Python programming language. Its main features are the complete implementation of the Python DB API 2.0 specification and the thread safety (several threads can @@ -82,7 +88,6 @@ Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) -Classifier: License :: OSI Approved :: Zope Public License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 diff -Nru psycopg2-2.8.5/scripts/make_errorcodes.py psycopg2-2.8.6/scripts/make_errorcodes.py --- psycopg2-2.8.5/scripts/make_errorcodes.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/scripts/make_errorcodes.py 2020-09-05 22:39:27.000000000 +0000 @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 """Generate the errorcodes module starting from PostgreSQL documentation. The script can be run at a new PostgreSQL release to refresh the module. @@ -20,7 +20,7 @@ import re import sys -import urllib2 +from urllib.request import urlopen from collections import defaultdict @@ -34,7 +34,9 @@ file_start = read_base_file(filename) # If you add a version to the list fix the docs (in errorcodes.rst) classes, errors = fetch_errors( - ['9.1', '9.2', '9.3', '9.4', '9.5', '9.6', '10', '11', '12']) + ['9.1', '9.2', '9.3', '9.4', '9.5', '9.6', '10', '11', '12', '13']) + + disambiguate(errors) f = open(filename, "w") for line in file_start: @@ -57,10 +59,10 @@ classes = {} errors = defaultdict(dict) - page = urllib2.urlopen(url) + page = urlopen(url) for line in page: # Strip comments and skip blanks - line = line.split('#')[0].strip() + line = line.decode("ascii").split('#')[0].strip() if not line: continue @@ -116,6 +118,18 @@ return classes, errors +def disambiguate(errors): + """ + Change name for exception defined more than once. + + Change the first occurrence, because before introdcing the function + they were pretty much lost (see ticket #1133) + """ + # Note: if some code is missing it will be caught downstream + for code in "01004 22004 2F002 2F003 2F004".split(): + errors[code[:2]][code] += "_" + + def generate_module_data(classes, errors): yield "" yield "# Error classes" @@ -124,11 +138,16 @@ .strip().replace(" ", "_").replace('/', "_").upper() yield "CLASS_%s = %r" % (err, clscode) + seen = set() + for clscode, clslabel in sorted(classes.items()): yield "" yield "# %s" % clslabel for errcode, errlabel in sorted(errors[clscode].items()): + if errlabel in seen: + raise Exception("error label already seen: %s" % errlabel) + seen.add(errlabel) yield "%s = %r" % (errlabel, errcode) diff -Nru psycopg2-2.8.5/scripts/make_errors.py psycopg2-2.8.6/scripts/make_errors.py --- psycopg2-2.8.5/scripts/make_errors.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/scripts/make_errors.py 2020-09-05 22:39:27.000000000 +0000 @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 """Generate the errors module from PostgreSQL source code. The script can be run at a new PostgreSQL release to refresh the module. @@ -21,7 +21,7 @@ import os import re import sys -import urllib2 +from urllib.request import urlopen from collections import defaultdict @@ -31,7 +31,7 @@ # If you add a version to the list fix the docs (in errors.rst) classes, errors = fetch_errors( - ['9.1', '9.2', '9.3', '9.4', '9.5', '9.6', '10', '11', '12']) + ['9.1', '9.2', '9.3', '9.4', '9.5', '9.6', '10', '11', '12', '13']) f = open(filename, "w") print("/*\n * Autogenerated by 'scripts/make_errors.py'.\n */\n", file=f) @@ -43,10 +43,10 @@ classes = {} errors = defaultdict(dict) - page = urllib2.urlopen(url) + page = urlopen(url) for line in page: # Strip comments and skip blanks - line = line.split('#')[0].strip() + line = line.decode('ascii').split('#')[0].strip() if not line: continue diff -Nru psycopg2-2.8.5/scripts/travis_prepare.sh psycopg2-2.8.6/scripts/travis_prepare.sh --- psycopg2-2.8.5/scripts/travis_prepare.sh 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/scripts/travis_prepare.sh 2020-09-05 22:39:27.000000000 +0000 @@ -13,6 +13,11 @@ # # The variables can be set in the travis configuration # (https://travis-ci.org/psycopg/psycopg2/settings) +export TEST_PAST=${TEST_PAST:-0} +export TEST_FUTURE=${TEST_FUTURE:-0} +export TEST_VERBOSE=${TEST_VERBOSE:-0} +export PSYCOPG2_TEST_FAST=${PSYCOPG2_TEST_FAST:-0} +export TEST_PRESENT=${TEST_PRESENT:-1} set_param () { # Set a parameter in a postgresql.conf file @@ -115,16 +120,19 @@ # Would give a permission denied error in the travis build dir cd / -# Postgres versions supported by Travis CI -if (( ! "$DONT_TEST_PRESENT" )); then - create 12 - create 11 - create 10 - create 9.6 - create 9.5 - create 9.4 +if (( "$TEST_PRESENT" )); then + if [[ ${TRAVIS_CPU_ARCH} == "arm64" ]]; then + # Postgres versions supported by ARM64 + create 10 + else + create 12 + create 11 + create 10 + create 9.6 + create 9.5 + create 9.4 + fi fi - # Unsupported postgres versions that we still support # Images built by https://github.com/psycopg/psycopg2-wheels/tree/build-dinosaurs if (( "$TEST_PAST" )); then diff -Nru psycopg2-2.8.5/scripts/travis_test.sh psycopg2-2.8.6/scripts/travis_test.sh --- psycopg2-2.8.5/scripts/travis_test.sh 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/scripts/travis_test.sh 2020-09-05 22:39:27.000000000 +0000 @@ -13,6 +13,12 @@ set -e -x +export TEST_PAST=${TEST_PAST:-0} +export TEST_FUTURE=${TEST_FUTURE:-0} +export TEST_VERBOSE=${TEST_VERBOSE:-0} +export PSYCOPG2_TEST_FAST=${PSYCOPG2_TEST_FAST:-0} +export TEST_PRESENT=${TEST_PRESENT:-1} + run_test () { VERSION=$1 DBNAME=psycopg2_test @@ -44,16 +50,20 @@ $VERBOSE } -# Postgres versions supported by Travis CI -if (( ! "$DONT_TEST_PRESENT" )); then - run_test 12 - run_test 11 - run_test 10 - run_test 9.6 - run_test 9.5 - run_test 9.4 +if (( "$TEST_PRESENT" )); then + if [[ "${TRAVIS_CPU_ARCH}" == "arm64" ]]; then + # Postgres versions supported by ARM64 + run_test 10 + else + # Postgres versions supported by Travis CI + run_test 12 + run_test 11 + run_test 10 + run_test 9.6 + run_test 9.5 + run_test 9.4 + fi fi - # Unsupported postgres versions that we still support # Images built by https://github.com/psycopg/psycopg2-wheels/tree/build-dinosaurs if (( "$TEST_PAST" )); then diff -Nru psycopg2-2.8.5/setup.py psycopg2-2.8.6/setup.py --- psycopg2-2.8.5/setup.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/setup.py 2020-09-05 22:39:27.000000000 +0000 @@ -48,7 +48,7 @@ # Take a look at https://www.python.org/dev/peps/pep-0440/ # for a consistent versioning pattern. -PSYCOPG_VERSION = '2.8.5' +PSYCOPG_VERSION = '2.8.6' # note: if you are changing the list of supported Python version please fix @@ -57,7 +57,6 @@ Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) -License :: OSI Approved :: Zope Public License Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 @@ -526,19 +525,21 @@ parser.read('setup.cfg') # check for mx package -have_mxdatetime = False mxincludedir = '' if parser.has_option('build_ext', 'mx_include_dir'): mxincludedir = parser.get('build_ext', 'mx_include_dir') if not mxincludedir: - mxincludedir = os.path.join(get_python_inc(plat_specific=1), "mx") + # look for mxDateTime.h; prefer one located in venv + candidate_dirs = [os.path.join(d, 'mx', 'DateTime', 'mxDateTime') for d in sys.path] \ + + [os.path.join(get_python_inc(plat_specific=1), "mx")] + candidate_dirs = [d for d in candidate_dirs if os.path.exists(os.path.join(d, 'mxDateTime.h'))] or [''] + mxincludedir = candidate_dirs[0] if mxincludedir.strip() and os.path.exists(mxincludedir): # Build the support for mx: we will check at runtime if it can be imported include_dirs.append(mxincludedir) define_macros.append(('HAVE_MXDATETIME', '1')) sources.append('adapter_mxdatetime.c') depends.extend(['adapter_mxdatetime.h', 'typecast_mxdatetime.c']) - have_mxdatetime = True version_flags.append('mx') # generate a nice version string to avoid confusion when users report bugs diff -Nru psycopg2-2.8.5/tests/test_async.py psycopg2-2.8.6/tests/test_async.py --- psycopg2-2.8.5/tests/test_async.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/tests/test_async.py 2020-09-05 22:39:27.000000000 +0000 @@ -33,7 +33,8 @@ import psycopg2.errors from psycopg2 import extensions as ext -from .testutils import ConnectingTestCase, StringIO, skip_before_postgres, slow +from .testutils import (ConnectingTestCase, StringIO, skip_before_postgres, + skip_if_crdb, crdb_version, slow) class PollableStub(object): @@ -62,6 +63,10 @@ self.wait(self.conn) curs = self.conn.cursor() + if crdb_version(self.sync_conn) is not None: + curs.execute("set experimental_enable_temp_tables = 'on'") + self.wait(curs) + curs.execute(''' CREATE TEMPORARY TABLE table1 ( id int PRIMARY KEY @@ -109,7 +114,6 @@ self.wait(cur) self.assertFalse(self.conn.isexecuting()) - self.assertEquals(cur.fetchall()[0][0], '') @slow def test_async_after_async(self): @@ -324,6 +328,7 @@ conn.close() @slow + @skip_if_crdb("flush on write flakey") def test_flush_on_write(self): # a very large query requires a flush loop to be sent to the backend curs = self.conn.cursor() @@ -350,6 +355,7 @@ self.assertEquals(cur.fetchone()[0], 1) @slow + @skip_if_crdb("notify") def test_notify(self): cur = self.conn.cursor() sync_cur = self.sync_conn.cursor() @@ -394,11 +400,12 @@ self.assertRaises(psycopg2.IntegrityError, self.wait, cur) cur.execute("insert into table1 values (%s); " "insert into table1 values (%s)", (2, 2)) - # this should fail as well + # this should fail as well (Postgres behaviour) self.assertRaises(psycopg2.IntegrityError, self.wait, cur) # but this should work - cur.execute("insert into table1 values (%s)", (2, )) - self.wait(cur) + if crdb_version(self.sync_conn) is None: + cur.execute("insert into table1 values (%s)", (2, )) + self.wait(cur) # and the cursor should be usable afterwards cur.execute("insert into table1 values (%s)", (3, )) self.wait(cur) @@ -426,6 +433,7 @@ self.wait(cur2) self.assertEquals(cur2.fetchone()[0], 1) + @skip_if_crdb("notice") def test_notices(self): del self.conn.notices[:] cur = self.conn.cursor() @@ -450,6 +458,7 @@ self.wait(self.conn) self.assertEqual(cur.fetchone(), (42,)) + @skip_if_crdb("copy") def test_async_connection_error_message(self): try: cnn = psycopg2.connect('dbname=thisdatabasedoesntexist', async_=True) @@ -467,6 +476,7 @@ self.assertRaises(psycopg2.ProgrammingError, self.wait, self.conn) @slow + @skip_if_crdb("notice") @skip_before_postgres(9, 0) def test_non_block_after_notification(self): from select import select @@ -500,6 +510,7 @@ def test_poll_noop(self): self.conn.poll() + @skip_if_crdb("notify") @skip_before_postgres(9, 0) def test_poll_conn_for_notification(self): with self.conn.cursor() as cur: @@ -522,6 +533,11 @@ else: self.fail("No notification received") + def test_close(self): + self.conn.close() + self.assertTrue(self.conn.closed) + self.assertTrue(self.conn.async_) + def test_suite(): return unittest.TestLoader().loadTestsFromName(__name__) diff -Nru psycopg2-2.8.5/tests/test_cancel.py psycopg2-2.8.6/tests/test_cancel.py --- psycopg2-2.8.5/tests/test_cancel.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/tests/test_cancel.py 2020-09-05 22:39:27.000000000 +0000 @@ -34,6 +34,7 @@ from .testconfig import dsn import unittest from .testutils import ConnectingTestCase, skip_before_postgres, slow +from .testutils import skip_if_crdb class CancelTests(ConnectingTestCase): @@ -41,6 +42,8 @@ def setUp(self): ConnectingTestCase.setUp(self) + skip_if_crdb("cancel", self.conn) + cur = self.conn.cursor() cur.execute(''' CREATE TEMPORARY TABLE table1 ( @@ -106,11 +109,6 @@ extras.wait_select(async_conn) self.assertEqual(cur.fetchall(), [(1, )]) - def test_async_connection_cancel(self): - async_conn = psycopg2.connect(dsn, async_=True) - async_conn.close() - self.assertTrue(async_conn.closed) - def test_suite(): return unittest.TestLoader().loadTestsFromName(__name__) diff -Nru psycopg2-2.8.5/tests/testconfig.py psycopg2-2.8.6/tests/testconfig.py --- psycopg2-2.8.5/tests/testconfig.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/tests/testconfig.py 2020-09-05 22:39:27.000000000 +0000 @@ -3,10 +3,10 @@ import os dbname = os.environ.get('PSYCOPG2_TESTDB', 'psycopg2_test') -dbhost = os.environ.get('PSYCOPG2_TESTDB_HOST', None) -dbport = os.environ.get('PSYCOPG2_TESTDB_PORT', None) -dbuser = os.environ.get('PSYCOPG2_TESTDB_USER', None) -dbpass = os.environ.get('PSYCOPG2_TESTDB_PASSWORD', None) +dbhost = os.environ.get('PSYCOPG2_TESTDB_HOST', os.environ.get('PGHOST')) +dbport = os.environ.get('PSYCOPG2_TESTDB_PORT', os.environ.get('PGPORT')) +dbuser = os.environ.get('PSYCOPG2_TESTDB_USER', os.environ.get('PGUSER')) +dbpass = os.environ.get('PSYCOPG2_TESTDB_PASSWORD', os.environ.get('PGPASSWORD')) # Check if we want to test psycopg's green path. green = os.environ.get('PSYCOPG2_TEST_GREEN', None) diff -Nru psycopg2-2.8.5/tests/test_connection.py psycopg2-2.8.6/tests/test_connection.py --- psycopg2-2.8.5/tests/test_connection.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/tests/test_connection.py 2020-09-05 22:39:27.000000000 +0000 @@ -44,7 +44,8 @@ from .testutils import ( PY2, unittest, skip_if_no_superuser, skip_before_postgres, skip_after_postgres, skip_before_libpq, skip_after_libpq, - ConnectingTestCase, skip_if_tpc_disabled, skip_if_windows, slow) + ConnectingTestCase, skip_if_tpc_disabled, skip_if_windows, slow, + skip_if_crdb, crdb_version) from .testconfig import dbhost, dsn, dbname @@ -74,6 +75,7 @@ conn.close() self.assertEqual(curs.closed, True) + @skip_if_crdb("backend pid") @skip_before_postgres(8, 4) @skip_if_no_superuser @skip_if_windows @@ -88,6 +90,7 @@ conn.close() self.assertEqual(conn.closed, 1) + @skip_if_crdb("isolation level") def test_reset(self): conn = self.conn # switch session characteristics @@ -111,6 +114,7 @@ if self.conn.info.server_version >= 90100: self.assert_(conn.deferrable is None) + @skip_if_crdb("notice") def test_notices(self): conn = self.conn cur = conn.cursor() @@ -120,6 +124,7 @@ self.assertEqual("CREATE TABLE", cur.statusmessage) self.assert_(conn.notices) + @skip_if_crdb("notice") def test_notices_consistent_order(self): conn = self.conn cur = conn.cursor() @@ -140,6 +145,7 @@ self.assert_('table4' in conn.notices[3]) @slow + @skip_if_crdb("notice") def test_notices_limited(self): conn = self.conn cur = conn.cursor() @@ -154,6 +160,7 @@ self.assert_('table99' in conn.notices[-1], conn.notices[-1]) @slow + @skip_if_crdb("notice") def test_notices_deque(self): conn = self.conn self.conn.notices = deque() @@ -184,6 +191,7 @@ self.assertEqual(len([n for n in conn.notices if 'CREATE TABLE' in n]), 100) + @skip_if_crdb("notice") def test_notices_noappend(self): conn = self.conn self.conn.notices = None # will make an error swallowes ok @@ -230,6 +238,7 @@ self.assert_(time.time() - t0 < 7, "something broken in concurrency") + @skip_if_crdb("encoding") def test_encoding_name(self): self.conn.set_client_encoding("EUC_JP") # conn.encoding is 'EUCJP' now. @@ -329,6 +338,7 @@ cur = conn.cursor(cursor_factory=None) self.assertEqual(type(cur), psycopg2.extras.DictCursor) + @skip_if_crdb("connect any db") def test_failed_init_status(self): class SubConnection(ext.connection): def __init__(self, dsn): @@ -563,13 +573,21 @@ conn = self.connect() cur = conn.cursor() + if crdb_version(conn) is not None: + cur.execute("create table if not exists isolevel (id integer)") + cur.execute("truncate isolevel") + conn.commit() + return + try: cur.execute("drop table isolevel;") except psycopg2.ProgrammingError: conn.rollback() - cur.execute("create table isolevel (id integer);") - conn.commit() - conn.close() + try: + cur.execute("create table isolevel (id integer);") + conn.commit() + finally: + conn.close() def test_isolation_level(self): conn = self.connect() @@ -581,6 +599,7 @@ conn = self.connect() self.assert_(conn.encoding in ext.encodings) + @skip_if_crdb("isolation level") def test_set_isolation_level(self): conn = self.connect() curs = conn.cursor() @@ -628,6 +647,7 @@ curs.execute('show transaction_isolation;') self.assertEqual(curs.fetchone()[0], 'serializable') + @skip_if_crdb("isolation level") def test_set_isolation_level_default(self): conn = self.connect() curs = conn.cursor() @@ -702,6 +722,7 @@ cur1.execute("select count(*) from isolevel;") self.assertEqual(1, cur1.fetchone()[0]) + @skip_if_crdb("isolation level") def test_isolation_level_read_committed(self): cnn1 = self.connect() cnn2 = self.connect() @@ -728,6 +749,7 @@ cur1.execute("select count(*) from isolevel;") self.assertEqual(2, cur1.fetchone()[0]) + @skip_if_crdb("isolation level") def test_isolation_level_serializable(self): cnn1 = self.connect() cnn2 = self.connect() @@ -765,6 +787,7 @@ self.assertRaises(psycopg2.InterfaceError, cnn.set_isolation_level, 1) + @skip_if_crdb("isolation level") def test_setattr_isolation_level_int(self): cur = self.conn.cursor() self.conn.isolation_level = ext.ISOLATION_LEVEL_SERIALIZABLE @@ -813,6 +836,7 @@ cur.execute("SHOW default_transaction_isolation;") self.assertEqual(cur.fetchone()[0], isol) + @skip_if_crdb("isolation level") def test_setattr_isolation_level_str(self): cur = self.conn.cursor() self.conn.isolation_level = "serializable" @@ -909,13 +933,22 @@ def make_test_table(self): cnn = self.connect() cur = cnn.cursor() + if crdb_version(cnn) is not None: + cur.execute("CREATE TABLE IF NOT EXISTS test_tpc (data text)") + cur.execute("TRUNCATE test_tpc") + cnn.commit() + cnn.close() + return + try: cur.execute("DROP TABLE test_tpc;") except psycopg2.ProgrammingError: cnn.rollback() - cur.execute("CREATE TABLE test_tpc (data text);") - cnn.commit() - cnn.close() + try: + cur.execute("CREATE TABLE test_tpc (data text);") + cnn.commit() + finally: + cnn.close() def count_xacts(self): """Return the number of prepared xacts currently in the test db.""" @@ -1240,6 +1273,7 @@ self.assertEqual(None, xid.bqual) +@skip_if_crdb("isolation level") class TransactionControlTests(ConnectingTestCase): def test_closed(self): self.conn.close() @@ -1668,6 +1702,7 @@ # the password away PasswordLeakTestCase.dsn = self.dsn + @skip_if_crdb("connect any db") def test_leak(self): self.assertRaises(psycopg2.DatabaseError, self.GrassingConnection, "dbname=nosuch password=whateva") @@ -1853,6 +1888,7 @@ self.assert_(self.conn.info.socket >= 0) self.assert_(self.bconn.info.socket < 0) + @skip_if_crdb("backend pid") def test_backend_pid(self): cur = self.conn.cursor() try: diff -Nru psycopg2-2.8.5/tests/test_copy.py psycopg2-2.8.6/tests/test_copy.py --- psycopg2-2.8.5/tests/test_copy.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/tests/test_copy.py 2020-09-05 22:39:27.000000000 +0000 @@ -27,7 +27,8 @@ import sys import string import unittest -from .testutils import (ConnectingTestCase, skip_before_postgres, slow, StringIO) +from .testutils import ConnectingTestCase, skip_before_postgres, slow, StringIO +from .testutils import skip_if_crdb from itertools import cycle from subprocess import Popen, PIPE @@ -66,6 +67,7 @@ self._create_temp_table() def _create_temp_table(self): + skip_if_crdb("copy", self.conn) curs = self.conn.cursor() curs.execute(''' CREATE TEMPORARY TABLE tcopy ( @@ -305,6 +307,28 @@ curs.copy_from, StringIO('aaa\nbbb\nccc\n'), 'tcopy') self.assertEqual(curs.rowcount, -1) + def test_copy_query(self): + curs = self.conn.cursor() + + curs.copy_from(StringIO('aaa\nbbb\nccc\n'), 'tcopy', columns=['data']) + self.assert_(b"copy " in curs.query.lower()) + self.assert_(b" from stdin" in curs.query.lower()) + + curs.copy_expert( + "copy tcopy (data) from stdin", + StringIO('ddd\neee\n')) + self.assert_(b"copy " in curs.query.lower()) + self.assert_(b" from stdin" in curs.query.lower()) + + curs.copy_to(StringIO(), "tcopy") + self.assert_(b"copy " in curs.query.lower()) + self.assert_(b" to stdout" in curs.query.lower()) + + curs.execute("insert into tcopy (data) values ('fff')") + curs.copy_expert("copy tcopy to stdout", StringIO()) + self.assert_(b"copy " in curs.query.lower()) + self.assert_(b" to stdout" in curs.query.lower()) + @slow def test_copy_from_segfault(self): # issue #219 diff -Nru psycopg2-2.8.5/tests/test_cursor.py psycopg2-2.8.6/tests/test_cursor.py --- psycopg2-2.8.5/tests/test_cursor.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/tests/test_cursor.py 2020-09-05 22:39:27.000000000 +0000 @@ -36,7 +36,7 @@ from weakref import ref from .testutils import (ConnectingTestCase, skip_before_postgres, skip_if_no_getrefcount, slow, skip_if_no_superuser, - skip_if_windows) + skip_if_windows, skip_if_crdb, crdb_version) import psycopg2.extras from psycopg2.compat import text_type @@ -59,7 +59,7 @@ def test_executemany_propagate_exceptions(self): conn = self.conn cur = conn.cursor() - cur.execute("create temp table test_exc (data int);") + cur.execute("create table test_exc (data int);") def buggygen(): yield 1 // 0 @@ -177,9 +177,237 @@ curs = self.conn.cursor(None) self.assertEqual(curs.name, None) + def test_description_attribs(self): + curs = self.conn.cursor() + curs.execute("""select + 3.14::decimal(10,2) as pi, + 'hello'::text as hi, + '2010-02-18'::date as now; + """) + self.assertEqual(len(curs.description), 3) + for c in curs.description: + self.assertEqual(len(c), 7) # DBAPI happy + for a in ('name', 'type_code', 'display_size', 'internal_size', + 'precision', 'scale', 'null_ok'): + self.assert_(hasattr(c, a), a) + + c = curs.description[0] + self.assertEqual(c.name, 'pi') + self.assert_(c.type_code in psycopg2.extensions.DECIMAL.values) + if crdb_version(self.conn) is None: + self.assert_(c.internal_size > 0) + self.assertEqual(c.precision, 10) + self.assertEqual(c.scale, 2) + + c = curs.description[1] + self.assertEqual(c.name, 'hi') + self.assert_(c.type_code in psycopg2.STRING.values) + self.assert_(c.internal_size < 0) + self.assertEqual(c.precision, None) + self.assertEqual(c.scale, None) + + c = curs.description[2] + self.assertEqual(c.name, 'now') + self.assert_(c.type_code in psycopg2.extensions.DATE.values) + self.assert_(c.internal_size > 0) + self.assertEqual(c.precision, None) + self.assertEqual(c.scale, None) + + @skip_if_crdb("table oid") + def test_description_extra_attribs(self): + curs = self.conn.cursor() + curs.execute(""" + create table testcol ( + pi decimal(10,2), + hi text) + """) + curs.execute("select oid from pg_class where relname = %s", ('testcol',)) + oid = curs.fetchone()[0] + + curs.execute("insert into testcol values (3.14, 'hello')") + curs.execute("select hi, pi, 42 from testcol") + self.assertEqual(curs.description[0].table_oid, oid) + self.assertEqual(curs.description[0].table_column, 2) + + self.assertEqual(curs.description[1].table_oid, oid) + self.assertEqual(curs.description[1].table_column, 1) + + self.assertEqual(curs.description[2].table_oid, None) + self.assertEqual(curs.description[2].table_column, None) + + def test_description_slice(self): + curs = self.conn.cursor() + curs.execute("select 1::int4 as a") + self.assertEqual(curs.description[0][0:2], ('a', 23)) + + def test_pickle_description(self): + curs = self.conn.cursor() + curs.execute('SELECT 1 AS foo') + description = curs.description + + pickled = pickle.dumps(description, pickle.HIGHEST_PROTOCOL) + unpickled = pickle.loads(pickled) + + self.assertEqual(description, unpickled) + + def test_bad_subclass(self): + # check that we get an error message instead of a segfault + # for badly written subclasses. + # see https://stackoverflow.com/questions/22019341/ + class StupidCursor(psycopg2.extensions.cursor): + def __init__(self, *args, **kwargs): + # I am stupid so not calling superclass init + pass + + cur = StupidCursor() + self.assertRaises(psycopg2.InterfaceError, cur.execute, 'select 1') + self.assertRaises(psycopg2.InterfaceError, cur.executemany, + 'select 1', []) + + def test_callproc_badparam(self): + cur = self.conn.cursor() + self.assertRaises(TypeError, cur.callproc, 'lower', 42) + + # It would be inappropriate to test callproc's named parameters in the + # DBAPI2.0 test section because they are a psycopg2 extension. + @skip_before_postgres(9, 0) + @skip_if_crdb("stored procedure") + def test_callproc_dict(self): + # This parameter name tests for injection and quote escaping + paramname = ''' + Robert'); drop table "students" -- + '''.strip() + escaped_paramname = '"%s"' % paramname.replace('"', '""') + procname = 'pg_temp.randall' + + cur = self.conn.cursor() + + # Set up the temporary function + cur.execute(''' + CREATE FUNCTION %s(%s INT) + RETURNS INT AS + 'SELECT $1 * $1' + LANGUAGE SQL + ''' % (procname, escaped_paramname)) + + # Make sure callproc works right + cur.callproc(procname, {paramname: 2}) + self.assertEquals(cur.fetchone()[0], 4) + + # Make sure callproc fails right + failing_cases = [ + ({paramname: 2, 'foo': 'bar'}, psycopg2.ProgrammingError), + ({paramname: '2'}, psycopg2.ProgrammingError), + ({paramname: 'two'}, psycopg2.ProgrammingError), + ({u'bj\xc3rn': 2}, psycopg2.ProgrammingError), + ({3: 2}, TypeError), + ({self: 2}, TypeError), + ] + for parameter_sequence, exception in failing_cases: + self.assertRaises(exception, cur.callproc, procname, parameter_sequence) + self.conn.rollback() + + @skip_if_no_superuser + @skip_if_windows + @skip_if_crdb("backend pid") + @skip_before_postgres(8, 4) + def test_external_close_sync(self): + # If a "victim" connection is closed by a "control" connection + # behind psycopg2's back, psycopg2 always handles it correctly: + # raise OperationalError, set conn.closed to 2. This reproduces + # issue #443, a race between control_conn closing victim_conn and + # psycopg2 noticing. + control_conn = self.conn + connect_func = self.connect + + def wait_func(conn): + pass + + self._test_external_close(control_conn, connect_func, wait_func) + + @skip_if_no_superuser + @skip_if_windows + @skip_if_crdb("backend pid") + @skip_before_postgres(8, 4) + def test_external_close_async(self): + # Issue #443 is in the async code too. Since the fix is duplicated, + # so is the test. + control_conn = self.conn + + def connect_func(): + return self.connect(async_=True) + + wait_func = psycopg2.extras.wait_select + self._test_external_close(control_conn, connect_func, wait_func) + + def _test_external_close(self, control_conn, connect_func, wait_func): + # The short sleep before using victim_conn the second time makes it + # much more likely to lose the race and see the bug. Repeating the + # test several times makes it even more likely. + for i in range(10): + victim_conn = connect_func() + wait_func(victim_conn) + + with victim_conn.cursor() as cur: + cur.execute('select pg_backend_pid()') + wait_func(victim_conn) + pid1 = cur.fetchall()[0][0] + + with control_conn.cursor() as cur: + cur.execute('select pg_terminate_backend(%s)', (pid1,)) + + time.sleep(0.001) + + def f(): + with victim_conn.cursor() as cur: + cur.execute('select 1') + wait_func(victim_conn) + + self.assertRaises(psycopg2.OperationalError, f) + + self.assertEqual(victim_conn.closed, 2) + + @skip_before_postgres(8, 2) + def test_rowcount_on_executemany_returning(self): + cur = self.conn.cursor() + cur.execute("create table execmany(id serial primary key, data int)") + cur.executemany( + "insert into execmany (data) values (%s)", + [(i,) for i in range(4)]) + self.assertEqual(cur.rowcount, 4) + + cur.executemany( + "insert into execmany (data) values (%s) returning data", + [(i,) for i in range(5)]) + self.assertEqual(cur.rowcount, 5) + + @skip_before_postgres(9) + def test_pgresult_ptr(self): + curs = self.conn.cursor() + self.assert_(curs.pgresult_ptr is None) + + curs.execute("select 'x'") + self.assert_(curs.pgresult_ptr is not None) + + try: + f = self.libpq.PQcmdStatus + except AttributeError: + pass + else: + f.argtypes = [ctypes.c_void_p] + f.restype = ctypes.c_char_p + status = f(curs.pgresult_ptr) + self.assertEqual(status, b'SELECT 1') + + curs.close() + self.assert_(curs.pgresult_ptr is None) + + +@skip_if_crdb("named cursor") +class NamedCursorTests(ConnectingTestCase): def test_invalid_name(self): curs = self.conn.cursor() - curs.execute("create temp table invname (data int);") + curs.execute("create table invname (data int);") for i in (10, 20, 30): curs.execute("insert into invname values (%s)", (i,)) curs.close() @@ -377,77 +605,6 @@ for i, rec in enumerate(curs): self.assertEqual(i + 1, curs.rownumber) - def test_description_attribs(self): - curs = self.conn.cursor() - curs.execute("""select - 3.14::decimal(10,2) as pi, - 'hello'::text as hi, - '2010-02-18'::date as now; - """) - self.assertEqual(len(curs.description), 3) - for c in curs.description: - self.assertEqual(len(c), 7) # DBAPI happy - for a in ('name', 'type_code', 'display_size', 'internal_size', - 'precision', 'scale', 'null_ok'): - self.assert_(hasattr(c, a), a) - - c = curs.description[0] - self.assertEqual(c.name, 'pi') - self.assert_(c.type_code in psycopg2.extensions.DECIMAL.values) - self.assert_(c.internal_size > 0) - self.assertEqual(c.precision, 10) - self.assertEqual(c.scale, 2) - - c = curs.description[1] - self.assertEqual(c.name, 'hi') - self.assert_(c.type_code in psycopg2.STRING.values) - self.assert_(c.internal_size < 0) - self.assertEqual(c.precision, None) - self.assertEqual(c.scale, None) - - c = curs.description[2] - self.assertEqual(c.name, 'now') - self.assert_(c.type_code in psycopg2.extensions.DATE.values) - self.assert_(c.internal_size > 0) - self.assertEqual(c.precision, None) - self.assertEqual(c.scale, None) - - def test_description_extra_attribs(self): - curs = self.conn.cursor() - curs.execute(""" - create table testcol ( - pi decimal(10,2), - hi text) - """) - curs.execute("select oid from pg_class where relname = %s", ('testcol',)) - oid = curs.fetchone()[0] - - curs.execute("insert into testcol values (3.14, 'hello')") - curs.execute("select hi, pi, 42 from testcol") - self.assertEqual(curs.description[0].table_oid, oid) - self.assertEqual(curs.description[0].table_column, 2) - - self.assertEqual(curs.description[1].table_oid, oid) - self.assertEqual(curs.description[1].table_column, 1) - - self.assertEqual(curs.description[2].table_oid, None) - self.assertEqual(curs.description[2].table_column, None) - - def test_description_slice(self): - curs = self.conn.cursor() - curs.execute("select 1::int as a") - self.assertEqual(curs.description[0][0:2], ('a', 23)) - - def test_pickle_description(self): - curs = self.conn.cursor() - curs.execute('SELECT 1 AS foo') - description = curs.description - - pickled = pickle.dumps(description, pickle.HIGHEST_PROTOCOL) - unpickled = pickle.loads(pickled) - - self.assertEqual(description, unpickled) - @skip_before_postgres(8, 0) def test_named_cursor_stealing(self): # you can use a named cursor to iterate on a refcursor created @@ -527,155 +684,6 @@ cur.scroll(9, mode='absolute') self.assertEqual(cur.fetchone(), (9,)) - def test_bad_subclass(self): - # check that we get an error message instead of a segfault - # for badly written subclasses. - # see https://stackoverflow.com/questions/22019341/ - class StupidCursor(psycopg2.extensions.cursor): - def __init__(self, *args, **kwargs): - # I am stupid so not calling superclass init - pass - - cur = StupidCursor() - self.assertRaises(psycopg2.InterfaceError, cur.execute, 'select 1') - self.assertRaises(psycopg2.InterfaceError, cur.executemany, - 'select 1', []) - - def test_callproc_badparam(self): - cur = self.conn.cursor() - self.assertRaises(TypeError, cur.callproc, 'lower', 42) - - # It would be inappropriate to test callproc's named parameters in the - # DBAPI2.0 test section because they are a psycopg2 extension. - @skip_before_postgres(9, 0) - def test_callproc_dict(self): - # This parameter name tests for injection and quote escaping - paramname = ''' - Robert'); drop table "students" -- - '''.strip() - escaped_paramname = '"%s"' % paramname.replace('"', '""') - procname = 'pg_temp.randall' - - cur = self.conn.cursor() - - # Set up the temporary function - cur.execute(''' - CREATE FUNCTION %s(%s INT) - RETURNS INT AS - 'SELECT $1 * $1' - LANGUAGE SQL - ''' % (procname, escaped_paramname)) - - # Make sure callproc works right - cur.callproc(procname, {paramname: 2}) - self.assertEquals(cur.fetchone()[0], 4) - - # Make sure callproc fails right - failing_cases = [ - ({paramname: 2, 'foo': 'bar'}, psycopg2.ProgrammingError), - ({paramname: '2'}, psycopg2.ProgrammingError), - ({paramname: 'two'}, psycopg2.ProgrammingError), - ({u'bj\xc3rn': 2}, psycopg2.ProgrammingError), - ({3: 2}, TypeError), - ({self: 2}, TypeError), - ] - for parameter_sequence, exception in failing_cases: - self.assertRaises(exception, cur.callproc, procname, parameter_sequence) - self.conn.rollback() - - @skip_if_no_superuser - @skip_if_windows - @skip_before_postgres(8, 4) - def test_external_close_sync(self): - # If a "victim" connection is closed by a "control" connection - # behind psycopg2's back, psycopg2 always handles it correctly: - # raise OperationalError, set conn.closed to 2. This reproduces - # issue #443, a race between control_conn closing victim_conn and - # psycopg2 noticing. - control_conn = self.conn - connect_func = self.connect - - def wait_func(conn): - pass - - self._test_external_close(control_conn, connect_func, wait_func) - - @skip_if_no_superuser - @skip_if_windows - @skip_before_postgres(8, 4) - def test_external_close_async(self): - # Issue #443 is in the async code too. Since the fix is duplicated, - # so is the test. - control_conn = self.conn - - def connect_func(): - return self.connect(async_=True) - - wait_func = psycopg2.extras.wait_select - self._test_external_close(control_conn, connect_func, wait_func) - - def _test_external_close(self, control_conn, connect_func, wait_func): - # The short sleep before using victim_conn the second time makes it - # much more likely to lose the race and see the bug. Repeating the - # test several times makes it even more likely. - for i in range(10): - victim_conn = connect_func() - wait_func(victim_conn) - - with victim_conn.cursor() as cur: - cur.execute('select pg_backend_pid()') - wait_func(victim_conn) - pid1 = cur.fetchall()[0][0] - - with control_conn.cursor() as cur: - cur.execute('select pg_terminate_backend(%s)', (pid1,)) - - time.sleep(0.001) - - def f(): - with victim_conn.cursor() as cur: - cur.execute('select 1') - wait_func(victim_conn) - - self.assertRaises(psycopg2.OperationalError, f) - - self.assertEqual(victim_conn.closed, 2) - - @skip_before_postgres(8, 2) - def test_rowcount_on_executemany_returning(self): - cur = self.conn.cursor() - cur.execute("create table execmany(id serial primary key, data int)") - cur.executemany( - "insert into execmany (data) values (%s)", - [(i,) for i in range(4)]) - self.assertEqual(cur.rowcount, 4) - - cur.executemany( - "insert into execmany (data) values (%s) returning data", - [(i,) for i in range(5)]) - self.assertEqual(cur.rowcount, 5) - - @skip_before_postgres(9) - def test_pgresult_ptr(self): - curs = self.conn.cursor() - self.assert_(curs.pgresult_ptr is None) - - curs.execute("select 'x'") - self.assert_(curs.pgresult_ptr is not None) - - try: - f = self.libpq.PQcmdStatus - except AttributeError: - pass - else: - f.argtypes = [ctypes.c_void_p] - f.restype = ctypes.c_char_p - status = f(curs.pgresult_ptr) - self.assertEqual(status, b'SELECT 1') - - curs.close() - self.assert_(curs.pgresult_ptr is None) - def test_suite(): return unittest.TestLoader().loadTestsFromName(__name__) diff -Nru psycopg2-2.8.5/tests/test_dates.py psycopg2-2.8.6/tests/test_dates.py --- psycopg2-2.8.5/tests/test_dates.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/tests/test_dates.py 2020-09-05 22:39:27.000000000 +0000 @@ -30,7 +30,7 @@ import psycopg2 from psycopg2.tz import FixedOffsetTimezone, ZERO import unittest -from .testutils import ConnectingTestCase, skip_before_postgres +from .testutils import ConnectingTestCase, skip_before_postgres, skip_if_crdb try: from mx.DateTime import Date, Time, DateTime, DateTimeDeltaFrom @@ -246,6 +246,7 @@ [time(13, 30, 29)]) self.assertEqual(value, '13:30:29') + @skip_if_crdb("cast adds tz") def test_adapt_datetime(self): value = self.execute('select (%s)::timestamp::text', [datetime(2007, 1, 1, 13, 30, 29)]) @@ -386,6 +387,7 @@ self.assertRaises(OverflowError, f, '00:00:100000000000000000:00') self.assertRaises(OverflowError, f, '00:00:00.100000000000000000') + @skip_if_crdb("infinity date") def test_adapt_infinity_tz(self): t = self.execute("select 'infinity'::timestamp") self.assert_(t.tzinfo is None) @@ -423,6 +425,7 @@ r = cur.fetchone()[0] self.assertEqual(r, v, "%s -> %s != %s" % (s, r, v)) + @skip_if_crdb("interval style") @skip_before_postgres(8, 4) def test_interval_iso_8601_not_supported(self): # We may end up supporting, but no pressure for it diff -Nru psycopg2-2.8.5/tests/test_errcodes.py psycopg2-2.8.6/tests/test_errcodes.py --- psycopg2-2.8.5/tests/test_errcodes.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/tests/test_errcodes.py 2020-09-05 22:39:27.000000000 +0000 @@ -57,6 +57,14 @@ len(errs), MAX_CYCLES, errs[0].__class__.__name__, errs[0])) + def test_ambiguous_names(self): + self.assertEqual( + errorcodes.lookup('2F004'), "READING_SQL_DATA_NOT_PERMITTED") + self.assertEqual( + errorcodes.lookup('38004'), "READING_SQL_DATA_NOT_PERMITTED") + self.assertEqual(errorcodes.READING_SQL_DATA_NOT_PERMITTED, '38004') + self.assertEqual(errorcodes.READING_SQL_DATA_NOT_PERMITTED_, '2F004') + def test_suite(): return unittest.TestLoader().loadTestsFromName(__name__) diff -Nru psycopg2-2.8.5/tests/test_extras_dictcursor.py psycopg2-2.8.6/tests/test_extras_dictcursor.py --- psycopg2-2.8.5/tests/test_extras_dictcursor.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/tests/test_extras_dictcursor.py 2020-09-05 22:39:27.000000000 +0000 @@ -27,13 +27,15 @@ from psycopg2.extras import NamedTupleConnection, NamedTupleCursor from .testutils import ConnectingTestCase, skip_before_postgres, \ - skip_before_python, skip_from_python + skip_before_python, skip_from_python, crdb_version, skip_if_crdb class _DictCursorBase(ConnectingTestCase): def setUp(self): ConnectingTestCase.setUp(self) curs = self.conn.cursor() + if crdb_version(self.conn) is not None: + curs.execute("SET experimental_enable_temp_tables = 'on'") curs.execute("CREATE TEMPORARY TABLE ExtrasDictCursorTests (foo text)") curs.execute("INSERT INTO ExtrasDictCursorTests VALUES ('bar')") self.conn.commit() @@ -62,6 +64,7 @@ class ExtrasDictCursorTests(_DictCursorBase): """Test if DictCursor extension class works.""" + @skip_if_crdb("named cursor") def testDictConnCursorArgs(self): self.conn.close() self.conn = self.connect(connection_factory=psycopg2.extras.DictConnection) @@ -129,16 +132,19 @@ return row self._testWithNamedCursor(getter) + @skip_if_crdb("named cursor") @skip_before_postgres(8, 2) def testDictCursorWithNamedCursorNotGreedy(self): curs = self.conn.cursor('tmp', cursor_factory=psycopg2.extras.DictCursor) self._testNamedCursorNotGreedy(curs) + @skip_if_crdb("named cursor") @skip_before_postgres(8, 0) def testDictCursorWithNamedCursorIterRowNumber(self): curs = self.conn.cursor('tmp', cursor_factory=psycopg2.extras.DictCursor) self._testIterRowNumber(curs) + @skip_if_crdb("named cursor") def _testWithNamedCursor(self, getter): curs = self.conn.cursor('aname', cursor_factory=psycopg2.extras.DictCursor) curs.execute("SELECT * FROM ExtrasDictCursorTests") @@ -314,16 +320,19 @@ return row self._testWithNamedCursorReal(getter) + @skip_if_crdb("named cursor") @skip_before_postgres(8, 2) def testDictCursorRealWithNamedCursorNotGreedy(self): curs = self.conn.cursor('tmp', cursor_factory=psycopg2.extras.RealDictCursor) self._testNamedCursorNotGreedy(curs) + @skip_if_crdb("named cursor") @skip_before_postgres(8, 0) def testDictCursorRealWithNamedCursorIterRowNumber(self): curs = self.conn.cursor('tmp', cursor_factory=psycopg2.extras.RealDictCursor) self._testIterRowNumber(curs) + @skip_if_crdb("named cursor") def _testWithNamedCursorReal(self, getter): curs = self.conn.cursor('aname', cursor_factory=psycopg2.extras.RealDictCursor) @@ -429,12 +438,15 @@ self.conn = self.connect(connection_factory=NamedTupleConnection) curs = self.conn.cursor() + if crdb_version(self.conn) is not None: + curs.execute("SET experimental_enable_temp_tables = 'on'") curs.execute("CREATE TEMPORARY TABLE nttest (i int, s text)") curs.execute("INSERT INTO nttest VALUES (1, 'foo')") curs.execute("INSERT INTO nttest VALUES (2, 'bar')") curs.execute("INSERT INTO nttest VALUES (3, 'baz')") self.conn.commit() + @skip_if_crdb("named cursor") def test_cursor_args(self): cur = self.conn.cursor('foo', cursor_factory=psycopg2.extras.DictCursor) self.assertEqual(cur.name, 'foo') @@ -592,6 +604,7 @@ finally: NamedTupleCursor._make_nt = f_orig + @skip_if_crdb("named cursor") @skip_before_postgres(8, 0) def test_named(self): curs = self.conn.cursor('tmp') @@ -602,24 +615,28 @@ recs.extend(curs.fetchall()) self.assertEqual(list(range(10)), [t.i for t in recs]) + @skip_if_crdb("named cursor") def test_named_fetchone(self): curs = self.conn.cursor('tmp') curs.execute("""select 42 as i""") t = curs.fetchone() self.assertEqual(t.i, 42) + @skip_if_crdb("named cursor") def test_named_fetchmany(self): curs = self.conn.cursor('tmp') curs.execute("""select 42 as i""") recs = curs.fetchmany(10) self.assertEqual(recs[0].i, 42) + @skip_if_crdb("named cursor") def test_named_fetchall(self): curs = self.conn.cursor('tmp') curs.execute("""select 42 as i""") recs = curs.fetchall() self.assertEqual(recs[0].i, 42) + @skip_if_crdb("named cursor") @skip_before_postgres(8, 2) def test_not_greedy(self): curs = self.conn.cursor('tmp') @@ -634,6 +651,7 @@ self.assert_(recs[1].ts - recs[0].ts < timedelta(seconds=0.005)) self.assert_(recs[2].ts - recs[1].ts > timedelta(seconds=0.0099)) + @skip_if_crdb("named cursor") @skip_before_postgres(8, 0) def test_named_rownumber(self): curs = self.conn.cursor('tmp') diff -Nru psycopg2-2.8.5/tests/test_green.py psycopg2-2.8.6/tests/test_green.py --- psycopg2-2.8.5/tests/test_green.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/tests/test_green.py 2020-09-05 22:39:27.000000000 +0000 @@ -33,6 +33,7 @@ from psycopg2.extensions import POLL_OK, POLL_READ, POLL_WRITE from .testutils import ConnectingTestCase, skip_before_postgres, slow +from .testutils import skip_if_crdb class ConnectionStub(object): @@ -67,6 +68,7 @@ return stub @slow + @skip_if_crdb("flush on write flakey") def test_flush_on_write(self): # a very large query requires a flush loop to be sent to the backend conn = self.conn @@ -122,8 +124,9 @@ cur.execute, "copy (select 1) to stdout") @slow + @skip_if_crdb("notice") @skip_before_postgres(9, 0) - def test_non_block_after_notification(self): + def test_non_block_after_notice(self): def wait(conn): while 1: state = conn.poll() @@ -216,6 +219,7 @@ self.fail("you should have had a success or an error by now") + @skip_if_crdb("named cursor") def test_errors_named_cursor(self): for i in range(100): self.to_error = None diff -Nru psycopg2-2.8.5/tests/test_ipaddress.py psycopg2-2.8.6/tests/test_ipaddress.py --- psycopg2-2.8.5/tests/test_ipaddress.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/tests/test_ipaddress.py 2020-09-05 22:39:27.000000000 +0000 @@ -71,6 +71,7 @@ cur.execute("select %s", [ip.ip_interface('::ffff:102:300/128')]) self.assertEquals(cur.fetchone()[0], '::ffff:102:300/128') + @testutils.skip_if_crdb("cidr") def test_cidr_cast(self): cur = self.conn.cursor() psycopg2.extras.register_ipaddress(cur) @@ -88,6 +89,7 @@ self.assert_(isinstance(obj, ip.IPv6Network), repr(obj)) self.assertEquals(obj, ip.ip_network('::ffff:102:300/128')) + @testutils.skip_if_crdb("cidr") @testutils.skip_before_postgres(8, 2) def test_cidr_array_cast(self): cur = self.conn.cursor() diff -Nru psycopg2-2.8.5/tests/test_lobject.py psycopg2-2.8.6/tests/test_lobject.py --- psycopg2-2.8.5/tests/test_lobject.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/tests/test_lobject.py 2020-09-05 22:39:27.000000000 +0000 @@ -32,13 +32,14 @@ import psycopg2.extensions import unittest from .testutils import (decorate_all_tests, skip_if_tpc_disabled, - skip_before_postgres, ConnectingTestCase, skip_if_green, slow) + skip_before_postgres, ConnectingTestCase, skip_if_green, skip_if_crdb, slow) -skip_if_no_lo = skip_before_postgres(8, 1, - "large objects only supported from PG 8.1") - -skip_lo_if_green = skip_if_green("libpq doesn't support LO in async mode") +def skip_if_no_lo(f): + f = skip_before_postgres(8, 1, "large objects only supported from PG 8.1")(f) + f = skip_if_green("libpq doesn't support LO in async mode")(f) + f = skip_if_crdb("large objects")(f) + return f class LargeObjectTestCase(ConnectingTestCase): @@ -67,7 +68,6 @@ @skip_if_no_lo -@skip_lo_if_green class LargeObjectTests(LargeObjectTestCase): def test_create(self): lo = self.conn.lobject() @@ -413,7 +413,6 @@ @skip_if_no_lo -@skip_lo_if_green @skip_if_no_truncate class LargeObjectTruncateTests(LargeObjectTestCase): def test_truncate(self): @@ -478,7 +477,6 @@ @skip_if_no_lo -@skip_lo_if_green @skip_if_no_truncate @skip_if_no_lo64 class LargeObject64Tests(LargeObjectTestCase): @@ -506,7 +504,6 @@ @skip_if_no_lo -@skip_lo_if_green @skip_if_no_truncate @skip_if_lo64 class LargeObjectNot64Tests(LargeObjectTestCase): diff -Nru psycopg2-2.8.5/tests/test_module.py psycopg2-2.8.6/tests/test_module.py --- psycopg2-2.8.5/tests/test_module.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/tests/test_module.py 2020-09-05 22:39:27.000000000 +0000 @@ -32,7 +32,7 @@ import unittest from .testutils import (skip_before_postgres, - ConnectingTestCase, skip_copy_if_green, slow, StringIO) + ConnectingTestCase, skip_copy_if_green, skip_if_crdb, slow, StringIO) import psycopg2 @@ -216,6 +216,7 @@ gc.collect() assert(w() is None) + @skip_if_crdb("copy") @skip_copy_if_green def test_diagnostics_copy(self): f = StringIO() @@ -244,6 +245,7 @@ self.assertEqual(diag1.sqlstate, '42601') self.assertEqual(diag2.sqlstate, '42P01') + @skip_if_crdb("deferrable") def test_diagnostics_from_commit(self): cur = self.conn.cursor() cur.execute(""" @@ -259,6 +261,7 @@ e = exc self.assertEqual(e.diag.sqlstate, '23503') + @skip_if_crdb("diagnostic") @skip_before_postgres(9, 3) def test_9_3_diagnostics(self): cur = self.conn.cursor() @@ -299,6 +302,7 @@ self.assertEqual(e.pgcode, e1.pgcode) self.assert_(e1.cursor is None) + @skip_if_crdb("connect any db") def test_pickle_connection_error(self): # segfaults on psycopg 2.5.1 - see ticket #170 try: diff -Nru psycopg2-2.8.5/tests/test_notify.py psycopg2-2.8.6/tests/test_notify.py --- psycopg2-2.8.5/tests/test_notify.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/tests/test_notify.py 2020-09-05 22:39:27.000000000 +0000 @@ -29,7 +29,7 @@ import psycopg2 from psycopg2 import extensions from psycopg2.extensions import Notify -from .testutils import ConnectingTestCase, slow +from .testutils import ConnectingTestCase, skip_if_crdb, slow from .testconfig import dsn import sys @@ -38,6 +38,7 @@ from subprocess import Popen, PIPE +@skip_if_crdb("notify") class NotifiesTests(ConnectingTestCase): def autocommit(self, conn): diff -Nru psycopg2-2.8.5/tests/test_quote.py psycopg2-2.8.6/tests/test_quote.py --- psycopg2-2.8.5/tests/test_quote.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/tests/test_quote.py 2020-09-05 22:39:27.000000000 +0000 @@ -25,7 +25,7 @@ from . import testutils import unittest -from .testutils import ConnectingTestCase, unichr, PY2 +from .testutils import ConnectingTestCase, skip_if_crdb, unichr, PY2 import psycopg2 import psycopg2.extensions @@ -121,6 +121,7 @@ self.assertEqual(res, data) self.assert_(not self.conn.notices) + @skip_if_crdb("encoding") def test_latin1(self): self.conn.set_client_encoding('LATIN1') curs = self.conn.cursor() @@ -146,6 +147,7 @@ self.assertEqual(res, data) self.assert_(not self.conn.notices) + @skip_if_crdb("encoding") def test_koi8(self): self.conn.set_client_encoding('KOI8') curs = self.conn.cursor() diff -Nru psycopg2-2.8.5/tests/test_sql.py psycopg2-2.8.6/tests/test_sql.py --- psycopg2-2.8.5/tests/test_sql.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/tests/test_sql.py 2020-09-05 22:39:27.000000000 +0000 @@ -26,7 +26,8 @@ import datetime as dt import unittest from .testutils import ( - ConnectingTestCase, skip_before_postgres, skip_copy_if_green, StringIO) + ConnectingTestCase, skip_before_postgres, skip_copy_if_green, StringIO, + skip_if_crdb) import psycopg2 from psycopg2 import sql @@ -151,6 +152,7 @@ self.assertEqual(cur.fetchall(), [(10, 'a', 'b', 'c'), (20, 'd', 'e', 'f')]) + @skip_if_crdb("copy") @skip_copy_if_green @skip_before_postgres(8, 2) def test_copy(self): diff -Nru psycopg2-2.8.5/tests/test_transaction.py psycopg2-2.8.6/tests/test_transaction.py --- psycopg2-2.8.5/tests/test_transaction.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/tests/test_transaction.py 2020-09-05 22:39:27.000000000 +0000 @@ -26,6 +26,7 @@ import threading import unittest from .testutils import ConnectingTestCase, skip_before_postgres, slow +from .testutils import skip_if_crdb import psycopg2 from psycopg2.extensions import ( @@ -36,6 +37,7 @@ def setUp(self): ConnectingTestCase.setUp(self) + skip_if_crdb("isolation level", self.conn) self.conn.set_isolation_level(ISOLATION_LEVEL_SERIALIZABLE) curs = self.conn.cursor() curs.execute(''' @@ -102,6 +104,7 @@ def setUp(self): ConnectingTestCase.setUp(self) + skip_if_crdb("isolation level", self.conn) curs = self.conn.cursor() # Drop table if it already exists diff -Nru psycopg2-2.8.5/tests/test_types_basic.py psycopg2-2.8.6/tests/test_types_basic.py --- psycopg2-2.8.5/tests/test_types_basic.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/tests/test_types_basic.py 2020-09-05 22:39:27.000000000 +0000 @@ -32,6 +32,7 @@ from . import testutils import unittest from .testutils import PY2, long, text_type, ConnectingTestCase, restore_types +from .testutils import skip_if_crdb import psycopg2 from psycopg2.extensions import AsIs, adapt, register_adapter @@ -148,12 +149,14 @@ buf2 = self.execute("SELECT %s::bytea AS foo", (buf,)) self.assertEqual(s, buf2.tobytes()) + @skip_if_crdb("nested array") def testArray(self): s = self.execute("SELECT %s AS foo", ([[1, 2], [3, 4]],)) self.failUnlessEqual(s, [[1, 2], [3, 4]]) s = self.execute("SELECT %s AS foo", (['one', 'two', 'three'],)) self.failUnlessEqual(s, ['one', 'two', 'three']) + @skip_if_crdb("nested array") def testEmptyArrayRegression(self): # ticket #42 curs = self.conn.cursor() @@ -170,6 +173,7 @@ curs.execute("select col from array_test where id = 2") self.assertEqual(curs.fetchone()[0], []) + @skip_if_crdb("nested array") @testutils.skip_before_postgres(8, 4) def testNestedEmptyArray(self): # issue #788 @@ -235,6 +239,7 @@ self.assert_(isinstance(x[0], bytes)) self.assertEqual(x, [b'a', b'b', b'c']) + @skip_if_crdb("nested array") @testutils.skip_before_postgres(8, 2) def testArrayOfNulls(self): curs = self.conn.cursor() @@ -271,6 +276,7 @@ curs.execute("insert into na (boolaa) values (%s)", ([[True, None]],)) curs.execute("insert into na (boolaa) values (%s)", ([[None, None]],)) + @skip_if_crdb("nested array") @testutils.skip_before_postgres(8, 2) def testNestedArrays(self): curs = self.conn.cursor() @@ -400,6 +406,7 @@ a = self.execute("select '{1, 2, NULL}'::int4[]") self.assertEqual(a, [2, 4, 'nada']) + @skip_if_crdb("cidr") @testutils.skip_before_postgres(8, 2) def testNetworkArray(self): # we don't know these types, but we know their arrays diff -Nru psycopg2-2.8.5/tests/test_types_extras.py psycopg2-2.8.6/tests/test_types_extras.py --- psycopg2-2.8.5/tests/test_types_extras.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/tests/test_types_extras.py 2020-09-05 22:39:27.000000000 +0000 @@ -27,7 +27,7 @@ import unittest from .testutils import (PY2, text_type, skip_if_no_uuid, skip_before_postgres, ConnectingTestCase, py3_raises_typeerror, slow, skip_from_python, - restore_types) + restore_types, skip_if_crdb, crdb_version) import psycopg2 import psycopg2.extras @@ -134,6 +134,7 @@ def skip_if_no_hstore(f): @wraps(f) + @skip_if_crdb("hstore") def skip_if_no_hstore_(self): oids = HstoreAdapter.get_oids(self.conn) if oids is None or not oids[0]: @@ -417,6 +418,7 @@ def skip_if_no_composite(f): @wraps(f) + @skip_if_crdb("composite") def skip_if_no_composite_(self): if self.conn.info.server_version < 80000: return self.skipTest( @@ -786,6 +788,7 @@ return skip_if_no_json_type_ +@skip_if_crdb("json") class JsonTestCase(ConnectingTestCase): def test_adapt(self): objs = [None, "te'xt", 123, 123.45, @@ -990,8 +993,9 @@ curs.execute("""select '{"a": 100.0, "b": null}'::jsonb""") self.assertEqual(curs.fetchone()[0], {'a': 100.0, 'b': None}) - curs.execute("""select array['{"a": 100.0, "b": null}']::jsonb[]""") - self.assertEqual(curs.fetchone()[0], [{'a': 100.0, 'b': None}]) + if crdb_version(self.conn) is None: + curs.execute("""select array['{"a": 100.0, "b": null}']::jsonb[]""") + self.assertEqual(curs.fetchone()[0], [{'a': 100.0, 'b': None}]) def test_register_on_connection(self): psycopg2.extras.register_json(self.conn, loads=self.myloads, name='jsonb') @@ -1025,11 +1029,12 @@ data = curs.fetchone()[0] self.assert_(isinstance(data['a'], Decimal)) self.assertEqual(data['a'], Decimal('100.0')) - # sure we are not manling json too? - curs.execute("""select '{"a": 100.0, "b": null}'::json""") - data = curs.fetchone()[0] - self.assert_(isinstance(data['a'], float)) - self.assertEqual(data['a'], 100.0) + # sure we are not mangling json too? + if crdb_version(self.conn) is None: + curs.execute("""select '{"a": 100.0, "b": null}'::json""") + data = curs.fetchone()[0] + self.assert_(isinstance(data['a'], float)) + self.assertEqual(data['a'], 100.0) def test_register_default(self): curs = self.conn.cursor() @@ -1044,17 +1049,19 @@ self.assert_(isinstance(data['a'], Decimal)) self.assertEqual(data['a'], Decimal('100.0')) - curs.execute("""select array['{"a": 100.0, "b": null}']::jsonb[]""") - data = curs.fetchone()[0] - self.assert_(isinstance(data[0]['a'], Decimal)) - self.assertEqual(data[0]['a'], Decimal('100.0')) + if crdb_version(self.conn) is None: + curs.execute("""select array['{"a": 100.0, "b": null}']::jsonb[]""") + data = curs.fetchone()[0] + self.assert_(isinstance(data[0]['a'], Decimal)) + self.assertEqual(data[0]['a'], Decimal('100.0')) def test_null(self): curs = self.conn.cursor() curs.execute("""select NULL::jsonb""") self.assertEqual(curs.fetchone()[0], None) - curs.execute("""select NULL::jsonb[]""") - self.assertEqual(curs.fetchone()[0], None) + if crdb_version(self.conn) is None: + curs.execute("""select NULL::jsonb[]""") + self.assertEqual(curs.fetchone()[0], None) class RangeTestCase(unittest.TestCase): @@ -1325,6 +1332,7 @@ self.assertEqual(result, expected) +@skip_if_crdb("range") @skip_before_postgres(9, 2, "range not supported before postgres 9.2") class RangeCasterTestCase(ConnectingTestCase): diff -Nru psycopg2-2.8.5/tests/testutils.py psycopg2-2.8.6/tests/testutils.py --- psycopg2-2.8.5/tests/testutils.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/tests/testutils.py 2020-09-05 22:39:27.000000000 +0000 @@ -29,6 +29,7 @@ import types import ctypes import select +import operator import platform import unittest from functools import wraps @@ -37,7 +38,7 @@ import psycopg2 import psycopg2.errors import psycopg2.extensions -from psycopg2.compat import PY2, PY3, text_type +from psycopg2.compat import PY2, PY3, string_types, text_type from .testconfig import green, dsn, repl_dsn @@ -248,6 +249,8 @@ @wraps(f) def skip_if_tpc_disabled_(self): cnn = self.connect() + skip_if_crdb("2-phase commit", cnn) + cur = cnn.cursor() try: cur.execute("SHOW max_prepared_transactions;") @@ -407,6 +410,114 @@ return decorator(cls) +def crdb_version(conn, __crdb_version=[]): + """ + Return the CockroachDB version if that's the db being tested, else None. + + Return the number as an integer similar to PQserverVersion: return + v20.1.3 as 200103. + + Assume all the connections are on the same db: return a cached result on + following calls. + + """ + if __crdb_version: + return __crdb_version[0] + + sver = conn.info.parameter_status("crdb_version") + if sver is None: + __crdb_version.append(None) + else: + m = re.search(r"\bv(\d+)\.(\d+)\.(\d+)", sver) + if not m: + raise ValueError( + "can't parse CockroachDB version from %s" % sver) + + ver = int(m.group(1)) * 10000 + int(m.group(2)) * 100 + int(m.group(3)) + __crdb_version.append(ver) + + return __crdb_version[0] + + +def skip_if_crdb(reason, conn=None, version=None): + """Skip a test or test class if we are testing against CockroachDB. + + Can be used as a decorator for tests function or classes: + + @skip_if_crdb("my reason") + class SomeUnitTest(UnitTest): + # ... + + Or as a normal function if the *conn* argument is passed. + + If *version* is specified it should be a string such as ">= 20.1", "< 20", + "== 20.1.3": the test will be skipped only if the version matches. + + """ + if not isinstance(reason, string_types): + raise TypeError("reason should be a string, got %r instead" % reason) + + if conn is not None: + ver = crdb_version(conn) + if ver is not None and _crdb_match_version(ver, version): + if reason in crdb_reasons: + reason = ( + "%s (https://github.com/cockroachdb/cockroach/issues/%s)" + % (reason, crdb_reasons[reason])) + raise unittest.SkipTest( + "not supported on CockroachDB %s: %s" % (ver, reason)) + + @decorate_all_tests + def skip_if_crdb_(f): + @wraps(f) + def skip_if_crdb__(self, *args, **kwargs): + skip_if_crdb(reason, conn=self.connect(), version=version) + return f(self, *args, **kwargs) + + return skip_if_crdb__ + + return skip_if_crdb_ + + +# mapping from reason description to ticket number +crdb_reasons = { + "2-phase commit": 22329, + "backend pid": 35897, + "cancel": 41335, + "cast adds tz": 51692, + "cidr": 18846, + "composite": 27792, + "copy": 41608, + "deferrable": 48307, + "encoding": 35882, + "hstore": 41284, + "infinity date": 41564, + "interval style": 35807, + "large objects": 243, + "named cursor": 41412, + "nested array": 32552, + "notify": 41522, + "range": 41282, + "stored procedure": 1751, +} + + +def _crdb_match_version(version, pattern): + if pattern is None: + return True + + m = re.match(r'^(>|>=|<|<=|==|!=)\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?$', pattern) + if m is None: + raise ValueError( + "bad crdb version pattern %r: should be 'OP MAJOR[.MINOR[.BUGFIX]]'" + % pattern) + + ops = {'>': 'gt', '>=': 'ge', '<': 'lt', '<=': 'le', '==': 'eq', '!=': 'ne'} + op = getattr(operator, ops[m.group(1)]) + ref = int(m.group(2)) * 10000 + int(m.group(3) or 0) * 100 + int(m.group(4) or 0) + return op(version, ref) + + class py3_raises_typeerror(object): def __enter__(self): pass diff -Nru psycopg2-2.8.5/tests/test_with.py psycopg2-2.8.6/tests/test_with.py --- psycopg2-2.8.5/tests/test_with.py 2020-04-06 05:35:09.000000000 +0000 +++ psycopg2-2.8.6/tests/test_with.py 2020-09-05 22:39:27.000000000 +0000 @@ -27,7 +27,7 @@ import psycopg2.extensions as ext import unittest -from .testutils import ConnectingTestCase, skip_before_postgres +from .testutils import ConnectingTestCase, skip_before_postgres, skip_if_crdb class WithTestCase(ConnectingTestCase): @@ -203,6 +203,7 @@ self.assert_(curs.closed) self.assert_(closes) + @skip_if_crdb("named cursor") def test_exception_swallow(self): # bug #262: __exit__ calls cur.close() that hides the exception # with another error. @@ -216,6 +217,7 @@ else: self.fail("where is my exception?") + @skip_if_crdb("named cursor") @skip_before_postgres(8, 2) def test_named_with_noop(self): with self.conn.cursor('named'):