diff options
| author | vitalyisaev <[email protected]> | 2023-11-14 09:58:56 +0300 |
|---|---|---|
| committer | vitalyisaev <[email protected]> | 2023-11-14 10:20:20 +0300 |
| commit | c2b2dfd9827a400a8495e172a56343462e3ceb82 (patch) | |
| tree | cd4e4f597d01bede4c82dffeb2d780d0a9046bd0 /contrib/python/pg8000 | |
| parent | d4ae8f119e67808cb0cf776ba6e0cf95296f2df7 (diff) | |
YQ Connector: move tests from yql to ydb (OSS)
Перенос папки с тестами на Коннектор из папки yql в папку ydb (синхронизируется с github).
Diffstat (limited to 'contrib/python/pg8000')
| -rw-r--r-- | contrib/python/pg8000/.dist-info/METADATA | 2459 | ||||
| -rw-r--r-- | contrib/python/pg8000/.dist-info/top_level.txt | 1 | ||||
| -rw-r--r-- | contrib/python/pg8000/LICENSE | 29 | ||||
| -rw-r--r-- | contrib/python/pg8000/README.rst | 2431 | ||||
| -rw-r--r-- | contrib/python/pg8000/pg8000/__init__.py | 213 | ||||
| -rw-r--r-- | contrib/python/pg8000/pg8000/converters.py | 771 | ||||
| -rw-r--r-- | contrib/python/pg8000/pg8000/core.py | 870 | ||||
| -rw-r--r-- | contrib/python/pg8000/pg8000/dbapi.py | 976 | ||||
| -rw-r--r-- | contrib/python/pg8000/pg8000/exceptions.py | 32 | ||||
| -rw-r--r-- | contrib/python/pg8000/pg8000/legacy.py | 822 | ||||
| -rw-r--r-- | contrib/python/pg8000/pg8000/native.py | 293 | ||||
| -rw-r--r-- | contrib/python/pg8000/pg8000/types.py | 305 | ||||
| -rw-r--r-- | contrib/python/pg8000/ya.make | 34 |
13 files changed, 9236 insertions, 0 deletions
diff --git a/contrib/python/pg8000/.dist-info/METADATA b/contrib/python/pg8000/.dist-info/METADATA new file mode 100644 index 00000000000..6980a1f026a --- /dev/null +++ b/contrib/python/pg8000/.dist-info/METADATA @@ -0,0 +1,2459 @@ +Metadata-Version: 2.1 +Name: pg8000 +Version: 1.30.2 +Summary: PostgreSQL interface library +License: BSD 3-Clause License +Project-URL: Homepage, https://github.com/tlocke/pg8000 +Keywords: postgresql,dbapi +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: Implementation +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Operating System :: OS Independent +Classifier: Topic :: Database :: Front-Ends +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Requires-Python: >=3.8 +Description-Content-Type: text/x-rst +License-File: LICENSE +Requires-Dist: scramp >=1.4.4 +Requires-Dist: python-dateutil >=2.8.2 + +====== +pg8000 +====== + +.. |ssl.SSLContext| replace:: ``ssl.SSLContext`` +.. _ssl.SSLContext: https://docs.python.org/3/library/ssl.html#ssl.SSLContext + +.. |ssl.create_default_context()| replace:: ``ssl.create_default_context()`` +.. _ssl.create_default_context(): https://docs.python.org/3/library/ssl.html#ssl.create_default_context + +pg8000 is a pure-`Python <https://www.python.org/>`_ +`PostgreSQL <http://www.postgresql.org/>`_ driver that complies with +`DB-API 2.0 <http://www.python.org/dev/peps/pep-0249/>`_. It is tested on Python +versions 3.8+, on CPython and PyPy, and PostgreSQL versions 11+. pg8000's name comes +from the belief that it is probably about the 8000th PostgreSQL interface for Python. +pg8000 is distributed under the BSD 3-clause license. + +All bug reports, feature requests and contributions are welcome at +`http://github.com/tlocke/pg8000/ <http://github.com/tlocke/pg8000/>`_. + +.. image:: https://github.com/tlocke/pg8000/workflows/pg8000/badge.svg + :alt: Build Status + +.. contents:: Table of Contents + :depth: 2 + :local: + +Installation +------------ + +To install pg8000 using `pip` type: + +`pip install pg8000` + + +Native API Interactive Examples +------------------------------- + +pg8000 comes with two APIs, the native pg8000 API and the DB-API 2.0 standard +API. These are the examples for the native API, and the DB-API 2.0 examples +follow in the next section. + + +Basic Example +````````````` + +Import pg8000, connect to the database, create a table, add some rows and then +query the table: + +>>> import pg8000.native +>>> +>>> # Connect to the database with user name postgres +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> # Create a temporary table +>>> +>>> con.run("CREATE TEMPORARY TABLE book (id SERIAL, title TEXT)") +>>> +>>> # Populate the table +>>> +>>> for title in ("Ender's Game", "The Magus"): +... con.run("INSERT INTO book (title) VALUES (:title)", title=title) +>>> +>>> # Print all the rows in the table +>>> +>>> for row in con.run("SELECT * FROM book"): +... print(row) +[1, "Ender's Game"] +[2, 'The Magus'] +>>> +>>> con.close() + + +Transactions +```````````` + +Here's how to run groups of SQL statements in a +`transaction <https://www.postgresql.org/docs/current/tutorial-transactions.html>`_: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("START TRANSACTION") +>>> +>>> # Create a temporary table +>>> con.run("CREATE TEMPORARY TABLE book (id SERIAL, title TEXT)") +>>> +>>> for title in ("Ender's Game", "The Magus", "Phineas Finn"): +... con.run("INSERT INTO book (title) VALUES (:title)", title=title) +>>> con.run("COMMIT") +>>> for row in con.run("SELECT * FROM book"): +... print(row) +[1, "Ender's Game"] +[2, 'The Magus'] +[3, 'Phineas Finn'] +>>> +>>> con.close() + +rolling back a transaction: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> # Create a temporary table +>>> con.run("CREATE TEMPORARY TABLE book (id SERIAL, title TEXT)") +>>> +>>> for title in ("Ender's Game", "The Magus", "Phineas Finn"): +... con.run("INSERT INTO book (title) VALUES (:title)", title=title) +>>> +>>> con.run("START TRANSACTION") +>>> con.run("DELETE FROM book WHERE title = :title", title="Phineas Finn") +>>> con.run("ROLLBACK") +>>> for row in con.run("SELECT * FROM book"): +... print(row) +[1, "Ender's Game"] +[2, 'The Magus'] +[3, 'Phineas Finn'] +>>> +>>> con.close() + +NB. There is `a longstanding bug <https://github.com/tlocke/pg8000/issues/36>`_ +in the PostgreSQL server whereby if a `COMMIT` is issued against a failed +transaction, the transaction is silently rolled back, rather than an error being +returned. pg8000 attempts to detect when this has happened and raise an +`InterfaceError`. + + +Query Using Functions +````````````````````` + +Another query, using some PostgreSQL functions: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("SELECT TO_CHAR(TIMESTAMP '2021-10-10', 'YYYY BC')") +[['2021 AD']] +>>> +>>> con.close() + + +Interval Type +````````````` + +A query that returns the PostgreSQL interval type: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> import datetime +>>> +>>> ts = datetime.date(1980, 4, 27) +>>> con.run("SELECT timestamp '2013-12-01 16:06' - :ts", ts=ts) +[[datetime.timedelta(days=12271, seconds=57960)]] +>>> +>>> con.close() + + +Point Type +`````````` + +A round-trip with a +`PostgreSQL point <https://www.postgresql.org/docs/current/datatype-geometric.html>`_ +type: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("SELECT CAST(:pt as point)", pt=(2.3,1)) +[[(2.3, 1.0)]] +>>> +>>> con.close() + + +Client Encoding +``````````````` + +When communicating with the server, pg8000 uses the character set that the server asks +it to use (the client encoding). By default the client encoding is the database's +character set (chosen when the database is created), but the client encoding can be +changed in a number of ways (eg. setting ``CLIENT_ENCODING`` in ``postgresql.conf``). +Another way of changing the client encoding is by using an SQL command. For example: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("SET CLIENT_ENCODING TO 'UTF8'") +>>> con.run("SHOW CLIENT_ENCODING") +[['UTF8']] +>>> +>>> con.close() + + +JSON +```` + +`JSON <https://www.postgresql.org/docs/current/datatype-json.html>`_ always comes back +from the server de-serialized. If the JSON you want to send is a ``dict`` then you can +just do: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> val = {'name': 'Apollo 11 Cave', 'zebra': True, 'age': 26.003} +>>> con.run("SELECT CAST(:apollo as jsonb)", apollo=val) +[[{'age': 26.003, 'name': 'Apollo 11 Cave', 'zebra': True}]] +>>> +>>> con.close() + +JSON can always be sent in serialized form to the server: + +>>> import json +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> +>>> val = ['Apollo 11 Cave', True, 26.003] +>>> con.run("SELECT CAST(:apollo as jsonb)", apollo=json.dumps(val)) +[[['Apollo 11 Cave', True, 26.003]]] +>>> +>>> con.close() + +JSON queries can be have parameters: + +>>> import pg8000.native +>>> +>>> with pg8000.native.Connection("postgres", password="cpsnow") as con: +... con.run(""" SELECT CAST('{"a":1, "b":2}' AS jsonb) @> :v """, v={"b": 2}) +[[True]] + + +Retrieve Column Metadata From Results +````````````````````````````````````` + +Find the column metadata returned from a query: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("create temporary table quark (id serial, name text)") +>>> for name in ('Up', 'Down'): +... con.run("INSERT INTO quark (name) VALUES (:name)", name=name) +>>> # Now execute the query +>>> +>>> con.run("SELECT * FROM quark") +[[1, 'Up'], [2, 'Down']] +>>> +>>> # and retrieve the metadata +>>> +>>> con.columns +[{'table_oid': ..., 'column_attrnum': 1, 'type_oid': 23, 'type_size': 4, 'type_modifier': -1, 'format': 0, 'name': 'id'}, {'table_oid': ..., 'column_attrnum': 2, 'type_oid': 25, 'type_size': -1, 'type_modifier': -1, 'format': 0, 'name': 'name'}] +>>> +>>> # Show just the column names +>>> +>>> [c['name'] for c in con.columns] +['id', 'name'] +>>> +>>> con.close() + + +Notices And Notifications +````````````````````````` + +PostgreSQL `notices +<https://www.postgresql.org/docs/current/static/plpgsql-errors-and-messages.html>`_ are +stored in a deque called ``Connection.notices`` and added using the ``append()`` +method. Similarly there are ``Connection.notifications`` for `notifications +<https://www.postgresql.org/docs/current/static/sql-notify.html>`_ and +``Connection.parameter_statuses`` for changes to the server configuration. Here's an +example: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("LISTEN aliens_landed") +>>> con.run("NOTIFY aliens_landed") +>>> # A notification is a tuple containing (backend_pid, channel, payload) +>>> +>>> con.notifications[0] +(..., 'aliens_landed', '') +>>> +>>> con.close() + + +LIMIT ALL +````````` + +You might think that the following would work, but in fact it fails: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("SELECT 'silo 1' LIMIT :lim", lim='ALL') +Traceback (most recent call last): +pg8000.exceptions.DatabaseError: ... +>>> +>>> con.close() + +Instead the `docs say <https://www.postgresql.org/docs/current/sql-select.html>`_ that +you can send ``null`` as an alternative to ``ALL``, which does work: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("SELECT 'silo 1' LIMIT :lim", lim=None) +[['silo 1']] +>>> +>>> con.close() + + +IN and NOT IN +````````````` + +You might think that the following would work, but in fact the server doesn't like it: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("SELECT 'silo 1' WHERE 'a' IN :v", v=['a', 'b']) +Traceback (most recent call last): +pg8000.exceptions.DatabaseError: ... +>>> +>>> con.close() + +instead you can write it using the `unnest +<https://www.postgresql.org/docs/current/functions-array.html>`_ function: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run( +... "SELECT 'silo 1' WHERE 'a' IN (SELECT unnest(CAST(:v as varchar[])))", +... v=['a', 'b']) +[['silo 1']] +>>> con.close() + +and you can do the same for ``NOT IN``. + + +Many SQL Statements Can't Be Parameterized +`````````````````````````````````````````` + +In PostgreSQL parameters can only be used for `data values, not identifiers +<https://www.postgresql.org/docs/current/xfunc-sql.html#XFUNC-SQL-FUNCTION-ARGUMENTS>`_. +Sometimes this might not work as expected, for example the following fails: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> channel = 'top_secret' +>>> +>>> con.run("LISTEN :channel", channel=channel) +Traceback (most recent call last): +pg8000.exceptions.DatabaseError: ... +>>> +>>> con.close() + +It fails because the PostgreSQL server doesn't allow this statement to have any +parameters. There are many SQL statements that one might think would have parameters, +but don't. For these cases the SQL has to be created manually, being careful to use the +``identifier()`` and ``literal()`` functions to escape the values to avoid `SQL +injection attacks <https://en.wikipedia.org/wiki/SQL_injection>`_: + +>>> from pg8000.native import Connection, identifier, literal +>>> +>>> con = Connection("postgres", password="cpsnow") +>>> +>>> channel = 'top_secret' +>>> payload = 'Aliens Landed!' +>>> con.run(f"LISTEN {identifier(channel)}") +>>> con.run(f"NOTIFY {identifier(channel)}, {literal(payload)}") +>>> +>>> con.notifications[0] +(..., 'top_secret', 'Aliens Landed!') +>>> +>>> con.close() + + +COPY FROM And TO A Stream +````````````````````````` + +The SQL `COPY <https://www.postgresql.org/docs/current/sql-copy.html>`_ statement can be +used to copy from and to a file or file-like object. Here' an example using the CSV +format: + +>>> import pg8000.native +>>> from io import StringIO +>>> import csv +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> # Create a CSV file in memory +>>> +>>> stream_in = StringIO() +>>> csv_writer = csv.writer(stream_in) +>>> csv_writer.writerow([1, "electron"]) +12 +>>> csv_writer.writerow([2, "muon"]) +8 +>>> csv_writer.writerow([3, "tau"]) +7 +>>> stream_in.seek(0) +0 +>>> +>>> # Create a table and then copy the CSV into it +>>> +>>> con.run("CREATE TEMPORARY TABLE lepton (id SERIAL, name TEXT)") +>>> con.run("COPY lepton FROM STDIN WITH (FORMAT CSV)", stream=stream_in) +>>> +>>> # COPY from a table to a stream +>>> +>>> stream_out = StringIO() +>>> con.run("COPY lepton TO STDOUT WITH (FORMAT CSV)", stream=stream_out) +>>> stream_out.seek(0) +0 +>>> for row in csv.reader(stream_out): +... print(row) +['1', 'electron'] +['2', 'muon'] +['3', 'tau'] +>>> +>>> con.close() + +It's also possible to COPY FROM an iterable, which is useful if you're creating rows +programmatically: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> # Generator function for creating rows +>>> def row_gen(): +... for i, name in ((1, "electron"), (2, "muon"), (3, "tau")): +... yield f"{i},{name}\n" +>>> +>>> # Create a table and then copy the CSV into it +>>> +>>> con.run("CREATE TEMPORARY TABLE lepton (id SERIAL, name TEXT)") +>>> con.run("COPY lepton FROM STDIN WITH (FORMAT CSV)", stream=row_gen()) +>>> +>>> # COPY from a table to a stream +>>> +>>> stream_out = StringIO() +>>> con.run("COPY lepton TO STDOUT WITH (FORMAT CSV)", stream=stream_out) +>>> stream_out.seek(0) +0 +>>> for row in csv.reader(stream_out): +... print(row) +['1', 'electron'] +['2', 'muon'] +['3', 'tau'] +>>> +>>> con.close() + + +Execute Multiple SQL Statements +``````````````````````````````` + +If you want to execute a series of SQL statements (eg. an ``.sql`` file), you can run +them as expected: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> statements = "SELECT 5; SELECT 'Erich Fromm';" +>>> +>>> con.run(statements) +[[5], ['Erich Fromm']] +>>> +>>> con.close() + +The only caveat is that when executing multiple statements you can't have any +parameters. + + +Quoted Identifiers in SQL +````````````````````````` + +Say you had a column called ``My Column``. Since it's case sensitive and contains a +space, you'd have to `surround it by double quotes +<https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIER>`_. +But you can't do: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("select 'hello' as "My Column"") +Traceback (most recent call last): +SyntaxError: invalid syntax... +>>> +>>> con.close() + +since Python uses double quotes to delimit string literals, so one solution is +to use Python's `triple quotes +<https://docs.python.org/3/tutorial/introduction.html#strings>`_ to delimit the string +instead: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run('''SELECT 'hello' AS "My Column"''') +[['hello']] +>>> +>>> con.close() + +another solution, that's especially useful if the identifier comes from an untrusted +source, is to use the ``identifier()`` function, which correctly quotes and escapes the +identifier as needed: + +>>> from pg8000.native import Connection, identifier +>>> +>>> con = Connection("postgres", password="cpsnow") +>>> +>>> sql = f"SELECT 'hello' as {identifier('My Column')}" +>>> print(sql) +SELECT 'hello' as "My Column" +>>> +>>> con.run(sql) +[['hello']] +>>> +>>> con.close() + +this approach guards against `SQL injection attacks +<https://en.wikipedia.org/wiki/SQL_injection>`_. One thing to note if you're using +explicit schemas (eg. ``pg_catalog.pg_language``) is that the schema name and table name +are both separate identifiers. So to escape them you'd do: + +>>> from pg8000.native import Connection, identifier +>>> +>>> con = Connection("postgres", password="cpsnow") +>>> +>>> query = ( +... f"SELECT lanname FROM {identifier('pg_catalog')}.{identifier('pg_language')} " +... f"WHERE lanname = 'sql'" +... ) +>>> print(query) +SELECT lanname FROM pg_catalog.pg_language WHERE lanname = 'sql' +>>> +>>> con.run(query) +[['sql']] +>>> +>>> con.close() + + +Custom adapter from a Python type to a PostgreSQL type +`````````````````````````````````````````````````````` + +pg8000 has a mapping from Python types to PostgreSQL types for when it needs to send +SQL parameters to the server. The default mapping that comes with pg8000 is designed to +work well in most cases, but you might want to add or replace the default mapping. + +A Python ``datetime.timedelta`` object is sent to the server as a PostgreSQL +``interval`` type, which has the ``oid`` 1186. But let's say we wanted to create our +own Python class to be sent as an ``interval`` type. Then we'd have to register an +adapter: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> class MyInterval(str): +... pass +>>> +>>> def my_interval_out(my_interval): +... return my_interval # Must return a str +>>> +>>> con.register_out_adapter(MyInterval, my_interval_out) +>>> con.run("SELECT CAST(:interval as interval)", interval=MyInterval("2 hours")) +[[datetime.timedelta(seconds=7200)]] +>>> +>>> con.close() + +Note that it still came back as a ``datetime.timedelta`` object because we only changed +the mapping from Python to PostgreSQL. See below for an example of how to change the +mapping from PostgreSQL to Python. + + +Custom adapter from a PostgreSQL type to a Python type +`````````````````````````````````````````````````````` + +pg8000 has a mapping from PostgreSQL types to Python types for when it receives SQL +results from the server. The default mapping that comes with pg8000 is designed to work +well in most cases, but you might want to add or replace the default mapping. + +If pg8000 receives PostgreSQL ``interval`` type, which has the ``oid`` 1186, it converts +it into a Python ``datetime.timedelta`` object. But let's say we wanted to create our +own Python class to be used instead of ``datetime.timedelta``. Then we'd have to +register an adapter: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> class MyInterval(str): +... pass +>>> +>>> def my_interval_in(my_interval_str): # The parameter is of type str +... return MyInterval(my_interval) +>>> +>>> con.register_in_adapter(1186, my_interval_in) +>>> con.run("SELECT \'2 years'") +[['2 years']] +>>> +>>> con.close() + +Note that registering the 'in' adapter only afects the mapping from the PostgreSQL type +to the Python type. See above for an example of how to change the mapping from +PostgreSQL to Python. + + +Could Not Determine Data Type Of Parameter +`````````````````````````````````````````` + +Sometimes you'll get the 'could not determine data type of parameter' error message from +the server: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("SELECT :v IS NULL", v=None) +Traceback (most recent call last): +pg8000.exceptions.DatabaseError: {'S': 'ERROR', 'V': 'ERROR', 'C': '42P18', 'M': 'could not determine data type of parameter $1', 'F': 'postgres.c', 'L': '...', 'R': 'exec_parse_message'} +>>> +>>> con.close() + +One way of solving it is to put a ``CAST`` in the SQL: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("SELECT cast(:v as TIMESTAMP) IS NULL", v=None) +[[True]] +>>> +>>> con.close() + +Another way is to override the type that pg8000 sends along with each parameter: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("SELECT :v IS NULL", v=None, types={'v': pg8000.native.TIMESTAMP}) +[[True]] +>>> +>>> con.close() + + +Prepared Statements +``````````````````` + +`Prepared statements <https://www.postgresql.org/docs/current/sql-prepare.html>`_ +can be useful in improving performance when you have a statement that's executed +repeatedly. Here's an example: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> # Create the prepared statement +>>> ps = con.prepare("SELECT cast(:v as varchar)") +>>> +>>> # Execute the statement repeatedly +>>> ps.run(v="speedy") +[['speedy']] +>>> ps.run(v="rapid") +[['rapid']] +>>> ps.run(v="swift") +[['swift']] +>>> +>>> # Close the prepared statement, releasing resources on the server +>>> ps.close() +>>> +>>> con.close() + + +Use Environment Variables As Connection Defaults +```````````````````````````````````````````````` + +You might want to use the current user as the database username for example: + +>>> import pg8000.native +>>> import getpass +>>> +>>> # Connect to the database with current user name +>>> username = getpass.getuser() +>>> connection = pg8000.native.Connection(username, password="cpsnow") +>>> +>>> connection.run("SELECT 'pilau'") +[['pilau']] +>>> +>>> connection.close() + +or perhaps you may want to use some of the same `environment variables that libpg uses +<https://www.postgresql.org/docs/current/libpq-envars.html>`_: + +>>> import pg8000.native +>>> from os import environ +>>> +>>> username = environ.get('PGUSER', 'postgres') +>>> password = environ.get('PGPASSWORD', 'cpsnow') +>>> host = environ.get('PGHOST', 'localhost') +>>> port = environ.get('PGPORT', '5432') +>>> database = environ.get('PGDATABASE') +>>> +>>> connection = pg8000.native.Connection( +... username, password=password, host=host, port=port, database=database) +>>> +>>> connection.run("SELECT 'Mr Cairo'") +[['Mr Cairo']] +>>> +>>> connection.close() + +It might be asked, why doesn't pg8000 have this behaviour built in? The thinking +follows the second aphorism of `The Zen of Python +<https://www.python.org/dev/peps/pep-0020/>`_: + + Explicit is better than implicit. + +So we've taken the approach of only being able to set connection parameters using the +``pg8000.native.Connection()`` constructor. + + +Connect To PostgreSQL Over SSL +`````````````````````````````` + +To connect to the server using SSL defaults do:: + + import pg8000.native + connection = pg8000.native.Connection('postgres', password="cpsnow", ssl_context=True) + connection.run("SELECT 'The game is afoot!'") + +To connect over SSL with custom settings, set the ``ssl_context`` parameter to an +|ssl.SSLContext|_ object: + +:: + + import pg8000.native + import ssl + + + ssl_context = ssl.create_default_context() + ssl_context.verify_mode = ssl.CERT_REQUIRED + ssl_context.load_verify_locations('root.pem') + connection = pg8000.native.Connection( + 'postgres', password="cpsnow", ssl_context=ssl_context) + +It may be that your PostgreSQL server is behind an SSL proxy server in which case you +can set a pg8000-specific attribute ``ssl.SSLContext.request_ssl = False`` which tells +pg8000 to connect using an SSL socket, but not to request SSL from the PostgreSQL +server: + +:: + + import pg8000.native + import ssl + + ssl_context = ssl.create_default_context() + ssl_context.request_ssl = False + connection = pg8000.native.Connection( + 'postgres', password="cpsnow", ssl_context=ssl_context) + + +Server-Side Cursors +``````````````````` + +You can use the SQL commands `DECLARE +<https://www.postgresql.org/docs/current/sql-declare.html>`_, +`FETCH <https://www.postgresql.org/docs/current/sql-fetch.html>`_, +`MOVE <https://www.postgresql.org/docs/current/sql-move.html>`_ and +`CLOSE <https://www.postgresql.org/docs/current/sql-close.html>`_ to manipulate +server-side cursors. For example: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection('postgres', password="cpsnow") +>>> con.run("START TRANSACTION") +>>> con.run("DECLARE c SCROLL CURSOR FOR SELECT * FROM generate_series(1, 100)") +>>> con.run("FETCH FORWARD 5 FROM c") +[[1], [2], [3], [4], [5]] +>>> con.run("MOVE FORWARD 50 FROM c") +>>> con.run("FETCH BACKWARD 10 FROM c") +[[54], [53], [52], [51], [50], [49], [48], [47], [46], [45]] +>>> con.run("CLOSE c") +>>> con.run("ROLLBACK") +>>> +>>> con.close() + + +BLOBs (Binary Large Objects) +```````````````````````````` + +There's a set of `SQL functions +<https://www.postgresql.org/docs/current/lo-funcs.html>`_ for manipulating BLOBs. +Here's an example: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection('postgres', password="cpsnow") +>>> +>>> # Create a BLOB and get its oid +>>> data = b'hello' +>>> res = con.run("SELECT lo_from_bytea(0, :data)", data=data) +>>> oid = res[0][0] +>>> +>>> # Create a table and store the oid of the BLOB +>>> con.run("CREATE TEMPORARY TABLE image (raster oid)") +>>> +>>> con.run("INSERT INTO image (raster) VALUES (:oid)", oid=oid) +>>> # Retrieve the data using the oid +>>> con.run("SELECT lo_get(:oid)", oid=oid) +[[b'hello']] +>>> +>>> # Add some data to the end of the BLOB +>>> more_data = b' all' +>>> offset = len(data) +>>> con.run( +... "SELECT lo_put(:oid, :offset, :data)", +... oid=oid, offset=offset, data=more_data) +[['']] +>>> con.run("SELECT lo_get(:oid)", oid=oid) +[[b'hello all']] +>>> +>>> # Download a part of the data +>>> con.run("SELECT lo_get(:oid, 6, 3)", oid=oid) +[[b'all']] +>>> +>>> con.close() + + +Replication Protocol +```````````````````` + +The PostgreSQL `Replication Protocol +<https://www.postgresql.org/docs/current/protocol-replication.html>`_ is supported using +the ``replication`` keyword when creating a connection: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection( +... 'postgres', password="cpsnow", replication="database") +>>> +>>> con.run("IDENTIFY_SYSTEM") +[['...', 1, '0/...', 'postgres']] +>>> +>>> con.close() + + +DB-API 2 Interactive Examples +----------------------------- + +These examples stick to the DB-API 2.0 standard. + + +Basic Example +````````````` + +Import pg8000, connect to the database, create a table, add some rows and then query the +table: + +>>> import pg8000.dbapi +>>> +>>> conn = pg8000.dbapi.connect(user="postgres", password="cpsnow") +>>> cursor = conn.cursor() +>>> cursor.execute("CREATE TEMPORARY TABLE book (id SERIAL, title TEXT)") +>>> cursor.execute( +... "INSERT INTO book (title) VALUES (%s), (%s) RETURNING id, title", +... ("Ender's Game", "Speaker for the Dead")) +>>> results = cursor.fetchall() +>>> for row in results: +... id, title = row +... print("id = %s, title = %s" % (id, title)) +id = 1, title = Ender's Game +id = 2, title = Speaker for the Dead +>>> conn.commit() +>>> +>>> conn.close() + + +Query Using Functions +````````````````````` + +Another query, using some PostgreSQL functions: + +>>> import pg8000.dbapi +>>> +>>> con = pg8000.dbapi.connect(user="postgres", password="cpsnow") +>>> cursor = con.cursor() +>>> +>>> cursor.execute("SELECT TO_CHAR(TIMESTAMP '2021-10-10', 'YYYY BC')") +>>> cursor.fetchone() +['2021 AD'] +>>> +>>> con.close() + + +Interval Type +````````````` + +A query that returns the PostgreSQL interval type: + +>>> import datetime +>>> import pg8000.dbapi +>>> +>>> con = pg8000.dbapi.connect(user="postgres", password="cpsnow") +>>> cursor = con.cursor() +>>> +>>> cursor.execute("SELECT timestamp '2013-12-01 16:06' - %s", +... (datetime.date(1980, 4, 27),)) +>>> cursor.fetchone() +[datetime.timedelta(days=12271, seconds=57960)] +>>> +>>> con.close() + + +Point Type +`````````` + +A round-trip with a `PostgreSQL point +<https://www.postgresql.org/docs/current/datatype-geometric.html>`_ type: + +>>> import pg8000.dbapi +>>> +>>> con = pg8000.dbapi.connect(user="postgres", password="cpsnow") +>>> cursor = con.cursor() +>>> +>>> cursor.execute("SELECT cast(%s as point)", ((2.3,1),)) +>>> cursor.fetchone() +[(2.3, 1.0)] +>>> +>>> con.close() + + +Numeric Parameter Style +``````````````````````` + +pg8000 supports all the DB-API parameter styles. Here's an example of using the +'numeric' parameter style: + +>>> import pg8000.dbapi +>>> +>>> pg8000.dbapi.paramstyle = "numeric" +>>> con = pg8000.dbapi.connect(user="postgres", password="cpsnow") +>>> cursor = con.cursor() +>>> +>>> cursor.execute("SELECT array_prepend(:1, CAST(:2 AS int[]))", (500, [1, 2, 3, 4],)) +>>> cursor.fetchone() +[[500, 1, 2, 3, 4]] +>>> pg8000.dbapi.paramstyle = "format" +>>> +>>> con.close() + + +Autocommit +`````````` + +Following the DB-API specification, autocommit is off by default. It can be turned on by +using the autocommit property of the connection: + +>>> import pg8000.dbapi +>>> +>>> con = pg8000.dbapi.connect(user="postgres", password="cpsnow") +>>> con.autocommit = True +>>> +>>> cur = con.cursor() +>>> cur.execute("vacuum") +>>> conn.autocommit = False +>>> cur.close() +>>> +>>> con.close() + + +Client Encoding +``````````````` + +When communicating with the server, pg8000 uses the character set that the server asks +it to use (the client encoding). By default the client encoding is the database's +character set (chosen when the database is created), but the client encoding can be +changed in a number of ways (eg. setting ``CLIENT_ENCODING`` in ``postgresql.conf``). +Another way of changing the client encoding is by using an SQL command. For example: + +>>> import pg8000.dbapi +>>> +>>> con = pg8000.dbapi.connect(user="postgres", password="cpsnow") +>>> cur = con.cursor() +>>> cur.execute("SET CLIENT_ENCODING TO 'UTF8'") +>>> cur.execute("SHOW CLIENT_ENCODING") +>>> cur.fetchone() +['UTF8'] +>>> cur.close() +>>> +>>> con.close() + + +JSON +```` + +JSON is sent to the server serialized, and returned de-serialized. Here's an example: + +>>> import json +>>> import pg8000.dbapi +>>> +>>> con = pg8000.dbapi.connect(user="postgres", password="cpsnow") +>>> cur = con.cursor() +>>> val = ['Apollo 11 Cave', True, 26.003] +>>> cur.execute("SELECT cast(%s as json)", (json.dumps(val),)) +>>> cur.fetchone() +[['Apollo 11 Cave', True, 26.003]] +>>> cur.close() +>>> +>>> con.close() + +JSON queries can be have parameters: + +>>> import pg8000.dbapi +>>> +>>> with pg8000.dbapi.connect("postgres", password="cpsnow") as con: +... cur = con.cursor() +... cur.execute(""" SELECT CAST('{"a":1, "b":2}' AS jsonb) @> %s """, ({"b": 2},)) +... for row in cur.fetchall(): +... print(row) +[True] + + +Retrieve Column Names From Results +`````````````````````````````````` + +Use the columns names retrieved from a query: + +>>> import pg8000 +>>> conn = pg8000.dbapi.connect(user="postgres", password="cpsnow") +>>> c = conn.cursor() +>>> c.execute("create temporary table quark (id serial, name text)") +>>> c.executemany("INSERT INTO quark (name) VALUES (%s)", (("Up",), ("Down",))) +>>> # +>>> # Now retrieve the results +>>> # +>>> c.execute("select * from quark") +>>> rows = c.fetchall() +>>> keys = [k[0] for k in c.description] +>>> results = [dict(zip(keys, row)) for row in rows] +>>> assert results == [{'id': 1, 'name': 'Up'}, {'id': 2, 'name': 'Down'}] +>>> +>>> conn.close() + + +COPY from and to a file +``````````````````````` + +The SQL `COPY <https://www.postgresql.org/docs/current/sql-copy.html>`__ statement can +be used to copy from and to a file or file-like object: + +>>> from io import StringIO +>>> import pg8000.dbapi +>>> +>>> con = pg8000.dbapi.connect(user="postgres", password="cpsnow") +>>> cur = con.cursor() +>>> # +>>> # COPY from a stream to a table +>>> # +>>> stream_in = StringIO('1\telectron\n2\tmuon\n3\ttau\n') +>>> cur = con.cursor() +>>> cur.execute("create temporary table lepton (id serial, name text)") +>>> cur.execute("COPY lepton FROM stdin", stream=stream_in) +>>> # +>>> # Now COPY from a table to a stream +>>> # +>>> stream_out = StringIO() +>>> cur.execute("copy lepton to stdout", stream=stream_out) +>>> stream_out.getvalue() +'1\telectron\n2\tmuon\n3\ttau\n' +>>> +>>> con.close() + + +Server-Side Cursors +``````````````````` + +You can use the SQL commands `DECLARE +<https://www.postgresql.org/docs/current/sql-declare.html>`_, +`FETCH <https://www.postgresql.org/docs/current/sql-fetch.html>`_, +`MOVE <https://www.postgresql.org/docs/current/sql-move.html>`_ and +`CLOSE <https://www.postgresql.org/docs/current/sql-close.html>`_ to manipulate +server-side cursors. For example: + +>>> import pg8000.dbapi +>>> +>>> con = pg8000.dbapi.connect(user="postgres", password="cpsnow") +>>> cur = con.cursor() +>>> cur.execute("START TRANSACTION") +>>> cur.execute( +... "DECLARE c SCROLL CURSOR FOR SELECT * FROM generate_series(1, 100)") +>>> cur.execute("FETCH FORWARD 5 FROM c") +>>> cur.fetchall() +([1], [2], [3], [4], [5]) +>>> cur.execute("MOVE FORWARD 50 FROM c") +>>> cur.execute("FETCH BACKWARD 10 FROM c") +>>> cur.fetchall() +([54], [53], [52], [51], [50], [49], [48], [47], [46], [45]) +>>> cur.execute("CLOSE c") +>>> cur.execute("ROLLBACK") +>>> +>>> con.close() + + +BLOBs (Binary Large Objects) +```````````````````````````` + +There's a set of `SQL functions +<https://www.postgresql.org/docs/current/lo-funcs.html>`_ for manipulating BLOBs. +Here's an example: + +>>> import pg8000.dbapi +>>> +>>> con = pg8000.dbapi.connect(user="postgres", password="cpsnow") +>>> cur = con.cursor() +>>> +>>> # Create a BLOB and get its oid +>>> data = b'hello' +>>> cur = con.cursor() +>>> cur.execute("SELECT lo_from_bytea(0, %s)", [data]) +>>> oid = cur.fetchone()[0] +>>> +>>> # Create a table and store the oid of the BLOB +>>> cur.execute("CREATE TEMPORARY TABLE image (raster oid)") +>>> cur.execute("INSERT INTO image (raster) VALUES (%s)", [oid]) +>>> +>>> # Retrieve the data using the oid +>>> cur.execute("SELECT lo_get(%s)", [oid]) +>>> cur.fetchall() +([b'hello'],) +>>> +>>> # Add some data to the end of the BLOB +>>> more_data = b' all' +>>> offset = len(data) +>>> cur.execute("SELECT lo_put(%s, %s, %s)", [oid, offset, more_data]) +>>> cur.execute("SELECT lo_get(%s)", [oid]) +>>> cur.fetchall() +([b'hello all'],) +>>> +>>> # Download a part of the data +>>> cur.execute("SELECT lo_get(%s, 6, 3)", [oid]) +>>> cur.fetchall() +([b'all'],) +>>> +>>> con.close() + + +Type Mapping +------------ + +The following table shows the default mapping between Python types and PostgreSQL types, +and vice versa. + +If pg8000 doesn't recognize a type that it receives from PostgreSQL, it will return it +as a ``str`` type. This is how pg8000 handles PostgreSQL ``enum`` and XML types. It's +possible to change the default mapping using adapters (see the examples). + +.. table:: Python to PostgreSQL Type Mapping + + +-----------------------+-----------------+-----------------------------------------+ + | Python Type | PostgreSQL Type | Notes | + +=======================+=================+=========================================+ + | bool | bool | | + +-----------------------+-----------------+-----------------------------------------+ + | int | int4 | | + +-----------------------+-----------------+-----------------------------------------+ + | str | text | | + +-----------------------+-----------------+-----------------------------------------+ + | float | float8 | | + +-----------------------+-----------------+-----------------------------------------+ + | decimal.Decimal | numeric | | + +-----------------------+-----------------+-----------------------------------------+ + | bytes | bytea | | + +-----------------------+-----------------+-----------------------------------------+ + | datetime.datetime | timestamp | +/-infinity PostgreSQL values are | + | (without tzinfo) | without | represented as Python ``str`` values. | + | | timezone | | + +-----------------------+-----------------+-----------------------------------------+ + | datetime.datetime | timestamp with | +/-infinity PostgreSQL values are | + | (with tzinfo) | timezone | represented as Python ``str`` values. | + +-----------------------+-----------------+-----------------------------------------+ + | datetime.date | date | +/-infinity PostgreSQL values are | + | | | represented as Python ``str`` values. | + +-----------------------+-----------------+-----------------------------------------+ + | datetime.time | time without | | + | | time zone | | + +-----------------------+-----------------+-----------------------------------------+ + | datetime.timedelta | interval | If an ``interval`` is too big for | + | | | ``datetime.timedelta`` then a | + | | | ``PGInterval`` is used. | + +-----------------------+-----------------+-----------------------------------------+ + | None | NULL | | + +-----------------------+-----------------+-----------------------------------------+ + | uuid.UUID | uuid | | + +-----------------------+-----------------+-----------------------------------------+ + | ipaddress.IPv4Address | inet | | + +-----------------------+-----------------+-----------------------------------------+ + | ipaddress.IPv6Address | inet | | + +-----------------------+-----------------+-----------------------------------------+ + | ipaddress.IPv4Network | inet | | + +-----------------------+-----------------+-----------------------------------------+ + | ipaddress.IPv6Network | inet | | + +-----------------------+-----------------+-----------------------------------------+ + | int | xid | | + +-----------------------+-----------------+-----------------------------------------+ + | list of int | INT4[] | | + +-----------------------+-----------------+-----------------------------------------+ + | list of float | FLOAT8[] | | + +-----------------------+-----------------+-----------------------------------------+ + | list of bool | BOOL[] | | + +-----------------------+-----------------+-----------------------------------------+ + | list of str | TEXT[] | | + +-----------------------+-----------------+-----------------------------------------+ + | int | int2vector | Only from PostgreSQL to Python | + +-----------------------+-----------------+-----------------------------------------+ + | JSON | json, jsonb | The Python JSON is provided as a Python | + | | | serialized string. Results returned as | + | | | de-serialized JSON. | + +-----------------------+-----------------+-----------------------------------------+ + | pg8000.Range | \*range | PostgreSQL multirange types are | + | | | represented in Python as a list of | + | | | range types. | + +-----------------------+-----------------+-----------------------------------------+ + | tuple | composite type | Only from Python to PostgreSQL | + +-----------------------+-----------------+-----------------------------------------+ + + + +Theory Of Operation +------------------- + + A concept is tolerated inside the microkernel only if moving it outside the kernel, + i.e., permitting competing implementations, would prevent the implementation of the + system's required functionality. + + -- Jochen Liedtke, Liedtke's minimality principle + +pg8000 is designed to be used with one thread per connection. + +Pg8000 communicates with the database using the `PostgreSQL Frontend/Backend Protocol +<https://www.postgresql.org/docs/current/protocol.html>`_ (FEBE). If a query has no +parameters, pg8000 uses the 'simple query protocol'. If a query does have parameters, +pg8000 uses the 'extended query protocol' with unnamed prepared statements. The steps +for a query with parameters are: + +1. Query comes in. + +#. Send a PARSE message to the server to create an unnamed prepared statement. + +#. Send a BIND message to run against the unnamed prepared statement, resulting in an + unnamed portal on the server. + +#. Send an EXECUTE message to read all the results from the portal. + +It's also possible to use named prepared statements. In which case the prepared +statement persists on the server, and represented in pg8000 using a +``PreparedStatement`` object. This means that the PARSE step gets executed once up +front, and then only the BIND and EXECUTE steps are repeated subsequently. + +There are a lot of PostgreSQL data types, but few primitive data types in Python. By +default, pg8000 doesn't send PostgreSQL data type information in the PARSE step, in +which case PostgreSQL assumes the types implied by the SQL statement. In some cases +PostgreSQL can't work out a parameter type and so an `explicit cast +<https://www.postgresql.org/docs/current/static/sql-expressions.html#SQL-SYNTAX-TYPE-CASTS>`_ +can be used in the SQL. + +In the FEBE protocol, each query parameter can be sent to the server either as binary +or text according to the format code. In pg8000 the parameters are always sent as text. + +Occasionally, the network connection between pg8000 and the server may go down. If +pg8000 encounters a network problem it'll raise an ``InterfaceError`` with the message +``network error`` and with the original exception set as the `cause +<https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement>`_. + + +Native API Docs +--------------- + +pg8000.native.Error +``````````````````` + +Generic exception that is the base exception of the other error exceptions. + + +pg8000.native.InterfaceError +```````````````````````````` + +For errors that originate within pg8000. + + +pg8000.native.DatabaseError +``````````````````````````` + +For errors that originate from the server. + +pg8000.native.Connection(user, host='localhost', database=None, port=5432, password=None, source_address=None, unix_sock=None, ssl_context=None, timeout=None, tcp_keepalive=True, application_name=None, replication=None, sock=None) +`````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````` + +Creates a connection to a PostgreSQL database. + +user + The username to connect to the PostgreSQL server with. If your server character + encoding is not ``ascii`` or ``utf8``, then you need to provide ``user`` as bytes, + eg. ``'my_name'.encode('EUC-JP')``. + +host + The hostname of the PostgreSQL server to connect with. Providing this parameter is + necessary for TCP/IP connections. One of either ``host`` or ``unix_sock`` must be + provided. The default is ``localhost``. + +database + The name of the database instance to connect with. If ``None`` then the PostgreSQL + server will assume the database name is the same as the username. If your server + character encoding is not ``ascii`` or ``utf8``, then you need to provide ``database`` + as bytes, eg. ``'my_db'.encode('EUC-JP')``. + +port + The TCP/IP port of the PostgreSQL server instance. This parameter defaults to + ``5432``, the registered common port of PostgreSQL TCP/IP servers. + +password + The user password to connect to the server with. This parameter is optional; if + omitted and the database server requests password-based authentication, the connection + will fail to open. If this parameter is provided but not + requested by the server, no error will occur. + + If your server character encoding is not ``ascii`` or ``utf8``, then you need to + provide ``password`` as bytes, eg. ``'my_password'.encode('EUC-JP')``. + +source_address + The source IP address which initiates the connection to the PostgreSQL server. The + default is ``None`` which means that the operating system will choose the source + address. + +unix_sock + The path to the UNIX socket to access the database through, for example, + ``'/tmp/.s.PGSQL.5432'``. One of either ``host`` or ``unix_sock`` must be provided. + +ssl_context + This governs SSL encryption for TCP/IP sockets. It can have three values: + + - ``None``, meaning no SSL (the default) + + - ``True``, means use SSL with an |ssl.SSLContext|_ created using + |ssl.create_default_context()|_ + + - An instance of |ssl.SSLContext|_ which will be used to create the SSL connection. + + If your PostgreSQL server is behind an SSL proxy, you can set the pg8000-specific + attribute ``ssl.SSLContext.request_ssl = False``, which tells pg8000 to use an SSL + socket, but not to request SSL from the PostgreSQL server. Note that this means you + can't use SCRAM authentication with channel binding. + +timeout + This is the time in seconds before the connection to the server will time out. The + default is ``None`` which means no timeout. + +tcp_keepalive + If ``True`` then use `TCP keepalive + <https://en.wikipedia.org/wiki/Keepalive#TCP_keepalive>`_. The default is ``True``. + +application_name + Sets the `application_name + <https://www.postgresql.org/docs/current/runtime-config-logging.html#GUC-APPLICATION-NAME>`_. + If your server character encoding is not ``ascii`` or ``utf8``, then you need to + provide values as bytes, eg. ``'my_application_name'.encode('EUC-JP')``. The default + is ``None`` which means that the server will set the application name. + +replication + Used to run in `streaming replication mode + <https://www.postgresql.org/docs/current/protocol-replication.html>`_. If your server + character encoding is not ``ascii`` or ``utf8``, then you need to provide values as + bytes, eg. ``'database'.encode('EUC-JP')``. + +sock + A socket-like object to use for the connection. For example, ``sock`` could be a plain + ``socket.socket``, or it could represent an SSH tunnel or perhaps an + ``ssl.SSLSocket`` to an SSL proxy. If an |ssl.SSLContext| is provided, then it will be + used to attempt to create an SSL socket from the provided socket. + +pg8000.native.Connection.notifications +`````````````````````````````````````` + +A deque of server-side `notifications +<https://www.postgresql.org/docs/current/sql-notify.html>`__ received by this database +connection (via the ``LISTEN`` / ``NOTIFY`` PostgreSQL commands). Each list item is a +three-element tuple containing the PostgreSQL backend PID that issued the notify, the +channel and the payload. + + +pg8000.native.Connection.notices +```````````````````````````````` + +A deque of server-side notices received by this database connection. + + +pg8000.native.Connection.parameter_statuses +``````````````````````````````````````````` + +A deque of server-side parameter statuses received by this database connection. + + +pg8000.native.Connection.run(sql, stream=None, types=None, \*\*kwargs) +`````````````````````````````````````````````````````````````````````` + +Executes an sql statement, and returns the results as a ``list``. For example:: + + con.run("SELECT * FROM cities where population > :pop", pop=10000) + +sql + The SQL statement to execute. Parameter placeholders appear as a ``:`` followed by the + parameter name. + +stream + For use with the PostgreSQL `COPY + <http://www.postgresql.org/docs/current/static/sql-copy.html>`__ command. The nature + of the parameter depends on whether the SQL command is ``COPY FROM`` or ``COPY TO``. + + ``COPY FROM`` + The stream parameter must be a readable file-like object or an iterable. If it's an + iterable then the items can be ``str`` or binary. + ``COPY TO`` + The stream parameter must be a writable file-like object. + +types + A dictionary of oids. A key corresponds to a parameter. + +kwargs + The parameters of the SQL statement. + + +pg8000.native.Connection.row_count +`````````````````````````````````` + +This read-only attribute contains the number of rows that the last ``run()`` method +produced (for query statements like ``SELECT``) or affected (for modification statements +like ``UPDATE``. + +The value is -1 if: + +- No ``run()`` method has been performed yet. +- There was no rowcount associated with the last ``run()``. + + +pg8000.native.Connection.columns +```````````````````````````````` + +A list of column metadata. Each item in the list is a dictionary with the following +keys: + +- name +- table_oid +- column_attrnum +- type_oid +- type_size +- type_modifier +- format + + +pg8000.native.Connection.close() +```````````````````````````````` + +Closes the database connection. + + +pg8000.native.Connection.register_out_adapter(typ, out_func) +```````````````````````````````````````````````````````````` + +Register a type adapter for types going out from pg8000 to the server. + +typ + The Python class that the adapter is for. + +out_func + A function that takes the Python object and returns its string representation + in the format that the server requires. + + +pg8000.native.Connection.register_in_adapter(oid, in_func) +`````````````````````````````````````````````````````````` + +Register a type adapter for types coming in from the server to pg8000. + +oid + The PostgreSQL type identifier found in the `pg_type system catalog + <https://www.postgresql.org/docs/current/catalog-pg-type.html>`_. + +in_func + A function that takes the PostgreSQL string representation and returns a corresponding + Python object. + + +pg8000.native.Connection.prepare(sql) +````````````````````````````````````` + +Returns a ``PreparedStatement`` object which represents a `prepared statement +<https://www.postgresql.org/docs/current/sql-prepare.html>`_ on the server. It can +subsequently be repeatedly executed. + +sql + The SQL statement to prepare. Parameter placeholders appear as a ``:`` followed by the + parameter name. + + +pg8000.native.PreparedStatement +``````````````````````````````` + +A prepared statement object is returned by the ``pg8000.native.Connection.prepare()`` +method of a connection. It has the following methods: + + +pg8000.native.PreparedStatement.run(\*\*kwargs) +``````````````````````````````````````````````` + +Executes the prepared statement, and returns the results as a ``tuple``. + +kwargs + The parameters of the prepared statement. + + +pg8000.native.PreparedStatement.close() +``````````````````````````````````````` + +Closes the prepared statement, releasing the prepared statement held on the server. + + +pg8000.native.identifier(ident) +``````````````````````````````` + +Correctly quotes and escapes a string to be used as an `SQL identifier +<https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS>`_. + +ident + The ``str`` to be used as an SQL identifier. + + +pg8000.native.literal(value) +```````````````````````````` + +Correctly quotes and escapes a value to be used as an `SQL literal +<https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-CONSTANTS>`_. + +value + The value to be used as an SQL literal. + + +DB-API 2 Docs +------------- + + +Properties +`````````` + + +pg8000.dbapi.apilevel +::::::::::::::::::::: + +The DBAPI level supported, currently "2.0". + + +pg8000.dbapi.threadsafety +::::::::::::::::::::::::: + +Integer constant stating the level of thread safety the DBAPI interface supports. For +pg8000, the threadsafety value is 1, meaning that threads may share the module but not +connections. + + +pg8000.dbapi.paramstyle +::::::::::::::::::::::: + +String property stating the type of parameter marker formatting expected by +the interface. This value defaults to "format", in which parameters are +marked in this format: "WHERE name=%s". + +As an extension to the DBAPI specification, this value is not constant; it can be +changed to any of the following values: + +qmark + Question mark style, eg. ``WHERE name=?`` + +numeric + Numeric positional style, eg. ``WHERE name=:1`` + +named + Named style, eg. ``WHERE name=:paramname`` + +format + printf format codes, eg. ``WHERE name=%s`` + +pyformat + Python format codes, eg. ``WHERE name=%(paramname)s`` + + +pg8000.dbapi.STRING +::::::::::::::::::: + +String type oid. + +pg8000.dbapi.BINARY +::::::::::::::::::: + + +pg8000.dbapi.NUMBER +::::::::::::::::::: + +Numeric type oid. + + +pg8000.dbapi.DATETIME +::::::::::::::::::::: + +Timestamp type oid + + +pg8000.dbapi.ROWID +:::::::::::::::::: + +ROWID type oid + + +Functions +````````` + +pg8000.dbapi.connect(user, host='localhost', database=None, port=5432, password=None, source_address=None, unix_sock=None, ssl_context=None, timeout=None, tcp_keepalive=True, application_name=None, replication=None, sock=None) +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + +Creates a connection to a PostgreSQL database. + +user + The username to connect to the PostgreSQL server with. If your server character + encoding is not ``ascii`` or ``utf8``, then you need to provide ``user`` as bytes, + eg. ``'my_name'.encode('EUC-JP')``. + +host + The hostname of the PostgreSQL server to connect with. Providing this parameter is + necessary for TCP/IP connections. One of either ``host`` or ``unix_sock`` must be + provided. The default is ``localhost``. + +database + The name of the database instance to connect with. If ``None`` then the PostgreSQL + server will assume the database name is the same as the username. If your server + character encoding is not ``ascii`` or ``utf8``, then you need to provide ``database`` + as bytes, eg. ``'my_db'.encode('EUC-JP')``. + +port + The TCP/IP port of the PostgreSQL server instance. This parameter defaults to + ``5432``, the registered common port of PostgreSQL TCP/IP servers. + +password + The user password to connect to the server with. This parameter is optional; if + omitted and the database server requests password-based authentication, the + connection will fail to open. If this parameter is provided but not requested by the + server, no error will occur. + + If your server character encoding is not ``ascii`` or ``utf8``, then you need to + provide ``password`` as bytes, eg. ``'my_password'.encode('EUC-JP')``. + +source_address + The source IP address which initiates the connection to the PostgreSQL server. The + default is ``None`` which means that the operating system will choose the source + address. + +unix_sock + The path to the UNIX socket to access the database through, for example, + ``'/tmp/.s.PGSQL.5432'``. One of either ``host`` or ``unix_sock`` must be provided. + +ssl_context + This governs SSL encryption for TCP/IP sockets. It can have three values: + + - ``None``, meaning no SSL (the default) + - ``True``, means use SSL with an |ssl.SSLContext|_ created using + |ssl.create_default_context()|_. + + - An instance of |ssl.SSLContext|_ which will be used to create the SSL connection. + + If your PostgreSQL server is behind an SSL proxy, you can set the pg8000-specific + attribute ``ssl.SSLContext.request_ssl = False``, which tells pg8000 to use an SSL + socket, but not to request SSL from the PostgreSQL server. Note that this means you + can't use SCRAM authentication with channel binding. + +timeout + This is the time in seconds before the connection to the server will time out. The + default is ``None`` which means no timeout. + +tcp_keepalive + If ``True`` then use `TCP keepalive + <https://en.wikipedia.org/wiki/Keepalive#TCP_keepalive>`_. The default is ``True``. + +application_name + Sets the `application_name + <https://www.postgresql.org/docs/current/runtime-config-logging.html#GUC-APPLICATION-NAME>`_. If your server character encoding is not ``ascii`` or ``utf8``, then you need to + provide values as bytes, eg. ``'my_application_name'.encode('EUC-JP')``. The default + is ``None`` which means that the server will set the application name. + +replication + Used to run in `streaming replication mode + <https://www.postgresql.org/docs/current/protocol-replication.html>`_. If your server + character encoding is not ``ascii`` or ``utf8``, then you need to provide values as + bytes, eg. ``'database'.encode('EUC-JP')``. + +sock + A socket-like object to use for the connection. For example, ``sock`` could be a plain + ``socket.socket``, or it could represent an SSH tunnel or perhaps an + ``ssl.SSLSocket`` to an SSL proxy. If an |ssl.SSLContext| is provided, then it will be + used to attempt to create an SSL socket from the provided socket. + + +pg8000.dbapi.Date(year, month, day) + +Construct an object holding a date value. + +This property is part of the `DBAPI 2.0 specification +<http://www.python.org/dev/peps/pep-0249/>`_. + +Returns: `datetime.date` + + +pg8000.dbapi.Time(hour, minute, second) +::::::::::::::::::::::::::::::::::::::: + +Construct an object holding a time value. + +Returns: ``datetime.time`` + + +pg8000.dbapi.Timestamp(year, month, day, hour, minute, second) +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + +Construct an object holding a timestamp value. + +Returns: ``datetime.datetime`` + + +pg8000.dbapi.DateFromTicks(ticks) +::::::::::::::::::::::::::::::::: + +Construct an object holding a date value from the given ticks value (number of seconds +since the epoch). + +Returns: ``datetime.datetime`` + + +pg8000.dbapi.TimeFromTicks(ticks) +::::::::::::::::::::::::::::::::: + +Construct an object holding a time value from the given ticks value (number of seconds +since the epoch). + +Returns: ``datetime.time`` + + +pg8000.dbapi.TimestampFromTicks(ticks) +:::::::::::::::::::::::::::::::::::::: + +Construct an object holding a timestamp value from the given ticks value (number of +seconds since the epoch). + +Returns: ``datetime.datetime`` + + +pg8000.dbapi.Binary(value) +:::::::::::::::::::::::::: + +Construct an object holding binary data. + +Returns: ``bytes``. + + +Generic Exceptions +`````````````````` + +Pg8000 uses the standard DBAPI 2.0 exception tree as "generic" exceptions. Generally, +more specific exception types are raised; these specific exception types are derived +from the generic exceptions. + +pg8000.dbapi.Warning +:::::::::::::::::::: + +Generic exception raised for important database warnings like data truncations. This +exception is not currently used by pg8000. + + +pg8000.dbapi.Error +:::::::::::::::::: + +Generic exception that is the base exception of all other error exceptions. + + +pg8000.dbapi.InterfaceError +::::::::::::::::::::::::::: + +Generic exception raised for errors that are related to the database interface rather +than the database itself. For example, if the interface attempts to use an SSL +connection but the server refuses, an InterfaceError will be raised. + + +pg8000.dbapi.DatabaseError +:::::::::::::::::::::::::: + +Generic exception raised for errors that are related to the database. This exception is +currently never raised by pg8000. + + +pg8000.dbapi.DataError +:::::::::::::::::::::: + +Generic exception raised for errors that are due to problems with the processed data. +This exception is not currently raised by pg8000. + + +pg8000.dbapi.OperationalError +::::::::::::::::::::::::::::: + +Generic exception raised for errors that are related to the database's operation and not +necessarily under the control of the programmer. This exception is currently never +raised by pg8000. + + +pg8000.dbapi.IntegrityError +::::::::::::::::::::::::::: + +Generic exception raised when the relational integrity of the database is affected. This +exception is not currently raised by pg8000. + + +pg8000.dbapi.InternalError +:::::::::::::::::::::::::: + +Generic exception raised when the database encounters an internal error. This is +currently only raised when unexpected state occurs in the pg8000 interface itself, and +is typically the result of a interface bug. + + +pg8000.dbapi.ProgrammingError +::::::::::::::::::::::::::::: + +Generic exception raised for programming errors. For example, this exception is raised +if more parameter fields are in a query string than there are available parameters. + + +pg8000.dbapi.NotSupportedError +:::::::::::::::::::::::::::::: + +Generic exception raised in case a method or database API was used which is not +supported by the database. + + +Classes +``````` + + +pg8000.dbapi.Connection +::::::::::::::::::::::: + +A connection object is returned by the ``pg8000.connect()`` function. It represents a +single physical connection to a PostgreSQL database. + + +pg8000.dbapi.Connection.autocommit +:::::::::::::::::::::::::::::::::: + +Following the DB-API specification, autocommit is off by default. It can be turned on by +setting this boolean pg8000-specific autocommit property to ``True``. + + +pg8000.dbapi.Connection.close() +::::::::::::::::::::::::::::::: + +Closes the database connection. + + +pg8000.dbapi.Connection.cursor() +:::::::::::::::::::::::::::::::: + +Creates a ``pg8000.dbapi.Cursor`` object bound to this connection. + + +pg8000.dbapi.Connection.rollback() +:::::::::::::::::::::::::::::::::: + +Rolls back the current database transaction. + + +pg8000.dbapi.Connection.tpc_begin(xid) +:::::::::::::::::::::::::::::::::::::: + +Begins a TPC transaction with the given transaction ID xid. This method should be +called outside of a transaction (i.e. nothing may have executed since the last +``commit()`` or ``rollback()``. Furthermore, it is an error to call ``commit()`` or +``rollback()`` within the TPC transaction. A ``ProgrammingError`` is raised, if the +application calls ``commit()`` or ``rollback()`` during an active TPC transaction. + + +pg8000.dbapi.Connection.tpc_commit(xid=None) +:::::::::::::::::::::::::::::::::::::::::::: + +When called with no arguments, ``tpc_commit()`` commits a TPC transaction previously +prepared with ``tpc_prepare()``. If ``tpc_commit()`` is called prior to +``tpc_prepare()``, a single phase commit is performed. A transaction manager may choose +to do this if only a single resource is participating in the global transaction. + +When called with a transaction ID ``xid``, the database commits the given transaction. +If an invalid transaction ID is provided, a ``ProgrammingError`` will be raised. This +form should be called outside of a transaction, and is intended for use in recovery. + +On return, the TPC transaction is ended. + + +pg8000.dbapi.Connection.tpc_prepare() +::::::::::::::::::::::::::::::::::::: + +Performs the first phase of a transaction started with ``.tpc_begin()``. A +``ProgrammingError`` is be raised if this method is called outside of a TPC transaction. + +After calling ``tpc_prepare()``, no statements can be executed until ``tpc_commit()`` or +``tpc_rollback()`` have been called. + + +pg8000.dbapi.Connection.tpc_recover() +::::::::::::::::::::::::::::::::::::: + +Returns a list of pending transaction IDs suitable for use with ``tpc_commit(xid)`` or +``tpc_rollback(xid)``. + + +pg8000.dbapi.Connection.tpc_rollback(xid=None) +:::::::::::::::::::::::::::::::::::::::::::::: + +When called with no arguments, ``tpc_rollback()`` rolls back a TPC transaction. It may +be called before or after ``tpc_prepare()``. + +When called with a transaction ID xid, it rolls back the given transaction. If an +invalid transaction ID is provided, a ``ProgrammingError`` is raised. This form should +be called outside of a transaction, and is intended for use in recovery. + +On return, the TPC transaction is ended. + + +pg8000.dbapi.Connection.xid(format_id, global_transaction_id, branch_qualifier) +::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + +Create a Transaction IDs (only global_transaction_id is used in pg) format_id and +branch_qualifier are not used in postgres global_transaction_id may be any string +identifier supported by postgres returns a tuple (format_id, global_transaction_id, +branch_qualifier) + + +pg8000.dbapi.Cursor +::::::::::::::::::: + +A cursor object is returned by the ``pg8000.dbapi.Connection.cursor()`` method of a +connection. It has the following attributes and methods: + +pg8000.dbapi.Cursor.arraysize +''''''''''''''''''''''''''''' + +This read/write attribute specifies the number of rows to fetch at a time with +``pg8000.dbapi.Cursor.fetchmany()``. It defaults to 1. + + +pg8000.dbapi.Cursor.connection +'''''''''''''''''''''''''''''' + +This read-only attribute contains a reference to the connection object (an instance of +``pg8000.dbapi.Connection``) on which the cursor was created. + + +pg8000.dbapi.Cursor.rowcount +'''''''''''''''''''''''''''' + +This read-only attribute contains the number of rows that the last ``execute()`` or +``executemany()`` method produced (for query statements like ``SELECT``) or affected +(for modification statements like ``UPDATE``. + +The value is -1 if: + +- No ``execute()`` or ``executemany()`` method has been performed yet on the cursor. + +- There was no rowcount associated with the last ``execute()``. + +- At least one of the statements executed as part of an ``executemany()`` had no row + count associated with it. + + +pg8000.dbapi.Cursor.description +''''''''''''''''''''''''''''''' + +This read-only attribute is a sequence of 7-item sequences. Each value contains +information describing one result column. The 7 items returned for each column are +(name, type_code, display_size, internal_size, precision, scale, null_ok). Only the +first two values are provided by the current implementation. + + +pg8000.dbapi.Cursor.close() +''''''''''''''''''''''''''' + +Closes the cursor. + + +pg8000.dbapi.Cursor.execute(operation, args=None, stream=None) +'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' + +Executes a database operation. Parameters may be provided as a sequence, or as a +mapping, depending upon the value of ``pg8000.dbapi.paramstyle``. Returns the cursor, +which may be iterated over. + +operation + The SQL statement to execute. + +args + If ``pg8000.dbapi.paramstyle`` is ``qmark``, ``numeric``, or ``format``, this + argument should be an array of parameters to bind into the statement. If + ``pg8000.dbapi.paramstyle`` is ``named``, the argument should be a ``dict`` mapping of + parameters. If ``pg8000.dbapi.paramstyle`` is ``pyformat``, the argument value may be + either an array or a mapping. + +stream + This is a pg8000 extension for use with the PostgreSQL `COPY + <http://www.postgresql.org/docs/current/static/sql-copy.html>`__ command. For a + ``COPY FROM`` the parameter must be a readable file-like object, and for ``COPY TO`` + it must be writable. + + +pg8000.dbapi.Cursor.executemany(operation, param_sets) +'''''''''''''''''''''''''''''''''''''''''''''''''''''' + +Prepare a database operation, and then execute it against all parameter sequences or +mappings provided. + +operation + The SQL statement to execute. + +parameter_sets + A sequence of parameters to execute the statement with. The values in the sequence + should be sequences or mappings of parameters, the same as the args argument of the + ``pg8000.dbapi.Cursor.execute()`` method. + + +pg8000.dbapi.Cursor.callproc(procname, parameters=None) +''''''''''''''''''''''''''''''''''''''''''''''''''''''' + +Call a stored database procedure with the given name and optional parameters. + + +procname + The name of the procedure to call. + +parameters + A list of parameters. + + +pg8000.dbapi.Cursor.fetchall() +'''''''''''''''''''''''''''''' + +Fetches all remaining rows of a query result. + +Returns: A sequence, each entry of which is a sequence of field values making up a row. + + +pg8000.dbapi.Cursor.fetchmany(size=None) +'''''''''''''''''''''''''''''''''''''''' + +Fetches the next set of rows of a query result. + +size + The number of rows to fetch when called. If not provided, the + ``pg8000.dbapi.Cursor.arraysize`` attribute value is used instead. + +Returns: A sequence, each entry of which is a sequence of field values making up a row. +If no more rows are available, an empty sequence will be returned. + + +pg8000.dbapi.Cursor.fetchone() +'''''''''''''''''''''''''''''' + +Fetch the next row of a query result set. + +Returns: A row as a sequence of field values, or ``None`` if no more rows are available. + + +pg8000.dbapi.Cursor.setinputsizes(\*sizes) +'''''''''''''''''''''''''''''''''''''''''' + +Used to set the parameter types of the next query. This is useful if it's difficult for +pg8000 to work out the types from the parameters themselves (eg. for parameters of type +None). + +sizes + Positional parameters that are either the Python type of the parameter to be sent, or + the PostgreSQL oid. Common oids are available as constants such as ``pg8000.STRING``, + ``pg8000.INTEGER``, ``pg8000.TIME`` etc. + + +pg8000.dbapi.Cursor.setoutputsize(size, column=None) +'''''''''''''''''''''''''''''''''''''''''''''''''''' + +Not implemented by pg8000. + + +pg8000.dbapi.Interval +''''''''''''''''''''' + +An Interval represents a measurement of time. In PostgreSQL, an interval is defined in +the measure of months, days, and microseconds; as such, the pg8000 interval type +represents the same information. + +Note that values of the ``pg8000.dbapi.Interval.microseconds``, +``pg8000.dbapi.Interval.days``, and ``pg8000.dbapi.Interval.months`` properties are +independently measured and cannot be converted to each other. A month may be 28, 29, 30, +or 31 days, and a day may occasionally be lengthened slightly by a leap second. + + +Design Decisions +---------------- + +For the ``Range`` type, the constructor follows the `PostgreSQL range constructor functions <https://www.postgresql.org/docs/current/rangetypes.html#RANGETYPES-CONSTRUCT>`_ +which makes `[closed, open) <https://fhur.me/posts/always-use-closed-open-intervals>`_ +the easiest to express: + +>>> from pg8000.types import Range +>>> +>>> pg_range = Range(2, 6) + + +Tests +----- + +- Install `tox <http://testrun.org/tox/latest/>`_: ``pip install tox`` + +- Enable the PostgreSQL hstore extension by running the SQL command: + ``create extension hstore;`` + +- Add a line to ``pg_hba.conf`` for the various authentication options: + +:: + + host pg8000_md5 all 127.0.0.1/32 md5 + host pg8000_gss all 127.0.0.1/32 gss + host pg8000_password all 127.0.0.1/32 password + host pg8000_scram_sha_256 all 127.0.0.1/32 scram-sha-256 + host all all 127.0.0.1/32 trust + +- Set password encryption to ``scram-sha-256`` in ``postgresql.conf``: + ``password_encryption = 'scram-sha-256'`` + +- Set the password for the postgres user: ``ALTER USER postgresql WITH PASSWORD 'pw';`` + +- Run ``tox`` from the ``pg8000`` directory: ``tox`` + +This will run the tests against the Python version of the virtual environment, on the +machine, and the installed PostgreSQL version listening on port 5432, or the ``PGPORT`` +environment variable if set. + +Benchmarks are run as part of the test suite at ``tests/test_benchmarks.py``. + + +README.rst +---------- + +This file is written in the `reStructuredText +<https://docutils.sourceforge.io/docs/user/rst/quickref.html>`_ format. To generate an +HTML page from it, do: + +- Activate the virtual environment: ``source venv/bin/activate`` +- Install ``Sphinx``: ``pip install Sphinx`` +- Run ``rst2html.py``: ``rst2html.py README.rst README.html`` + + +Doing A Release Of pg8000 +------------------------- + +Run ``tox`` to make sure all tests pass, then update the release notes, then do: + +:: + + git tag -a x.y.z -m "version x.y.z" + rm -r dist + python -m build + twine upload dist/* + + +Release Notes +------------- + +Version 1.30.2, 2023-09-17 +`````````````````````````` + +- Bug fix where dollar-quoted string constants weren't supported. + + +Version 1.30.1, 2023-07-29 +`````````````````````````` + +- There was a problem uploading the previous version (1.30.0) to PyPI because the + markup of the README.rst was invalid. There's now a step in the automated tests to + check for this. + + +Version 1.30.0, 2023-07-27 +`````````````````````````` + +- Remove support for Python 3.7 + +- Add a ``sock`` keyword parameter for creating a connection from a pre-configured + socket. + + +Version 1.29.8, 2023-06-16 +`````````````````````````` + +- Ranges don't work with legacy API. + + +Version 1.29.7, 2023-06-16 +`````````````````````````` + +- Add support for PostgreSQL ``range`` and ``multirange`` types. Previously pg8000 + would just return them as strings, but now they're returned as ``Range`` and lists of + ``Range``. + +- The PostgreSQL ``record`` type is now returned as a ``tuple`` of strings, whereas + before it was returned as one string. + + +Version 1.29.6, 2023-05-29 +`````````````````````````` + +- Fixed two bugs with composite types. Nulls should be represented by an empty string, + and in an array of composite types, the elements should be surrounded by double + quotes. + + +Version 1.29.5, 2023-05-09 +`````````````````````````` + +- Fixed bug where pg8000 didn't handle the case when the number of bytes received from + a socket was fewer than requested. This was being interpreted as a network error, but + in fact we just needed to wait until more bytes were available. + +- When using the ``PGInterval`` type, if a response from the server contained the period + ``millennium``, it wasn't recognised. This was caused by a spelling mistake where we + had ``millenium`` rather than ``millennium``. + +- Added support for sending PostgreSQL composite types. If a value is sent as a + ``tuple``, pg8000 will send it to the server as a ``(`` delimited composite string. + + +Version 1.29.4, 2022-12-14 +`````````````````````````` + +- Fixed bug in ``pg8000.dbapi`` in the ``setinputsizes()`` method where if a ``size`` + was a recognized Python type, the method failed. + + +Version 1.29.3, 2022-10-26 +`````````````````````````` + +- Upgrade the SCRAM library to version 1.4.3. This adds support for the case where the + client supports channel binding but the server doesn't. + + +Version 1.29.2, 2022-10-09 +`````````````````````````` + +- Fixed a bug where in a literal array, items such as ``\n`` and ``\r`` weren't + escaped properly before being sent to the server. + +- Fixed a bug where if the PostgreSQL server has a half-hour time zone set, values of + type ``timestamp with time zone`` failed. This has been fixed by using the ``parse`` + function of the ``dateutil`` package if the ``datetime`` parser fails. + + +Version 1.29.1, 2022-05-23 +`````````````````````````` + +- In trying to determine if there's been a failed commit, check for ``ROLLBACK TO + SAVEPOINT``. + + +Version 1.29.0, 2022-05-21 +`````````````````````````` + +- Implement a workaround for the `silent failed commit + <https://github.com/tlocke/pg8000/issues/36>`_ bug. + +- Previously if an empty string was sent as the query an exception would be raised, but + that isn't done now. + + +Version 1.28.3, 2022-05-18 +`````````````````````````` + +- Put back ``__version__`` attributes that were inadvertently removed. + + +Version 1.28.2, 2022-05-17 +`````````````````````````` + +- Use a build system that's compliant with PEP517. + + +Version 1.28.1, 2022-05-17 +`````````````````````````` + +- If when doing a ``COPY FROM`` the ``stream`` parameter is an iterator of ``str``, + pg8000 used to silently append a newline to the end. That no longer happens. + + +Version 1.28.0, 2022-05-17 +`````````````````````````` + +- When using the ``COPY FROM`` SQL statement, allow the ``stream`` parameter to be an + iterable. + + +Version 1.27.1, 2022-05-16 +`````````````````````````` + +- The ``seconds`` attribute of ``PGInterval`` is now always a ``float``, to cope with + fractional seconds. + +- Updated the ``interval`` parsers for ``iso_8601`` and ``sql_standard`` to take + account of fractional seconds. + + +Version 1.27.0, 2022-05-16 +`````````````````````````` + +- It used to be that by default, if pg8000 received an ``interval`` type from the server + and it was too big to fit into a ``datetime.timedelta`` then an exception would be + raised. Now if an interval is too big for ``datetime.timedelta`` a ``PGInterval`` is + returned. + +* pg8000 now supports all the output formats for an ``interval`` (``postgres``, + ``postgres_verbose``, ``iso_8601`` and ``sql_standard``). + + +Version 1.26.1, 2022-04-23 +`````````````````````````` + +- Make sure all tests are run by the GitHub Actions tests on commit. +- Remove support for Python 3.6 +- Remove support for PostgreSQL 9.6 + + +Version 1.26.0, 2022-04-18 +`````````````````````````` + +- When connecting, raise an ``InterfaceError('network error')`` rather than let the + underlying ``struct.error`` float up. + +- Make licence text the same as that used by the OSI. Previously the licence wording + differed slightly from the BSD 3 Clause licence at + https://opensource.org/licenses/BSD-3-Clause. This meant that automated tools didn't + pick it up as being Open Source. The changes are believed to not alter the meaning of the license at all. + + +Version 1.25.0, 2022-04-17 +`````````````````````````` + +- Fix more cases where a ``ResourceWarning`` would be raise because of a socket that had + been left open. + +- We now have a single ``InterfaceError`` with the message 'network error' for all + network errors, with the underlying exception held in the ``cause`` of the exception. + + +Version 1.24.2, 2022-04-15 +`````````````````````````` + +- To prevent a ``ResourceWarning`` close socket if a connection can't be created. + + +Version 1.24.1, 2022-03-02 +`````````````````````````` + +- Return pg +/-infinity dates as ``str``. Previously +/-infinity pg values would cause + an error when returned, but now we return +/-infinity as strings. + + +Version 1.24.0, 2022-02-06 +`````````````````````````` + +- Add SQL escape functions identifier() and literal() to the native API. For use when a + query can't be parameterised and the SQL string has to be created using untrusted + values. + + +Version 1.23.0, 2021-11-13 +`````````````````````````` + +- If a query has no parameters, then the query will no longer be parsed. Although there + are performance benefits for doing this, the main reason is to avoid query rewriting, + which can introduce errors. + + +Version 1.22.1, 2021-11-10 +`````````````````````````` + +- Fix bug in PGInterval type where ``str()`` failed for a millennia value. + + +Version 1.22.0, 2021-10-13 +`````````````````````````` + +- Rather than specifying the oids in the ``Parse`` step of the Postgres protocol, pg8000 + now omits them, and so Postgres will use the oids it determines from the query. This + makes the pg8000 code simpler and also it should also make the nuances of type + matching more straightforward. diff --git a/contrib/python/pg8000/.dist-info/top_level.txt b/contrib/python/pg8000/.dist-info/top_level.txt new file mode 100644 index 00000000000..94345bbec5b --- /dev/null +++ b/contrib/python/pg8000/.dist-info/top_level.txt @@ -0,0 +1 @@ +pg8000 diff --git a/contrib/python/pg8000/LICENSE b/contrib/python/pg8000/LICENSE new file mode 100644 index 00000000000..1c9c560e304 --- /dev/null +++ b/contrib/python/pg8000/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright Mathieu Fenniak and Contributors to pg8000. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/contrib/python/pg8000/README.rst b/contrib/python/pg8000/README.rst new file mode 100644 index 00000000000..2d0973d2685 --- /dev/null +++ b/contrib/python/pg8000/README.rst @@ -0,0 +1,2431 @@ +====== +pg8000 +====== + +.. |ssl.SSLContext| replace:: ``ssl.SSLContext`` +.. _ssl.SSLContext: https://docs.python.org/3/library/ssl.html#ssl.SSLContext + +.. |ssl.create_default_context()| replace:: ``ssl.create_default_context()`` +.. _ssl.create_default_context(): https://docs.python.org/3/library/ssl.html#ssl.create_default_context + +pg8000 is a pure-`Python <https://www.python.org/>`_ +`PostgreSQL <http://www.postgresql.org/>`_ driver that complies with +`DB-API 2.0 <http://www.python.org/dev/peps/pep-0249/>`_. It is tested on Python +versions 3.8+, on CPython and PyPy, and PostgreSQL versions 11+. pg8000's name comes +from the belief that it is probably about the 8000th PostgreSQL interface for Python. +pg8000 is distributed under the BSD 3-clause license. + +All bug reports, feature requests and contributions are welcome at +`http://github.com/tlocke/pg8000/ <http://github.com/tlocke/pg8000/>`_. + +.. image:: https://github.com/tlocke/pg8000/workflows/pg8000/badge.svg + :alt: Build Status + +.. contents:: Table of Contents + :depth: 2 + :local: + +Installation +------------ + +To install pg8000 using `pip` type: + +`pip install pg8000` + + +Native API Interactive Examples +------------------------------- + +pg8000 comes with two APIs, the native pg8000 API and the DB-API 2.0 standard +API. These are the examples for the native API, and the DB-API 2.0 examples +follow in the next section. + + +Basic Example +````````````` + +Import pg8000, connect to the database, create a table, add some rows and then +query the table: + +>>> import pg8000.native +>>> +>>> # Connect to the database with user name postgres +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> # Create a temporary table +>>> +>>> con.run("CREATE TEMPORARY TABLE book (id SERIAL, title TEXT)") +>>> +>>> # Populate the table +>>> +>>> for title in ("Ender's Game", "The Magus"): +... con.run("INSERT INTO book (title) VALUES (:title)", title=title) +>>> +>>> # Print all the rows in the table +>>> +>>> for row in con.run("SELECT * FROM book"): +... print(row) +[1, "Ender's Game"] +[2, 'The Magus'] +>>> +>>> con.close() + + +Transactions +```````````` + +Here's how to run groups of SQL statements in a +`transaction <https://www.postgresql.org/docs/current/tutorial-transactions.html>`_: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("START TRANSACTION") +>>> +>>> # Create a temporary table +>>> con.run("CREATE TEMPORARY TABLE book (id SERIAL, title TEXT)") +>>> +>>> for title in ("Ender's Game", "The Magus", "Phineas Finn"): +... con.run("INSERT INTO book (title) VALUES (:title)", title=title) +>>> con.run("COMMIT") +>>> for row in con.run("SELECT * FROM book"): +... print(row) +[1, "Ender's Game"] +[2, 'The Magus'] +[3, 'Phineas Finn'] +>>> +>>> con.close() + +rolling back a transaction: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> # Create a temporary table +>>> con.run("CREATE TEMPORARY TABLE book (id SERIAL, title TEXT)") +>>> +>>> for title in ("Ender's Game", "The Magus", "Phineas Finn"): +... con.run("INSERT INTO book (title) VALUES (:title)", title=title) +>>> +>>> con.run("START TRANSACTION") +>>> con.run("DELETE FROM book WHERE title = :title", title="Phineas Finn") +>>> con.run("ROLLBACK") +>>> for row in con.run("SELECT * FROM book"): +... print(row) +[1, "Ender's Game"] +[2, 'The Magus'] +[3, 'Phineas Finn'] +>>> +>>> con.close() + +NB. There is `a longstanding bug <https://github.com/tlocke/pg8000/issues/36>`_ +in the PostgreSQL server whereby if a `COMMIT` is issued against a failed +transaction, the transaction is silently rolled back, rather than an error being +returned. pg8000 attempts to detect when this has happened and raise an +`InterfaceError`. + + +Query Using Functions +````````````````````` + +Another query, using some PostgreSQL functions: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("SELECT TO_CHAR(TIMESTAMP '2021-10-10', 'YYYY BC')") +[['2021 AD']] +>>> +>>> con.close() + + +Interval Type +````````````` + +A query that returns the PostgreSQL interval type: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> import datetime +>>> +>>> ts = datetime.date(1980, 4, 27) +>>> con.run("SELECT timestamp '2013-12-01 16:06' - :ts", ts=ts) +[[datetime.timedelta(days=12271, seconds=57960)]] +>>> +>>> con.close() + + +Point Type +`````````` + +A round-trip with a +`PostgreSQL point <https://www.postgresql.org/docs/current/datatype-geometric.html>`_ +type: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("SELECT CAST(:pt as point)", pt=(2.3,1)) +[[(2.3, 1.0)]] +>>> +>>> con.close() + + +Client Encoding +``````````````` + +When communicating with the server, pg8000 uses the character set that the server asks +it to use (the client encoding). By default the client encoding is the database's +character set (chosen when the database is created), but the client encoding can be +changed in a number of ways (eg. setting ``CLIENT_ENCODING`` in ``postgresql.conf``). +Another way of changing the client encoding is by using an SQL command. For example: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("SET CLIENT_ENCODING TO 'UTF8'") +>>> con.run("SHOW CLIENT_ENCODING") +[['UTF8']] +>>> +>>> con.close() + + +JSON +```` + +`JSON <https://www.postgresql.org/docs/current/datatype-json.html>`_ always comes back +from the server de-serialized. If the JSON you want to send is a ``dict`` then you can +just do: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> val = {'name': 'Apollo 11 Cave', 'zebra': True, 'age': 26.003} +>>> con.run("SELECT CAST(:apollo as jsonb)", apollo=val) +[[{'age': 26.003, 'name': 'Apollo 11 Cave', 'zebra': True}]] +>>> +>>> con.close() + +JSON can always be sent in serialized form to the server: + +>>> import json +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> +>>> val = ['Apollo 11 Cave', True, 26.003] +>>> con.run("SELECT CAST(:apollo as jsonb)", apollo=json.dumps(val)) +[[['Apollo 11 Cave', True, 26.003]]] +>>> +>>> con.close() + +JSON queries can be have parameters: + +>>> import pg8000.native +>>> +>>> with pg8000.native.Connection("postgres", password="cpsnow") as con: +... con.run(""" SELECT CAST('{"a":1, "b":2}' AS jsonb) @> :v """, v={"b": 2}) +[[True]] + + +Retrieve Column Metadata From Results +````````````````````````````````````` + +Find the column metadata returned from a query: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("create temporary table quark (id serial, name text)") +>>> for name in ('Up', 'Down'): +... con.run("INSERT INTO quark (name) VALUES (:name)", name=name) +>>> # Now execute the query +>>> +>>> con.run("SELECT * FROM quark") +[[1, 'Up'], [2, 'Down']] +>>> +>>> # and retrieve the metadata +>>> +>>> con.columns +[{'table_oid': ..., 'column_attrnum': 1, 'type_oid': 23, 'type_size': 4, 'type_modifier': -1, 'format': 0, 'name': 'id'}, {'table_oid': ..., 'column_attrnum': 2, 'type_oid': 25, 'type_size': -1, 'type_modifier': -1, 'format': 0, 'name': 'name'}] +>>> +>>> # Show just the column names +>>> +>>> [c['name'] for c in con.columns] +['id', 'name'] +>>> +>>> con.close() + + +Notices And Notifications +````````````````````````` + +PostgreSQL `notices +<https://www.postgresql.org/docs/current/static/plpgsql-errors-and-messages.html>`_ are +stored in a deque called ``Connection.notices`` and added using the ``append()`` +method. Similarly there are ``Connection.notifications`` for `notifications +<https://www.postgresql.org/docs/current/static/sql-notify.html>`_ and +``Connection.parameter_statuses`` for changes to the server configuration. Here's an +example: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("LISTEN aliens_landed") +>>> con.run("NOTIFY aliens_landed") +>>> # A notification is a tuple containing (backend_pid, channel, payload) +>>> +>>> con.notifications[0] +(..., 'aliens_landed', '') +>>> +>>> con.close() + + +LIMIT ALL +````````` + +You might think that the following would work, but in fact it fails: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("SELECT 'silo 1' LIMIT :lim", lim='ALL') +Traceback (most recent call last): +pg8000.exceptions.DatabaseError: ... +>>> +>>> con.close() + +Instead the `docs say <https://www.postgresql.org/docs/current/sql-select.html>`_ that +you can send ``null`` as an alternative to ``ALL``, which does work: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("SELECT 'silo 1' LIMIT :lim", lim=None) +[['silo 1']] +>>> +>>> con.close() + + +IN and NOT IN +````````````` + +You might think that the following would work, but in fact the server doesn't like it: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("SELECT 'silo 1' WHERE 'a' IN :v", v=['a', 'b']) +Traceback (most recent call last): +pg8000.exceptions.DatabaseError: ... +>>> +>>> con.close() + +instead you can write it using the `unnest +<https://www.postgresql.org/docs/current/functions-array.html>`_ function: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run( +... "SELECT 'silo 1' WHERE 'a' IN (SELECT unnest(CAST(:v as varchar[])))", +... v=['a', 'b']) +[['silo 1']] +>>> con.close() + +and you can do the same for ``NOT IN``. + + +Many SQL Statements Can't Be Parameterized +`````````````````````````````````````````` + +In PostgreSQL parameters can only be used for `data values, not identifiers +<https://www.postgresql.org/docs/current/xfunc-sql.html#XFUNC-SQL-FUNCTION-ARGUMENTS>`_. +Sometimes this might not work as expected, for example the following fails: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> channel = 'top_secret' +>>> +>>> con.run("LISTEN :channel", channel=channel) +Traceback (most recent call last): +pg8000.exceptions.DatabaseError: ... +>>> +>>> con.close() + +It fails because the PostgreSQL server doesn't allow this statement to have any +parameters. There are many SQL statements that one might think would have parameters, +but don't. For these cases the SQL has to be created manually, being careful to use the +``identifier()`` and ``literal()`` functions to escape the values to avoid `SQL +injection attacks <https://en.wikipedia.org/wiki/SQL_injection>`_: + +>>> from pg8000.native import Connection, identifier, literal +>>> +>>> con = Connection("postgres", password="cpsnow") +>>> +>>> channel = 'top_secret' +>>> payload = 'Aliens Landed!' +>>> con.run(f"LISTEN {identifier(channel)}") +>>> con.run(f"NOTIFY {identifier(channel)}, {literal(payload)}") +>>> +>>> con.notifications[0] +(..., 'top_secret', 'Aliens Landed!') +>>> +>>> con.close() + + +COPY FROM And TO A Stream +````````````````````````` + +The SQL `COPY <https://www.postgresql.org/docs/current/sql-copy.html>`_ statement can be +used to copy from and to a file or file-like object. Here' an example using the CSV +format: + +>>> import pg8000.native +>>> from io import StringIO +>>> import csv +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> # Create a CSV file in memory +>>> +>>> stream_in = StringIO() +>>> csv_writer = csv.writer(stream_in) +>>> csv_writer.writerow([1, "electron"]) +12 +>>> csv_writer.writerow([2, "muon"]) +8 +>>> csv_writer.writerow([3, "tau"]) +7 +>>> stream_in.seek(0) +0 +>>> +>>> # Create a table and then copy the CSV into it +>>> +>>> con.run("CREATE TEMPORARY TABLE lepton (id SERIAL, name TEXT)") +>>> con.run("COPY lepton FROM STDIN WITH (FORMAT CSV)", stream=stream_in) +>>> +>>> # COPY from a table to a stream +>>> +>>> stream_out = StringIO() +>>> con.run("COPY lepton TO STDOUT WITH (FORMAT CSV)", stream=stream_out) +>>> stream_out.seek(0) +0 +>>> for row in csv.reader(stream_out): +... print(row) +['1', 'electron'] +['2', 'muon'] +['3', 'tau'] +>>> +>>> con.close() + +It's also possible to COPY FROM an iterable, which is useful if you're creating rows +programmatically: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> # Generator function for creating rows +>>> def row_gen(): +... for i, name in ((1, "electron"), (2, "muon"), (3, "tau")): +... yield f"{i},{name}\n" +>>> +>>> # Create a table and then copy the CSV into it +>>> +>>> con.run("CREATE TEMPORARY TABLE lepton (id SERIAL, name TEXT)") +>>> con.run("COPY lepton FROM STDIN WITH (FORMAT CSV)", stream=row_gen()) +>>> +>>> # COPY from a table to a stream +>>> +>>> stream_out = StringIO() +>>> con.run("COPY lepton TO STDOUT WITH (FORMAT CSV)", stream=stream_out) +>>> stream_out.seek(0) +0 +>>> for row in csv.reader(stream_out): +... print(row) +['1', 'electron'] +['2', 'muon'] +['3', 'tau'] +>>> +>>> con.close() + + +Execute Multiple SQL Statements +``````````````````````````````` + +If you want to execute a series of SQL statements (eg. an ``.sql`` file), you can run +them as expected: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> statements = "SELECT 5; SELECT 'Erich Fromm';" +>>> +>>> con.run(statements) +[[5], ['Erich Fromm']] +>>> +>>> con.close() + +The only caveat is that when executing multiple statements you can't have any +parameters. + + +Quoted Identifiers in SQL +````````````````````````` + +Say you had a column called ``My Column``. Since it's case sensitive and contains a +space, you'd have to `surround it by double quotes +<https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIER>`_. +But you can't do: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("select 'hello' as "My Column"") +Traceback (most recent call last): +SyntaxError: invalid syntax... +>>> +>>> con.close() + +since Python uses double quotes to delimit string literals, so one solution is +to use Python's `triple quotes +<https://docs.python.org/3/tutorial/introduction.html#strings>`_ to delimit the string +instead: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run('''SELECT 'hello' AS "My Column"''') +[['hello']] +>>> +>>> con.close() + +another solution, that's especially useful if the identifier comes from an untrusted +source, is to use the ``identifier()`` function, which correctly quotes and escapes the +identifier as needed: + +>>> from pg8000.native import Connection, identifier +>>> +>>> con = Connection("postgres", password="cpsnow") +>>> +>>> sql = f"SELECT 'hello' as {identifier('My Column')}" +>>> print(sql) +SELECT 'hello' as "My Column" +>>> +>>> con.run(sql) +[['hello']] +>>> +>>> con.close() + +this approach guards against `SQL injection attacks +<https://en.wikipedia.org/wiki/SQL_injection>`_. One thing to note if you're using +explicit schemas (eg. ``pg_catalog.pg_language``) is that the schema name and table name +are both separate identifiers. So to escape them you'd do: + +>>> from pg8000.native import Connection, identifier +>>> +>>> con = Connection("postgres", password="cpsnow") +>>> +>>> query = ( +... f"SELECT lanname FROM {identifier('pg_catalog')}.{identifier('pg_language')} " +... f"WHERE lanname = 'sql'" +... ) +>>> print(query) +SELECT lanname FROM pg_catalog.pg_language WHERE lanname = 'sql' +>>> +>>> con.run(query) +[['sql']] +>>> +>>> con.close() + + +Custom adapter from a Python type to a PostgreSQL type +`````````````````````````````````````````````````````` + +pg8000 has a mapping from Python types to PostgreSQL types for when it needs to send +SQL parameters to the server. The default mapping that comes with pg8000 is designed to +work well in most cases, but you might want to add or replace the default mapping. + +A Python ``datetime.timedelta`` object is sent to the server as a PostgreSQL +``interval`` type, which has the ``oid`` 1186. But let's say we wanted to create our +own Python class to be sent as an ``interval`` type. Then we'd have to register an +adapter: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> class MyInterval(str): +... pass +>>> +>>> def my_interval_out(my_interval): +... return my_interval # Must return a str +>>> +>>> con.register_out_adapter(MyInterval, my_interval_out) +>>> con.run("SELECT CAST(:interval as interval)", interval=MyInterval("2 hours")) +[[datetime.timedelta(seconds=7200)]] +>>> +>>> con.close() + +Note that it still came back as a ``datetime.timedelta`` object because we only changed +the mapping from Python to PostgreSQL. See below for an example of how to change the +mapping from PostgreSQL to Python. + + +Custom adapter from a PostgreSQL type to a Python type +`````````````````````````````````````````````````````` + +pg8000 has a mapping from PostgreSQL types to Python types for when it receives SQL +results from the server. The default mapping that comes with pg8000 is designed to work +well in most cases, but you might want to add or replace the default mapping. + +If pg8000 receives PostgreSQL ``interval`` type, which has the ``oid`` 1186, it converts +it into a Python ``datetime.timedelta`` object. But let's say we wanted to create our +own Python class to be used instead of ``datetime.timedelta``. Then we'd have to +register an adapter: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> class MyInterval(str): +... pass +>>> +>>> def my_interval_in(my_interval_str): # The parameter is of type str +... return MyInterval(my_interval) +>>> +>>> con.register_in_adapter(1186, my_interval_in) +>>> con.run("SELECT \'2 years'") +[['2 years']] +>>> +>>> con.close() + +Note that registering the 'in' adapter only afects the mapping from the PostgreSQL type +to the Python type. See above for an example of how to change the mapping from +PostgreSQL to Python. + + +Could Not Determine Data Type Of Parameter +`````````````````````````````````````````` + +Sometimes you'll get the 'could not determine data type of parameter' error message from +the server: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("SELECT :v IS NULL", v=None) +Traceback (most recent call last): +pg8000.exceptions.DatabaseError: {'S': 'ERROR', 'V': 'ERROR', 'C': '42P18', 'M': 'could not determine data type of parameter $1', 'F': 'postgres.c', 'L': '...', 'R': 'exec_parse_message'} +>>> +>>> con.close() + +One way of solving it is to put a ``CAST`` in the SQL: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("SELECT cast(:v as TIMESTAMP) IS NULL", v=None) +[[True]] +>>> +>>> con.close() + +Another way is to override the type that pg8000 sends along with each parameter: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> con.run("SELECT :v IS NULL", v=None, types={'v': pg8000.native.TIMESTAMP}) +[[True]] +>>> +>>> con.close() + + +Prepared Statements +``````````````````` + +`Prepared statements <https://www.postgresql.org/docs/current/sql-prepare.html>`_ +can be useful in improving performance when you have a statement that's executed +repeatedly. Here's an example: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection("postgres", password="cpsnow") +>>> +>>> # Create the prepared statement +>>> ps = con.prepare("SELECT cast(:v as varchar)") +>>> +>>> # Execute the statement repeatedly +>>> ps.run(v="speedy") +[['speedy']] +>>> ps.run(v="rapid") +[['rapid']] +>>> ps.run(v="swift") +[['swift']] +>>> +>>> # Close the prepared statement, releasing resources on the server +>>> ps.close() +>>> +>>> con.close() + + +Use Environment Variables As Connection Defaults +```````````````````````````````````````````````` + +You might want to use the current user as the database username for example: + +>>> import pg8000.native +>>> import getpass +>>> +>>> # Connect to the database with current user name +>>> username = getpass.getuser() +>>> connection = pg8000.native.Connection(username, password="cpsnow") +>>> +>>> connection.run("SELECT 'pilau'") +[['pilau']] +>>> +>>> connection.close() + +or perhaps you may want to use some of the same `environment variables that libpg uses +<https://www.postgresql.org/docs/current/libpq-envars.html>`_: + +>>> import pg8000.native +>>> from os import environ +>>> +>>> username = environ.get('PGUSER', 'postgres') +>>> password = environ.get('PGPASSWORD', 'cpsnow') +>>> host = environ.get('PGHOST', 'localhost') +>>> port = environ.get('PGPORT', '5432') +>>> database = environ.get('PGDATABASE') +>>> +>>> connection = pg8000.native.Connection( +... username, password=password, host=host, port=port, database=database) +>>> +>>> connection.run("SELECT 'Mr Cairo'") +[['Mr Cairo']] +>>> +>>> connection.close() + +It might be asked, why doesn't pg8000 have this behaviour built in? The thinking +follows the second aphorism of `The Zen of Python +<https://www.python.org/dev/peps/pep-0020/>`_: + + Explicit is better than implicit. + +So we've taken the approach of only being able to set connection parameters using the +``pg8000.native.Connection()`` constructor. + + +Connect To PostgreSQL Over SSL +`````````````````````````````` + +To connect to the server using SSL defaults do:: + + import pg8000.native + connection = pg8000.native.Connection('postgres', password="cpsnow", ssl_context=True) + connection.run("SELECT 'The game is afoot!'") + +To connect over SSL with custom settings, set the ``ssl_context`` parameter to an +|ssl.SSLContext|_ object: + +:: + + import pg8000.native + import ssl + + + ssl_context = ssl.create_default_context() + ssl_context.verify_mode = ssl.CERT_REQUIRED + ssl_context.load_verify_locations('root.pem') + connection = pg8000.native.Connection( + 'postgres', password="cpsnow", ssl_context=ssl_context) + +It may be that your PostgreSQL server is behind an SSL proxy server in which case you +can set a pg8000-specific attribute ``ssl.SSLContext.request_ssl = False`` which tells +pg8000 to connect using an SSL socket, but not to request SSL from the PostgreSQL +server: + +:: + + import pg8000.native + import ssl + + ssl_context = ssl.create_default_context() + ssl_context.request_ssl = False + connection = pg8000.native.Connection( + 'postgres', password="cpsnow", ssl_context=ssl_context) + + +Server-Side Cursors +``````````````````` + +You can use the SQL commands `DECLARE +<https://www.postgresql.org/docs/current/sql-declare.html>`_, +`FETCH <https://www.postgresql.org/docs/current/sql-fetch.html>`_, +`MOVE <https://www.postgresql.org/docs/current/sql-move.html>`_ and +`CLOSE <https://www.postgresql.org/docs/current/sql-close.html>`_ to manipulate +server-side cursors. For example: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection('postgres', password="cpsnow") +>>> con.run("START TRANSACTION") +>>> con.run("DECLARE c SCROLL CURSOR FOR SELECT * FROM generate_series(1, 100)") +>>> con.run("FETCH FORWARD 5 FROM c") +[[1], [2], [3], [4], [5]] +>>> con.run("MOVE FORWARD 50 FROM c") +>>> con.run("FETCH BACKWARD 10 FROM c") +[[54], [53], [52], [51], [50], [49], [48], [47], [46], [45]] +>>> con.run("CLOSE c") +>>> con.run("ROLLBACK") +>>> +>>> con.close() + + +BLOBs (Binary Large Objects) +```````````````````````````` + +There's a set of `SQL functions +<https://www.postgresql.org/docs/current/lo-funcs.html>`_ for manipulating BLOBs. +Here's an example: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection('postgres', password="cpsnow") +>>> +>>> # Create a BLOB and get its oid +>>> data = b'hello' +>>> res = con.run("SELECT lo_from_bytea(0, :data)", data=data) +>>> oid = res[0][0] +>>> +>>> # Create a table and store the oid of the BLOB +>>> con.run("CREATE TEMPORARY TABLE image (raster oid)") +>>> +>>> con.run("INSERT INTO image (raster) VALUES (:oid)", oid=oid) +>>> # Retrieve the data using the oid +>>> con.run("SELECT lo_get(:oid)", oid=oid) +[[b'hello']] +>>> +>>> # Add some data to the end of the BLOB +>>> more_data = b' all' +>>> offset = len(data) +>>> con.run( +... "SELECT lo_put(:oid, :offset, :data)", +... oid=oid, offset=offset, data=more_data) +[['']] +>>> con.run("SELECT lo_get(:oid)", oid=oid) +[[b'hello all']] +>>> +>>> # Download a part of the data +>>> con.run("SELECT lo_get(:oid, 6, 3)", oid=oid) +[[b'all']] +>>> +>>> con.close() + + +Replication Protocol +```````````````````` + +The PostgreSQL `Replication Protocol +<https://www.postgresql.org/docs/current/protocol-replication.html>`_ is supported using +the ``replication`` keyword when creating a connection: + +>>> import pg8000.native +>>> +>>> con = pg8000.native.Connection( +... 'postgres', password="cpsnow", replication="database") +>>> +>>> con.run("IDENTIFY_SYSTEM") +[['...', 1, '0/...', 'postgres']] +>>> +>>> con.close() + + +DB-API 2 Interactive Examples +----------------------------- + +These examples stick to the DB-API 2.0 standard. + + +Basic Example +````````````` + +Import pg8000, connect to the database, create a table, add some rows and then query the +table: + +>>> import pg8000.dbapi +>>> +>>> conn = pg8000.dbapi.connect(user="postgres", password="cpsnow") +>>> cursor = conn.cursor() +>>> cursor.execute("CREATE TEMPORARY TABLE book (id SERIAL, title TEXT)") +>>> cursor.execute( +... "INSERT INTO book (title) VALUES (%s), (%s) RETURNING id, title", +... ("Ender's Game", "Speaker for the Dead")) +>>> results = cursor.fetchall() +>>> for row in results: +... id, title = row +... print("id = %s, title = %s" % (id, title)) +id = 1, title = Ender's Game +id = 2, title = Speaker for the Dead +>>> conn.commit() +>>> +>>> conn.close() + + +Query Using Functions +````````````````````` + +Another query, using some PostgreSQL functions: + +>>> import pg8000.dbapi +>>> +>>> con = pg8000.dbapi.connect(user="postgres", password="cpsnow") +>>> cursor = con.cursor() +>>> +>>> cursor.execute("SELECT TO_CHAR(TIMESTAMP '2021-10-10', 'YYYY BC')") +>>> cursor.fetchone() +['2021 AD'] +>>> +>>> con.close() + + +Interval Type +````````````` + +A query that returns the PostgreSQL interval type: + +>>> import datetime +>>> import pg8000.dbapi +>>> +>>> con = pg8000.dbapi.connect(user="postgres", password="cpsnow") +>>> cursor = con.cursor() +>>> +>>> cursor.execute("SELECT timestamp '2013-12-01 16:06' - %s", +... (datetime.date(1980, 4, 27),)) +>>> cursor.fetchone() +[datetime.timedelta(days=12271, seconds=57960)] +>>> +>>> con.close() + + +Point Type +`````````` + +A round-trip with a `PostgreSQL point +<https://www.postgresql.org/docs/current/datatype-geometric.html>`_ type: + +>>> import pg8000.dbapi +>>> +>>> con = pg8000.dbapi.connect(user="postgres", password="cpsnow") +>>> cursor = con.cursor() +>>> +>>> cursor.execute("SELECT cast(%s as point)", ((2.3,1),)) +>>> cursor.fetchone() +[(2.3, 1.0)] +>>> +>>> con.close() + + +Numeric Parameter Style +``````````````````````` + +pg8000 supports all the DB-API parameter styles. Here's an example of using the +'numeric' parameter style: + +>>> import pg8000.dbapi +>>> +>>> pg8000.dbapi.paramstyle = "numeric" +>>> con = pg8000.dbapi.connect(user="postgres", password="cpsnow") +>>> cursor = con.cursor() +>>> +>>> cursor.execute("SELECT array_prepend(:1, CAST(:2 AS int[]))", (500, [1, 2, 3, 4],)) +>>> cursor.fetchone() +[[500, 1, 2, 3, 4]] +>>> pg8000.dbapi.paramstyle = "format" +>>> +>>> con.close() + + +Autocommit +`````````` + +Following the DB-API specification, autocommit is off by default. It can be turned on by +using the autocommit property of the connection: + +>>> import pg8000.dbapi +>>> +>>> con = pg8000.dbapi.connect(user="postgres", password="cpsnow") +>>> con.autocommit = True +>>> +>>> cur = con.cursor() +>>> cur.execute("vacuum") +>>> conn.autocommit = False +>>> cur.close() +>>> +>>> con.close() + + +Client Encoding +``````````````` + +When communicating with the server, pg8000 uses the character set that the server asks +it to use (the client encoding). By default the client encoding is the database's +character set (chosen when the database is created), but the client encoding can be +changed in a number of ways (eg. setting ``CLIENT_ENCODING`` in ``postgresql.conf``). +Another way of changing the client encoding is by using an SQL command. For example: + +>>> import pg8000.dbapi +>>> +>>> con = pg8000.dbapi.connect(user="postgres", password="cpsnow") +>>> cur = con.cursor() +>>> cur.execute("SET CLIENT_ENCODING TO 'UTF8'") +>>> cur.execute("SHOW CLIENT_ENCODING") +>>> cur.fetchone() +['UTF8'] +>>> cur.close() +>>> +>>> con.close() + + +JSON +```` + +JSON is sent to the server serialized, and returned de-serialized. Here's an example: + +>>> import json +>>> import pg8000.dbapi +>>> +>>> con = pg8000.dbapi.connect(user="postgres", password="cpsnow") +>>> cur = con.cursor() +>>> val = ['Apollo 11 Cave', True, 26.003] +>>> cur.execute("SELECT cast(%s as json)", (json.dumps(val),)) +>>> cur.fetchone() +[['Apollo 11 Cave', True, 26.003]] +>>> cur.close() +>>> +>>> con.close() + +JSON queries can be have parameters: + +>>> import pg8000.dbapi +>>> +>>> with pg8000.dbapi.connect("postgres", password="cpsnow") as con: +... cur = con.cursor() +... cur.execute(""" SELECT CAST('{"a":1, "b":2}' AS jsonb) @> %s """, ({"b": 2},)) +... for row in cur.fetchall(): +... print(row) +[True] + + +Retrieve Column Names From Results +`````````````````````````````````` + +Use the columns names retrieved from a query: + +>>> import pg8000 +>>> conn = pg8000.dbapi.connect(user="postgres", password="cpsnow") +>>> c = conn.cursor() +>>> c.execute("create temporary table quark (id serial, name text)") +>>> c.executemany("INSERT INTO quark (name) VALUES (%s)", (("Up",), ("Down",))) +>>> # +>>> # Now retrieve the results +>>> # +>>> c.execute("select * from quark") +>>> rows = c.fetchall() +>>> keys = [k[0] for k in c.description] +>>> results = [dict(zip(keys, row)) for row in rows] +>>> assert results == [{'id': 1, 'name': 'Up'}, {'id': 2, 'name': 'Down'}] +>>> +>>> conn.close() + + +COPY from and to a file +``````````````````````` + +The SQL `COPY <https://www.postgresql.org/docs/current/sql-copy.html>`__ statement can +be used to copy from and to a file or file-like object: + +>>> from io import StringIO +>>> import pg8000.dbapi +>>> +>>> con = pg8000.dbapi.connect(user="postgres", password="cpsnow") +>>> cur = con.cursor() +>>> # +>>> # COPY from a stream to a table +>>> # +>>> stream_in = StringIO('1\telectron\n2\tmuon\n3\ttau\n') +>>> cur = con.cursor() +>>> cur.execute("create temporary table lepton (id serial, name text)") +>>> cur.execute("COPY lepton FROM stdin", stream=stream_in) +>>> # +>>> # Now COPY from a table to a stream +>>> # +>>> stream_out = StringIO() +>>> cur.execute("copy lepton to stdout", stream=stream_out) +>>> stream_out.getvalue() +'1\telectron\n2\tmuon\n3\ttau\n' +>>> +>>> con.close() + + +Server-Side Cursors +``````````````````` + +You can use the SQL commands `DECLARE +<https://www.postgresql.org/docs/current/sql-declare.html>`_, +`FETCH <https://www.postgresql.org/docs/current/sql-fetch.html>`_, +`MOVE <https://www.postgresql.org/docs/current/sql-move.html>`_ and +`CLOSE <https://www.postgresql.org/docs/current/sql-close.html>`_ to manipulate +server-side cursors. For example: + +>>> import pg8000.dbapi +>>> +>>> con = pg8000.dbapi.connect(user="postgres", password="cpsnow") +>>> cur = con.cursor() +>>> cur.execute("START TRANSACTION") +>>> cur.execute( +... "DECLARE c SCROLL CURSOR FOR SELECT * FROM generate_series(1, 100)") +>>> cur.execute("FETCH FORWARD 5 FROM c") +>>> cur.fetchall() +([1], [2], [3], [4], [5]) +>>> cur.execute("MOVE FORWARD 50 FROM c") +>>> cur.execute("FETCH BACKWARD 10 FROM c") +>>> cur.fetchall() +([54], [53], [52], [51], [50], [49], [48], [47], [46], [45]) +>>> cur.execute("CLOSE c") +>>> cur.execute("ROLLBACK") +>>> +>>> con.close() + + +BLOBs (Binary Large Objects) +```````````````````````````` + +There's a set of `SQL functions +<https://www.postgresql.org/docs/current/lo-funcs.html>`_ for manipulating BLOBs. +Here's an example: + +>>> import pg8000.dbapi +>>> +>>> con = pg8000.dbapi.connect(user="postgres", password="cpsnow") +>>> cur = con.cursor() +>>> +>>> # Create a BLOB and get its oid +>>> data = b'hello' +>>> cur = con.cursor() +>>> cur.execute("SELECT lo_from_bytea(0, %s)", [data]) +>>> oid = cur.fetchone()[0] +>>> +>>> # Create a table and store the oid of the BLOB +>>> cur.execute("CREATE TEMPORARY TABLE image (raster oid)") +>>> cur.execute("INSERT INTO image (raster) VALUES (%s)", [oid]) +>>> +>>> # Retrieve the data using the oid +>>> cur.execute("SELECT lo_get(%s)", [oid]) +>>> cur.fetchall() +([b'hello'],) +>>> +>>> # Add some data to the end of the BLOB +>>> more_data = b' all' +>>> offset = len(data) +>>> cur.execute("SELECT lo_put(%s, %s, %s)", [oid, offset, more_data]) +>>> cur.execute("SELECT lo_get(%s)", [oid]) +>>> cur.fetchall() +([b'hello all'],) +>>> +>>> # Download a part of the data +>>> cur.execute("SELECT lo_get(%s, 6, 3)", [oid]) +>>> cur.fetchall() +([b'all'],) +>>> +>>> con.close() + + +Type Mapping +------------ + +The following table shows the default mapping between Python types and PostgreSQL types, +and vice versa. + +If pg8000 doesn't recognize a type that it receives from PostgreSQL, it will return it +as a ``str`` type. This is how pg8000 handles PostgreSQL ``enum`` and XML types. It's +possible to change the default mapping using adapters (see the examples). + +.. table:: Python to PostgreSQL Type Mapping + + +-----------------------+-----------------+-----------------------------------------+ + | Python Type | PostgreSQL Type | Notes | + +=======================+=================+=========================================+ + | bool | bool | | + +-----------------------+-----------------+-----------------------------------------+ + | int | int4 | | + +-----------------------+-----------------+-----------------------------------------+ + | str | text | | + +-----------------------+-----------------+-----------------------------------------+ + | float | float8 | | + +-----------------------+-----------------+-----------------------------------------+ + | decimal.Decimal | numeric | | + +-----------------------+-----------------+-----------------------------------------+ + | bytes | bytea | | + +-----------------------+-----------------+-----------------------------------------+ + | datetime.datetime | timestamp | +/-infinity PostgreSQL values are | + | (without tzinfo) | without | represented as Python ``str`` values. | + | | timezone | | + +-----------------------+-----------------+-----------------------------------------+ + | datetime.datetime | timestamp with | +/-infinity PostgreSQL values are | + | (with tzinfo) | timezone | represented as Python ``str`` values. | + +-----------------------+-----------------+-----------------------------------------+ + | datetime.date | date | +/-infinity PostgreSQL values are | + | | | represented as Python ``str`` values. | + +-----------------------+-----------------+-----------------------------------------+ + | datetime.time | time without | | + | | time zone | | + +-----------------------+-----------------+-----------------------------------------+ + | datetime.timedelta | interval | If an ``interval`` is too big for | + | | | ``datetime.timedelta`` then a | + | | | ``PGInterval`` is used. | + +-----------------------+-----------------+-----------------------------------------+ + | None | NULL | | + +-----------------------+-----------------+-----------------------------------------+ + | uuid.UUID | uuid | | + +-----------------------+-----------------+-----------------------------------------+ + | ipaddress.IPv4Address | inet | | + +-----------------------+-----------------+-----------------------------------------+ + | ipaddress.IPv6Address | inet | | + +-----------------------+-----------------+-----------------------------------------+ + | ipaddress.IPv4Network | inet | | + +-----------------------+-----------------+-----------------------------------------+ + | ipaddress.IPv6Network | inet | | + +-----------------------+-----------------+-----------------------------------------+ + | int | xid | | + +-----------------------+-----------------+-----------------------------------------+ + | list of int | INT4[] | | + +-----------------------+-----------------+-----------------------------------------+ + | list of float | FLOAT8[] | | + +-----------------------+-----------------+-----------------------------------------+ + | list of bool | BOOL[] | | + +-----------------------+-----------------+-----------------------------------------+ + | list of str | TEXT[] | | + +-----------------------+-----------------+-----------------------------------------+ + | int | int2vector | Only from PostgreSQL to Python | + +-----------------------+-----------------+-----------------------------------------+ + | JSON | json, jsonb | The Python JSON is provided as a Python | + | | | serialized string. Results returned as | + | | | de-serialized JSON. | + +-----------------------+-----------------+-----------------------------------------+ + | pg8000.Range | \*range | PostgreSQL multirange types are | + | | | represented in Python as a list of | + | | | range types. | + +-----------------------+-----------------+-----------------------------------------+ + | tuple | composite type | Only from Python to PostgreSQL | + +-----------------------+-----------------+-----------------------------------------+ + + + +Theory Of Operation +------------------- + + A concept is tolerated inside the microkernel only if moving it outside the kernel, + i.e., permitting competing implementations, would prevent the implementation of the + system's required functionality. + + -- Jochen Liedtke, Liedtke's minimality principle + +pg8000 is designed to be used with one thread per connection. + +Pg8000 communicates with the database using the `PostgreSQL Frontend/Backend Protocol +<https://www.postgresql.org/docs/current/protocol.html>`_ (FEBE). If a query has no +parameters, pg8000 uses the 'simple query protocol'. If a query does have parameters, +pg8000 uses the 'extended query protocol' with unnamed prepared statements. The steps +for a query with parameters are: + +1. Query comes in. + +#. Send a PARSE message to the server to create an unnamed prepared statement. + +#. Send a BIND message to run against the unnamed prepared statement, resulting in an + unnamed portal on the server. + +#. Send an EXECUTE message to read all the results from the portal. + +It's also possible to use named prepared statements. In which case the prepared +statement persists on the server, and represented in pg8000 using a +``PreparedStatement`` object. This means that the PARSE step gets executed once up +front, and then only the BIND and EXECUTE steps are repeated subsequently. + +There are a lot of PostgreSQL data types, but few primitive data types in Python. By +default, pg8000 doesn't send PostgreSQL data type information in the PARSE step, in +which case PostgreSQL assumes the types implied by the SQL statement. In some cases +PostgreSQL can't work out a parameter type and so an `explicit cast +<https://www.postgresql.org/docs/current/static/sql-expressions.html#SQL-SYNTAX-TYPE-CASTS>`_ +can be used in the SQL. + +In the FEBE protocol, each query parameter can be sent to the server either as binary +or text according to the format code. In pg8000 the parameters are always sent as text. + +Occasionally, the network connection between pg8000 and the server may go down. If +pg8000 encounters a network problem it'll raise an ``InterfaceError`` with the message +``network error`` and with the original exception set as the `cause +<https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement>`_. + + +Native API Docs +--------------- + +pg8000.native.Error +``````````````````` + +Generic exception that is the base exception of the other error exceptions. + + +pg8000.native.InterfaceError +```````````````````````````` + +For errors that originate within pg8000. + + +pg8000.native.DatabaseError +``````````````````````````` + +For errors that originate from the server. + +pg8000.native.Connection(user, host='localhost', database=None, port=5432, password=None, source_address=None, unix_sock=None, ssl_context=None, timeout=None, tcp_keepalive=True, application_name=None, replication=None, sock=None) +`````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````` + +Creates a connection to a PostgreSQL database. + +user + The username to connect to the PostgreSQL server with. If your server character + encoding is not ``ascii`` or ``utf8``, then you need to provide ``user`` as bytes, + eg. ``'my_name'.encode('EUC-JP')``. + +host + The hostname of the PostgreSQL server to connect with. Providing this parameter is + necessary for TCP/IP connections. One of either ``host`` or ``unix_sock`` must be + provided. The default is ``localhost``. + +database + The name of the database instance to connect with. If ``None`` then the PostgreSQL + server will assume the database name is the same as the username. If your server + character encoding is not ``ascii`` or ``utf8``, then you need to provide ``database`` + as bytes, eg. ``'my_db'.encode('EUC-JP')``. + +port + The TCP/IP port of the PostgreSQL server instance. This parameter defaults to + ``5432``, the registered common port of PostgreSQL TCP/IP servers. + +password + The user password to connect to the server with. This parameter is optional; if + omitted and the database server requests password-based authentication, the connection + will fail to open. If this parameter is provided but not + requested by the server, no error will occur. + + If your server character encoding is not ``ascii`` or ``utf8``, then you need to + provide ``password`` as bytes, eg. ``'my_password'.encode('EUC-JP')``. + +source_address + The source IP address which initiates the connection to the PostgreSQL server. The + default is ``None`` which means that the operating system will choose the source + address. + +unix_sock + The path to the UNIX socket to access the database through, for example, + ``'/tmp/.s.PGSQL.5432'``. One of either ``host`` or ``unix_sock`` must be provided. + +ssl_context + This governs SSL encryption for TCP/IP sockets. It can have three values: + + - ``None``, meaning no SSL (the default) + + - ``True``, means use SSL with an |ssl.SSLContext|_ created using + |ssl.create_default_context()|_ + + - An instance of |ssl.SSLContext|_ which will be used to create the SSL connection. + + If your PostgreSQL server is behind an SSL proxy, you can set the pg8000-specific + attribute ``ssl.SSLContext.request_ssl = False``, which tells pg8000 to use an SSL + socket, but not to request SSL from the PostgreSQL server. Note that this means you + can't use SCRAM authentication with channel binding. + +timeout + This is the time in seconds before the connection to the server will time out. The + default is ``None`` which means no timeout. + +tcp_keepalive + If ``True`` then use `TCP keepalive + <https://en.wikipedia.org/wiki/Keepalive#TCP_keepalive>`_. The default is ``True``. + +application_name + Sets the `application_name + <https://www.postgresql.org/docs/current/runtime-config-logging.html#GUC-APPLICATION-NAME>`_. + If your server character encoding is not ``ascii`` or ``utf8``, then you need to + provide values as bytes, eg. ``'my_application_name'.encode('EUC-JP')``. The default + is ``None`` which means that the server will set the application name. + +replication + Used to run in `streaming replication mode + <https://www.postgresql.org/docs/current/protocol-replication.html>`_. If your server + character encoding is not ``ascii`` or ``utf8``, then you need to provide values as + bytes, eg. ``'database'.encode('EUC-JP')``. + +sock + A socket-like object to use for the connection. For example, ``sock`` could be a plain + ``socket.socket``, or it could represent an SSH tunnel or perhaps an + ``ssl.SSLSocket`` to an SSL proxy. If an |ssl.SSLContext| is provided, then it will be + used to attempt to create an SSL socket from the provided socket. + +pg8000.native.Connection.notifications +`````````````````````````````````````` + +A deque of server-side `notifications +<https://www.postgresql.org/docs/current/sql-notify.html>`__ received by this database +connection (via the ``LISTEN`` / ``NOTIFY`` PostgreSQL commands). Each list item is a +three-element tuple containing the PostgreSQL backend PID that issued the notify, the +channel and the payload. + + +pg8000.native.Connection.notices +```````````````````````````````` + +A deque of server-side notices received by this database connection. + + +pg8000.native.Connection.parameter_statuses +``````````````````````````````````````````` + +A deque of server-side parameter statuses received by this database connection. + + +pg8000.native.Connection.run(sql, stream=None, types=None, \*\*kwargs) +`````````````````````````````````````````````````````````````````````` + +Executes an sql statement, and returns the results as a ``list``. For example:: + + con.run("SELECT * FROM cities where population > :pop", pop=10000) + +sql + The SQL statement to execute. Parameter placeholders appear as a ``:`` followed by the + parameter name. + +stream + For use with the PostgreSQL `COPY + <http://www.postgresql.org/docs/current/static/sql-copy.html>`__ command. The nature + of the parameter depends on whether the SQL command is ``COPY FROM`` or ``COPY TO``. + + ``COPY FROM`` + The stream parameter must be a readable file-like object or an iterable. If it's an + iterable then the items can be ``str`` or binary. + ``COPY TO`` + The stream parameter must be a writable file-like object. + +types + A dictionary of oids. A key corresponds to a parameter. + +kwargs + The parameters of the SQL statement. + + +pg8000.native.Connection.row_count +`````````````````````````````````` + +This read-only attribute contains the number of rows that the last ``run()`` method +produced (for query statements like ``SELECT``) or affected (for modification statements +like ``UPDATE``. + +The value is -1 if: + +- No ``run()`` method has been performed yet. +- There was no rowcount associated with the last ``run()``. + + +pg8000.native.Connection.columns +```````````````````````````````` + +A list of column metadata. Each item in the list is a dictionary with the following +keys: + +- name +- table_oid +- column_attrnum +- type_oid +- type_size +- type_modifier +- format + + +pg8000.native.Connection.close() +```````````````````````````````` + +Closes the database connection. + + +pg8000.native.Connection.register_out_adapter(typ, out_func) +```````````````````````````````````````````````````````````` + +Register a type adapter for types going out from pg8000 to the server. + +typ + The Python class that the adapter is for. + +out_func + A function that takes the Python object and returns its string representation + in the format that the server requires. + + +pg8000.native.Connection.register_in_adapter(oid, in_func) +`````````````````````````````````````````````````````````` + +Register a type adapter for types coming in from the server to pg8000. + +oid + The PostgreSQL type identifier found in the `pg_type system catalog + <https://www.postgresql.org/docs/current/catalog-pg-type.html>`_. + +in_func + A function that takes the PostgreSQL string representation and returns a corresponding + Python object. + + +pg8000.native.Connection.prepare(sql) +````````````````````````````````````` + +Returns a ``PreparedStatement`` object which represents a `prepared statement +<https://www.postgresql.org/docs/current/sql-prepare.html>`_ on the server. It can +subsequently be repeatedly executed. + +sql + The SQL statement to prepare. Parameter placeholders appear as a ``:`` followed by the + parameter name. + + +pg8000.native.PreparedStatement +``````````````````````````````` + +A prepared statement object is returned by the ``pg8000.native.Connection.prepare()`` +method of a connection. It has the following methods: + + +pg8000.native.PreparedStatement.run(\*\*kwargs) +``````````````````````````````````````````````` + +Executes the prepared statement, and returns the results as a ``tuple``. + +kwargs + The parameters of the prepared statement. + + +pg8000.native.PreparedStatement.close() +``````````````````````````````````````` + +Closes the prepared statement, releasing the prepared statement held on the server. + + +pg8000.native.identifier(ident) +``````````````````````````````` + +Correctly quotes and escapes a string to be used as an `SQL identifier +<https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS>`_. + +ident + The ``str`` to be used as an SQL identifier. + + +pg8000.native.literal(value) +```````````````````````````` + +Correctly quotes and escapes a value to be used as an `SQL literal +<https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-CONSTANTS>`_. + +value + The value to be used as an SQL literal. + + +DB-API 2 Docs +------------- + + +Properties +`````````` + + +pg8000.dbapi.apilevel +::::::::::::::::::::: + +The DBAPI level supported, currently "2.0". + + +pg8000.dbapi.threadsafety +::::::::::::::::::::::::: + +Integer constant stating the level of thread safety the DBAPI interface supports. For +pg8000, the threadsafety value is 1, meaning that threads may share the module but not +connections. + + +pg8000.dbapi.paramstyle +::::::::::::::::::::::: + +String property stating the type of parameter marker formatting expected by +the interface. This value defaults to "format", in which parameters are +marked in this format: "WHERE name=%s". + +As an extension to the DBAPI specification, this value is not constant; it can be +changed to any of the following values: + +qmark + Question mark style, eg. ``WHERE name=?`` + +numeric + Numeric positional style, eg. ``WHERE name=:1`` + +named + Named style, eg. ``WHERE name=:paramname`` + +format + printf format codes, eg. ``WHERE name=%s`` + +pyformat + Python format codes, eg. ``WHERE name=%(paramname)s`` + + +pg8000.dbapi.STRING +::::::::::::::::::: + +String type oid. + +pg8000.dbapi.BINARY +::::::::::::::::::: + + +pg8000.dbapi.NUMBER +::::::::::::::::::: + +Numeric type oid. + + +pg8000.dbapi.DATETIME +::::::::::::::::::::: + +Timestamp type oid + + +pg8000.dbapi.ROWID +:::::::::::::::::: + +ROWID type oid + + +Functions +````````` + +pg8000.dbapi.connect(user, host='localhost', database=None, port=5432, password=None, source_address=None, unix_sock=None, ssl_context=None, timeout=None, tcp_keepalive=True, application_name=None, replication=None, sock=None) +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + +Creates a connection to a PostgreSQL database. + +user + The username to connect to the PostgreSQL server with. If your server character + encoding is not ``ascii`` or ``utf8``, then you need to provide ``user`` as bytes, + eg. ``'my_name'.encode('EUC-JP')``. + +host + The hostname of the PostgreSQL server to connect with. Providing this parameter is + necessary for TCP/IP connections. One of either ``host`` or ``unix_sock`` must be + provided. The default is ``localhost``. + +database + The name of the database instance to connect with. If ``None`` then the PostgreSQL + server will assume the database name is the same as the username. If your server + character encoding is not ``ascii`` or ``utf8``, then you need to provide ``database`` + as bytes, eg. ``'my_db'.encode('EUC-JP')``. + +port + The TCP/IP port of the PostgreSQL server instance. This parameter defaults to + ``5432``, the registered common port of PostgreSQL TCP/IP servers. + +password + The user password to connect to the server with. This parameter is optional; if + omitted and the database server requests password-based authentication, the + connection will fail to open. If this parameter is provided but not requested by the + server, no error will occur. + + If your server character encoding is not ``ascii`` or ``utf8``, then you need to + provide ``password`` as bytes, eg. ``'my_password'.encode('EUC-JP')``. + +source_address + The source IP address which initiates the connection to the PostgreSQL server. The + default is ``None`` which means that the operating system will choose the source + address. + +unix_sock + The path to the UNIX socket to access the database through, for example, + ``'/tmp/.s.PGSQL.5432'``. One of either ``host`` or ``unix_sock`` must be provided. + +ssl_context + This governs SSL encryption for TCP/IP sockets. It can have three values: + + - ``None``, meaning no SSL (the default) + - ``True``, means use SSL with an |ssl.SSLContext|_ created using + |ssl.create_default_context()|_. + + - An instance of |ssl.SSLContext|_ which will be used to create the SSL connection. + + If your PostgreSQL server is behind an SSL proxy, you can set the pg8000-specific + attribute ``ssl.SSLContext.request_ssl = False``, which tells pg8000 to use an SSL + socket, but not to request SSL from the PostgreSQL server. Note that this means you + can't use SCRAM authentication with channel binding. + +timeout + This is the time in seconds before the connection to the server will time out. The + default is ``None`` which means no timeout. + +tcp_keepalive + If ``True`` then use `TCP keepalive + <https://en.wikipedia.org/wiki/Keepalive#TCP_keepalive>`_. The default is ``True``. + +application_name + Sets the `application_name + <https://www.postgresql.org/docs/current/runtime-config-logging.html#GUC-APPLICATION-NAME>`_. If your server character encoding is not ``ascii`` or ``utf8``, then you need to + provide values as bytes, eg. ``'my_application_name'.encode('EUC-JP')``. The default + is ``None`` which means that the server will set the application name. + +replication + Used to run in `streaming replication mode + <https://www.postgresql.org/docs/current/protocol-replication.html>`_. If your server + character encoding is not ``ascii`` or ``utf8``, then you need to provide values as + bytes, eg. ``'database'.encode('EUC-JP')``. + +sock + A socket-like object to use for the connection. For example, ``sock`` could be a plain + ``socket.socket``, or it could represent an SSH tunnel or perhaps an + ``ssl.SSLSocket`` to an SSL proxy. If an |ssl.SSLContext| is provided, then it will be + used to attempt to create an SSL socket from the provided socket. + + +pg8000.dbapi.Date(year, month, day) + +Construct an object holding a date value. + +This property is part of the `DBAPI 2.0 specification +<http://www.python.org/dev/peps/pep-0249/>`_. + +Returns: `datetime.date` + + +pg8000.dbapi.Time(hour, minute, second) +::::::::::::::::::::::::::::::::::::::: + +Construct an object holding a time value. + +Returns: ``datetime.time`` + + +pg8000.dbapi.Timestamp(year, month, day, hour, minute, second) +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + +Construct an object holding a timestamp value. + +Returns: ``datetime.datetime`` + + +pg8000.dbapi.DateFromTicks(ticks) +::::::::::::::::::::::::::::::::: + +Construct an object holding a date value from the given ticks value (number of seconds +since the epoch). + +Returns: ``datetime.datetime`` + + +pg8000.dbapi.TimeFromTicks(ticks) +::::::::::::::::::::::::::::::::: + +Construct an object holding a time value from the given ticks value (number of seconds +since the epoch). + +Returns: ``datetime.time`` + + +pg8000.dbapi.TimestampFromTicks(ticks) +:::::::::::::::::::::::::::::::::::::: + +Construct an object holding a timestamp value from the given ticks value (number of +seconds since the epoch). + +Returns: ``datetime.datetime`` + + +pg8000.dbapi.Binary(value) +:::::::::::::::::::::::::: + +Construct an object holding binary data. + +Returns: ``bytes``. + + +Generic Exceptions +`````````````````` + +Pg8000 uses the standard DBAPI 2.0 exception tree as "generic" exceptions. Generally, +more specific exception types are raised; these specific exception types are derived +from the generic exceptions. + +pg8000.dbapi.Warning +:::::::::::::::::::: + +Generic exception raised for important database warnings like data truncations. This +exception is not currently used by pg8000. + + +pg8000.dbapi.Error +:::::::::::::::::: + +Generic exception that is the base exception of all other error exceptions. + + +pg8000.dbapi.InterfaceError +::::::::::::::::::::::::::: + +Generic exception raised for errors that are related to the database interface rather +than the database itself. For example, if the interface attempts to use an SSL +connection but the server refuses, an InterfaceError will be raised. + + +pg8000.dbapi.DatabaseError +:::::::::::::::::::::::::: + +Generic exception raised for errors that are related to the database. This exception is +currently never raised by pg8000. + + +pg8000.dbapi.DataError +:::::::::::::::::::::: + +Generic exception raised for errors that are due to problems with the processed data. +This exception is not currently raised by pg8000. + + +pg8000.dbapi.OperationalError +::::::::::::::::::::::::::::: + +Generic exception raised for errors that are related to the database's operation and not +necessarily under the control of the programmer. This exception is currently never +raised by pg8000. + + +pg8000.dbapi.IntegrityError +::::::::::::::::::::::::::: + +Generic exception raised when the relational integrity of the database is affected. This +exception is not currently raised by pg8000. + + +pg8000.dbapi.InternalError +:::::::::::::::::::::::::: + +Generic exception raised when the database encounters an internal error. This is +currently only raised when unexpected state occurs in the pg8000 interface itself, and +is typically the result of a interface bug. + + +pg8000.dbapi.ProgrammingError +::::::::::::::::::::::::::::: + +Generic exception raised for programming errors. For example, this exception is raised +if more parameter fields are in a query string than there are available parameters. + + +pg8000.dbapi.NotSupportedError +:::::::::::::::::::::::::::::: + +Generic exception raised in case a method or database API was used which is not +supported by the database. + + +Classes +``````` + + +pg8000.dbapi.Connection +::::::::::::::::::::::: + +A connection object is returned by the ``pg8000.connect()`` function. It represents a +single physical connection to a PostgreSQL database. + + +pg8000.dbapi.Connection.autocommit +:::::::::::::::::::::::::::::::::: + +Following the DB-API specification, autocommit is off by default. It can be turned on by +setting this boolean pg8000-specific autocommit property to ``True``. + + +pg8000.dbapi.Connection.close() +::::::::::::::::::::::::::::::: + +Closes the database connection. + + +pg8000.dbapi.Connection.cursor() +:::::::::::::::::::::::::::::::: + +Creates a ``pg8000.dbapi.Cursor`` object bound to this connection. + + +pg8000.dbapi.Connection.rollback() +:::::::::::::::::::::::::::::::::: + +Rolls back the current database transaction. + + +pg8000.dbapi.Connection.tpc_begin(xid) +:::::::::::::::::::::::::::::::::::::: + +Begins a TPC transaction with the given transaction ID xid. This method should be +called outside of a transaction (i.e. nothing may have executed since the last +``commit()`` or ``rollback()``. Furthermore, it is an error to call ``commit()`` or +``rollback()`` within the TPC transaction. A ``ProgrammingError`` is raised, if the +application calls ``commit()`` or ``rollback()`` during an active TPC transaction. + + +pg8000.dbapi.Connection.tpc_commit(xid=None) +:::::::::::::::::::::::::::::::::::::::::::: + +When called with no arguments, ``tpc_commit()`` commits a TPC transaction previously +prepared with ``tpc_prepare()``. If ``tpc_commit()`` is called prior to +``tpc_prepare()``, a single phase commit is performed. A transaction manager may choose +to do this if only a single resource is participating in the global transaction. + +When called with a transaction ID ``xid``, the database commits the given transaction. +If an invalid transaction ID is provided, a ``ProgrammingError`` will be raised. This +form should be called outside of a transaction, and is intended for use in recovery. + +On return, the TPC transaction is ended. + + +pg8000.dbapi.Connection.tpc_prepare() +::::::::::::::::::::::::::::::::::::: + +Performs the first phase of a transaction started with ``.tpc_begin()``. A +``ProgrammingError`` is be raised if this method is called outside of a TPC transaction. + +After calling ``tpc_prepare()``, no statements can be executed until ``tpc_commit()`` or +``tpc_rollback()`` have been called. + + +pg8000.dbapi.Connection.tpc_recover() +::::::::::::::::::::::::::::::::::::: + +Returns a list of pending transaction IDs suitable for use with ``tpc_commit(xid)`` or +``tpc_rollback(xid)``. + + +pg8000.dbapi.Connection.tpc_rollback(xid=None) +:::::::::::::::::::::::::::::::::::::::::::::: + +When called with no arguments, ``tpc_rollback()`` rolls back a TPC transaction. It may +be called before or after ``tpc_prepare()``. + +When called with a transaction ID xid, it rolls back the given transaction. If an +invalid transaction ID is provided, a ``ProgrammingError`` is raised. This form should +be called outside of a transaction, and is intended for use in recovery. + +On return, the TPC transaction is ended. + + +pg8000.dbapi.Connection.xid(format_id, global_transaction_id, branch_qualifier) +::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + +Create a Transaction IDs (only global_transaction_id is used in pg) format_id and +branch_qualifier are not used in postgres global_transaction_id may be any string +identifier supported by postgres returns a tuple (format_id, global_transaction_id, +branch_qualifier) + + +pg8000.dbapi.Cursor +::::::::::::::::::: + +A cursor object is returned by the ``pg8000.dbapi.Connection.cursor()`` method of a +connection. It has the following attributes and methods: + +pg8000.dbapi.Cursor.arraysize +''''''''''''''''''''''''''''' + +This read/write attribute specifies the number of rows to fetch at a time with +``pg8000.dbapi.Cursor.fetchmany()``. It defaults to 1. + + +pg8000.dbapi.Cursor.connection +'''''''''''''''''''''''''''''' + +This read-only attribute contains a reference to the connection object (an instance of +``pg8000.dbapi.Connection``) on which the cursor was created. + + +pg8000.dbapi.Cursor.rowcount +'''''''''''''''''''''''''''' + +This read-only attribute contains the number of rows that the last ``execute()`` or +``executemany()`` method produced (for query statements like ``SELECT``) or affected +(for modification statements like ``UPDATE``. + +The value is -1 if: + +- No ``execute()`` or ``executemany()`` method has been performed yet on the cursor. + +- There was no rowcount associated with the last ``execute()``. + +- At least one of the statements executed as part of an ``executemany()`` had no row + count associated with it. + + +pg8000.dbapi.Cursor.description +''''''''''''''''''''''''''''''' + +This read-only attribute is a sequence of 7-item sequences. Each value contains +information describing one result column. The 7 items returned for each column are +(name, type_code, display_size, internal_size, precision, scale, null_ok). Only the +first two values are provided by the current implementation. + + +pg8000.dbapi.Cursor.close() +''''''''''''''''''''''''''' + +Closes the cursor. + + +pg8000.dbapi.Cursor.execute(operation, args=None, stream=None) +'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' + +Executes a database operation. Parameters may be provided as a sequence, or as a +mapping, depending upon the value of ``pg8000.dbapi.paramstyle``. Returns the cursor, +which may be iterated over. + +operation + The SQL statement to execute. + +args + If ``pg8000.dbapi.paramstyle`` is ``qmark``, ``numeric``, or ``format``, this + argument should be an array of parameters to bind into the statement. If + ``pg8000.dbapi.paramstyle`` is ``named``, the argument should be a ``dict`` mapping of + parameters. If ``pg8000.dbapi.paramstyle`` is ``pyformat``, the argument value may be + either an array or a mapping. + +stream + This is a pg8000 extension for use with the PostgreSQL `COPY + <http://www.postgresql.org/docs/current/static/sql-copy.html>`__ command. For a + ``COPY FROM`` the parameter must be a readable file-like object, and for ``COPY TO`` + it must be writable. + + +pg8000.dbapi.Cursor.executemany(operation, param_sets) +'''''''''''''''''''''''''''''''''''''''''''''''''''''' + +Prepare a database operation, and then execute it against all parameter sequences or +mappings provided. + +operation + The SQL statement to execute. + +parameter_sets + A sequence of parameters to execute the statement with. The values in the sequence + should be sequences or mappings of parameters, the same as the args argument of the + ``pg8000.dbapi.Cursor.execute()`` method. + + +pg8000.dbapi.Cursor.callproc(procname, parameters=None) +''''''''''''''''''''''''''''''''''''''''''''''''''''''' + +Call a stored database procedure with the given name and optional parameters. + + +procname + The name of the procedure to call. + +parameters + A list of parameters. + + +pg8000.dbapi.Cursor.fetchall() +'''''''''''''''''''''''''''''' + +Fetches all remaining rows of a query result. + +Returns: A sequence, each entry of which is a sequence of field values making up a row. + + +pg8000.dbapi.Cursor.fetchmany(size=None) +'''''''''''''''''''''''''''''''''''''''' + +Fetches the next set of rows of a query result. + +size + The number of rows to fetch when called. If not provided, the + ``pg8000.dbapi.Cursor.arraysize`` attribute value is used instead. + +Returns: A sequence, each entry of which is a sequence of field values making up a row. +If no more rows are available, an empty sequence will be returned. + + +pg8000.dbapi.Cursor.fetchone() +'''''''''''''''''''''''''''''' + +Fetch the next row of a query result set. + +Returns: A row as a sequence of field values, or ``None`` if no more rows are available. + + +pg8000.dbapi.Cursor.setinputsizes(\*sizes) +'''''''''''''''''''''''''''''''''''''''''' + +Used to set the parameter types of the next query. This is useful if it's difficult for +pg8000 to work out the types from the parameters themselves (eg. for parameters of type +None). + +sizes + Positional parameters that are either the Python type of the parameter to be sent, or + the PostgreSQL oid. Common oids are available as constants such as ``pg8000.STRING``, + ``pg8000.INTEGER``, ``pg8000.TIME`` etc. + + +pg8000.dbapi.Cursor.setoutputsize(size, column=None) +'''''''''''''''''''''''''''''''''''''''''''''''''''' + +Not implemented by pg8000. + + +pg8000.dbapi.Interval +''''''''''''''''''''' + +An Interval represents a measurement of time. In PostgreSQL, an interval is defined in +the measure of months, days, and microseconds; as such, the pg8000 interval type +represents the same information. + +Note that values of the ``pg8000.dbapi.Interval.microseconds``, +``pg8000.dbapi.Interval.days``, and ``pg8000.dbapi.Interval.months`` properties are +independently measured and cannot be converted to each other. A month may be 28, 29, 30, +or 31 days, and a day may occasionally be lengthened slightly by a leap second. + + +Design Decisions +---------------- + +For the ``Range`` type, the constructor follows the `PostgreSQL range constructor functions <https://www.postgresql.org/docs/current/rangetypes.html#RANGETYPES-CONSTRUCT>`_ +which makes `[closed, open) <https://fhur.me/posts/always-use-closed-open-intervals>`_ +the easiest to express: + +>>> from pg8000.types import Range +>>> +>>> pg_range = Range(2, 6) + + +Tests +----- + +- Install `tox <http://testrun.org/tox/latest/>`_: ``pip install tox`` + +- Enable the PostgreSQL hstore extension by running the SQL command: + ``create extension hstore;`` + +- Add a line to ``pg_hba.conf`` for the various authentication options: + +:: + + host pg8000_md5 all 127.0.0.1/32 md5 + host pg8000_gss all 127.0.0.1/32 gss + host pg8000_password all 127.0.0.1/32 password + host pg8000_scram_sha_256 all 127.0.0.1/32 scram-sha-256 + host all all 127.0.0.1/32 trust + +- Set password encryption to ``scram-sha-256`` in ``postgresql.conf``: + ``password_encryption = 'scram-sha-256'`` + +- Set the password for the postgres user: ``ALTER USER postgresql WITH PASSWORD 'pw';`` + +- Run ``tox`` from the ``pg8000`` directory: ``tox`` + +This will run the tests against the Python version of the virtual environment, on the +machine, and the installed PostgreSQL version listening on port 5432, or the ``PGPORT`` +environment variable if set. + +Benchmarks are run as part of the test suite at ``tests/test_benchmarks.py``. + + +README.rst +---------- + +This file is written in the `reStructuredText +<https://docutils.sourceforge.io/docs/user/rst/quickref.html>`_ format. To generate an +HTML page from it, do: + +- Activate the virtual environment: ``source venv/bin/activate`` +- Install ``Sphinx``: ``pip install Sphinx`` +- Run ``rst2html.py``: ``rst2html.py README.rst README.html`` + + +Doing A Release Of pg8000 +------------------------- + +Run ``tox`` to make sure all tests pass, then update the release notes, then do: + +:: + + git tag -a x.y.z -m "version x.y.z" + rm -r dist + python -m build + twine upload dist/* + + +Release Notes +------------- + +Version 1.30.2, 2023-09-17 +`````````````````````````` + +- Bug fix where dollar-quoted string constants weren't supported. + + +Version 1.30.1, 2023-07-29 +`````````````````````````` + +- There was a problem uploading the previous version (1.30.0) to PyPI because the + markup of the README.rst was invalid. There's now a step in the automated tests to + check for this. + + +Version 1.30.0, 2023-07-27 +`````````````````````````` + +- Remove support for Python 3.7 + +- Add a ``sock`` keyword parameter for creating a connection from a pre-configured + socket. + + +Version 1.29.8, 2023-06-16 +`````````````````````````` + +- Ranges don't work with legacy API. + + +Version 1.29.7, 2023-06-16 +`````````````````````````` + +- Add support for PostgreSQL ``range`` and ``multirange`` types. Previously pg8000 + would just return them as strings, but now they're returned as ``Range`` and lists of + ``Range``. + +- The PostgreSQL ``record`` type is now returned as a ``tuple`` of strings, whereas + before it was returned as one string. + + +Version 1.29.6, 2023-05-29 +`````````````````````````` + +- Fixed two bugs with composite types. Nulls should be represented by an empty string, + and in an array of composite types, the elements should be surrounded by double + quotes. + + +Version 1.29.5, 2023-05-09 +`````````````````````````` + +- Fixed bug where pg8000 didn't handle the case when the number of bytes received from + a socket was fewer than requested. This was being interpreted as a network error, but + in fact we just needed to wait until more bytes were available. + +- When using the ``PGInterval`` type, if a response from the server contained the period + ``millennium``, it wasn't recognised. This was caused by a spelling mistake where we + had ``millenium`` rather than ``millennium``. + +- Added support for sending PostgreSQL composite types. If a value is sent as a + ``tuple``, pg8000 will send it to the server as a ``(`` delimited composite string. + + +Version 1.29.4, 2022-12-14 +`````````````````````````` + +- Fixed bug in ``pg8000.dbapi`` in the ``setinputsizes()`` method where if a ``size`` + was a recognized Python type, the method failed. + + +Version 1.29.3, 2022-10-26 +`````````````````````````` + +- Upgrade the SCRAM library to version 1.4.3. This adds support for the case where the + client supports channel binding but the server doesn't. + + +Version 1.29.2, 2022-10-09 +`````````````````````````` + +- Fixed a bug where in a literal array, items such as ``\n`` and ``\r`` weren't + escaped properly before being sent to the server. + +- Fixed a bug where if the PostgreSQL server has a half-hour time zone set, values of + type ``timestamp with time zone`` failed. This has been fixed by using the ``parse`` + function of the ``dateutil`` package if the ``datetime`` parser fails. + + +Version 1.29.1, 2022-05-23 +`````````````````````````` + +- In trying to determine if there's been a failed commit, check for ``ROLLBACK TO + SAVEPOINT``. + + +Version 1.29.0, 2022-05-21 +`````````````````````````` + +- Implement a workaround for the `silent failed commit + <https://github.com/tlocke/pg8000/issues/36>`_ bug. + +- Previously if an empty string was sent as the query an exception would be raised, but + that isn't done now. + + +Version 1.28.3, 2022-05-18 +`````````````````````````` + +- Put back ``__version__`` attributes that were inadvertently removed. + + +Version 1.28.2, 2022-05-17 +`````````````````````````` + +- Use a build system that's compliant with PEP517. + + +Version 1.28.1, 2022-05-17 +`````````````````````````` + +- If when doing a ``COPY FROM`` the ``stream`` parameter is an iterator of ``str``, + pg8000 used to silently append a newline to the end. That no longer happens. + + +Version 1.28.0, 2022-05-17 +`````````````````````````` + +- When using the ``COPY FROM`` SQL statement, allow the ``stream`` parameter to be an + iterable. + + +Version 1.27.1, 2022-05-16 +`````````````````````````` + +- The ``seconds`` attribute of ``PGInterval`` is now always a ``float``, to cope with + fractional seconds. + +- Updated the ``interval`` parsers for ``iso_8601`` and ``sql_standard`` to take + account of fractional seconds. + + +Version 1.27.0, 2022-05-16 +`````````````````````````` + +- It used to be that by default, if pg8000 received an ``interval`` type from the server + and it was too big to fit into a ``datetime.timedelta`` then an exception would be + raised. Now if an interval is too big for ``datetime.timedelta`` a ``PGInterval`` is + returned. + +* pg8000 now supports all the output formats for an ``interval`` (``postgres``, + ``postgres_verbose``, ``iso_8601`` and ``sql_standard``). + + +Version 1.26.1, 2022-04-23 +`````````````````````````` + +- Make sure all tests are run by the GitHub Actions tests on commit. +- Remove support for Python 3.6 +- Remove support for PostgreSQL 9.6 + + +Version 1.26.0, 2022-04-18 +`````````````````````````` + +- When connecting, raise an ``InterfaceError('network error')`` rather than let the + underlying ``struct.error`` float up. + +- Make licence text the same as that used by the OSI. Previously the licence wording + differed slightly from the BSD 3 Clause licence at + https://opensource.org/licenses/BSD-3-Clause. This meant that automated tools didn't + pick it up as being Open Source. The changes are believed to not alter the meaning of the license at all. + + +Version 1.25.0, 2022-04-17 +`````````````````````````` + +- Fix more cases where a ``ResourceWarning`` would be raise because of a socket that had + been left open. + +- We now have a single ``InterfaceError`` with the message 'network error' for all + network errors, with the underlying exception held in the ``cause`` of the exception. + + +Version 1.24.2, 2022-04-15 +`````````````````````````` + +- To prevent a ``ResourceWarning`` close socket if a connection can't be created. + + +Version 1.24.1, 2022-03-02 +`````````````````````````` + +- Return pg +/-infinity dates as ``str``. Previously +/-infinity pg values would cause + an error when returned, but now we return +/-infinity as strings. + + +Version 1.24.0, 2022-02-06 +`````````````````````````` + +- Add SQL escape functions identifier() and literal() to the native API. For use when a + query can't be parameterised and the SQL string has to be created using untrusted + values. + + +Version 1.23.0, 2021-11-13 +`````````````````````````` + +- If a query has no parameters, then the query will no longer be parsed. Although there + are performance benefits for doing this, the main reason is to avoid query rewriting, + which can introduce errors. + + +Version 1.22.1, 2021-11-10 +`````````````````````````` + +- Fix bug in PGInterval type where ``str()`` failed for a millennia value. + + +Version 1.22.0, 2021-10-13 +`````````````````````````` + +- Rather than specifying the oids in the ``Parse`` step of the Postgres protocol, pg8000 + now omits them, and so Postgres will use the oids it determines from the query. This + makes the pg8000 code simpler and also it should also make the nuances of type + matching more straightforward. diff --git a/contrib/python/pg8000/pg8000/__init__.py b/contrib/python/pg8000/pg8000/__init__.py new file mode 100644 index 00000000000..42f13b850c5 --- /dev/null +++ b/contrib/python/pg8000/pg8000/__init__.py @@ -0,0 +1,213 @@ +from pg8000.legacy import ( + BIGINTEGER, + BINARY, + BOOLEAN, + BOOLEAN_ARRAY, + BYTES, + Binary, + CHAR, + CHAR_ARRAY, + Connection, + Cursor, + DATE, + DATETIME, + DECIMAL, + DECIMAL_ARRAY, + DataError, + DatabaseError, + Date, + DateFromTicks, + Error, + FLOAT, + FLOAT_ARRAY, + INET, + INT2VECTOR, + INTEGER, + INTEGER_ARRAY, + INTERVAL, + IntegrityError, + InterfaceError, + InternalError, + JSON, + JSONB, + MACADDR, + NAME, + NAME_ARRAY, + NULLTYPE, + NUMBER, + NotSupportedError, + OID, + OperationalError, + PGInterval, + ProgrammingError, + ROWID, + Range, + STRING, + TEXT, + TEXT_ARRAY, + TIME, + TIMEDELTA, + TIMESTAMP, + TIMESTAMPTZ, + Time, + TimeFromTicks, + Timestamp, + TimestampFromTicks, + UNKNOWN, + UUID_TYPE, + VARCHAR, + VARCHAR_ARRAY, + Warning, + XID, + __version__, + pginterval_in, + pginterval_out, + timedelta_in, +) + +# Copyright (c) 2007-2009, Mathieu Fenniak +# Copyright (c) The Contributors +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + + +def connect( + user, + host="localhost", + database=None, + port=5432, + password=None, + source_address=None, + unix_sock=None, + ssl_context=None, + timeout=None, + tcp_keepalive=True, + application_name=None, + replication=None, +): + return Connection( + user, + host=host, + database=database, + port=port, + password=password, + source_address=source_address, + unix_sock=unix_sock, + ssl_context=ssl_context, + timeout=timeout, + tcp_keepalive=tcp_keepalive, + application_name=application_name, + replication=replication, + ) + + +apilevel = "2.0" +"""The DBAPI level supported, currently "2.0". + +This property is part of the `DBAPI 2.0 specification +<http://www.python.org/dev/peps/pep-0249/>`_. +""" + +threadsafety = 1 +"""Integer constant stating the level of thread safety the DBAPI interface +supports. This DBAPI module supports sharing of the module only. Connections +and cursors my not be shared between threads. This gives pg8000 a threadsafety +value of 1. + +This property is part of the `DBAPI 2.0 specification +<http://www.python.org/dev/peps/pep-0249/>`_. +""" + +paramstyle = "format" + + +__all__ = [ + "BIGINTEGER", + "BINARY", + "BOOLEAN", + "BOOLEAN_ARRAY", + "BYTES", + "Binary", + "CHAR", + "CHAR_ARRAY", + "Connection", + "Cursor", + "DATE", + "DATETIME", + "DECIMAL", + "DECIMAL_ARRAY", + "DataError", + "DatabaseError", + "Date", + "DateFromTicks", + "Error", + "FLOAT", + "FLOAT_ARRAY", + "INET", + "INT2VECTOR", + "INTEGER", + "INTEGER_ARRAY", + "INTERVAL", + "IntegrityError", + "InterfaceError", + "InternalError", + "JSON", + "JSONB", + "MACADDR", + "NAME", + "NAME_ARRAY", + "NULLTYPE", + "NUMBER", + "NotSupportedError", + "OID", + "OperationalError", + "PGInterval", + "ProgrammingError", + "ROWID", + "Range", + "STRING", + "TEXT", + "TEXT_ARRAY", + "TIME", + "TIMEDELTA", + "TIMESTAMP", + "TIMESTAMPTZ", + "Time", + "TimeFromTicks", + "Timestamp", + "TimestampFromTicks", + "UNKNOWN", + "UUID_TYPE", + "VARCHAR", + "VARCHAR_ARRAY", + "Warning", + "XID", + "__version__", + "connect", + "pginterval_in", + "pginterval_out", + "timedelta_in", +] diff --git a/contrib/python/pg8000/pg8000/converters.py b/contrib/python/pg8000/pg8000/converters.py new file mode 100644 index 00000000000..b9d17df5ec2 --- /dev/null +++ b/contrib/python/pg8000/pg8000/converters.py @@ -0,0 +1,771 @@ +from datetime import ( + date as Date, + datetime as Datetime, + time as Time, + timedelta as Timedelta, + timezone as Timezone, +) +from decimal import Decimal +from enum import Enum +from ipaddress import ( + IPv4Address, + IPv4Network, + IPv6Address, + IPv6Network, + ip_address, + ip_network, +) +from json import dumps, loads +from uuid import UUID + +from dateutil.parser import parse + +from pg8000.exceptions import InterfaceError +from pg8000.types import PGInterval, Range + + +ANY_ARRAY = 2277 +BIGINT = 20 +BIGINT_ARRAY = 1016 +BOOLEAN = 16 +BOOLEAN_ARRAY = 1000 +BYTES = 17 +BYTES_ARRAY = 1001 +CHAR = 1042 +CHAR_ARRAY = 1014 +CIDR = 650 +CIDR_ARRAY = 651 +CSTRING = 2275 +CSTRING_ARRAY = 1263 +DATE = 1082 +DATE_ARRAY = 1182 +DATEMULTIRANGE = 4535 +DATERANGE = 3912 +FLOAT = 701 +FLOAT_ARRAY = 1022 +INET = 869 +INET_ARRAY = 1041 +INT2VECTOR = 22 +INT4MULTIRANGE = 4451 +INT4RANGE = 3904 +INT8MULTIRANGE = 4536 +INT8RANGE = 3926 +INTEGER = 23 +INTEGER_ARRAY = 1007 +INTERVAL = 1186 +INTERVAL_ARRAY = 1187 +OID = 26 +JSON = 114 +JSON_ARRAY = 199 +JSONB = 3802 +JSONB_ARRAY = 3807 +MACADDR = 829 +MONEY = 790 +MONEY_ARRAY = 791 +NAME = 19 +NAME_ARRAY = 1003 +NUMERIC = 1700 +NUMERIC_ARRAY = 1231 +NUMRANGE = 3906 +NUMMULTIRANGE = 4532 +NULLTYPE = -1 +OID = 26 +POINT = 600 +REAL = 700 +REAL_ARRAY = 1021 +RECORD = 2249 +SMALLINT = 21 +SMALLINT_ARRAY = 1005 +SMALLINT_VECTOR = 22 +STRING = 1043 +TEXT = 25 +TEXT_ARRAY = 1009 +TIME = 1083 +TIME_ARRAY = 1183 +TIMESTAMP = 1114 +TIMESTAMP_ARRAY = 1115 +TIMESTAMPTZ = 1184 +TIMESTAMPTZ_ARRAY = 1185 +TSMULTIRANGE = 4533 +TSRANGE = 3908 +TSTZMULTIRANGE = 4534 +TSTZRANGE = 3910 +UNKNOWN = 705 +UUID_TYPE = 2950 +UUID_ARRAY = 2951 +VARCHAR = 1043 +VARCHAR_ARRAY = 1015 +XID = 28 + + +MIN_INT2, MAX_INT2 = -(2**15), 2**15 +MIN_INT4, MAX_INT4 = -(2**31), 2**31 +MIN_INT8, MAX_INT8 = -(2**63), 2**63 + + +def bool_in(data): + return data == "t" + + +def bool_out(v): + return "true" if v else "false" + + +def bytes_in(data): + return bytes.fromhex(data[2:]) + + +def bytes_out(v): + return "\\x" + v.hex() + + +def cidr_out(v): + return str(v) + + +def cidr_in(data): + return ip_network(data, False) if "/" in data else ip_address(data) + + +def date_in(data): + if data in ("infinity", "-infinity"): + return data + else: + return Datetime.strptime(data, "%Y-%m-%d").date() + + +def date_out(v): + return v.isoformat() + + +def datetime_out(v): + if v.tzinfo is None: + return v.isoformat() + else: + return v.astimezone(Timezone.utc).isoformat() + + +def enum_out(v): + return str(v.value) + + +def float_out(v): + return str(v) + + +def inet_in(data): + return ip_network(data, False) if "/" in data else ip_address(data) + + +def inet_out(v): + return str(v) + + +def int_in(data): + return int(data) + + +def int_out(v): + return str(v) + + +def interval_in(data): + pg_interval = PGInterval.from_str(data) + try: + return pg_interval.to_timedelta() + except ValueError: + return pg_interval + + +def interval_out(v): + return f"{v.days} days {v.seconds} seconds {v.microseconds} microseconds" + + +def json_in(data): + return loads(data) + + +def json_out(v): + return dumps(v) + + +def null_out(v): + return None + + +def numeric_in(data): + return Decimal(data) + + +def numeric_out(d): + return str(d) + + +def point_in(data): + return tuple(map(float, data[1:-1].split(","))) + + +def pg_interval_in(data): + return PGInterval.from_str(data) + + +def pg_interval_out(v): + return str(v) + + +def range_out(v): + if v.is_empty: + return "empty" + else: + le = v.lower + val_lower = "" if le is None else make_param(PY_TYPES, le) + ue = v.upper + val_upper = "" if ue is None else make_param(PY_TYPES, ue) + return f"{v.bounds[0]}{val_lower},{val_upper}{v.bounds[1]}" + + +def string_in(data): + return data + + +def string_out(v): + return v + + +def time_in(data): + pattern = "%H:%M:%S.%f" if "." in data else "%H:%M:%S" + return Datetime.strptime(data, pattern).time() + + +def time_out(v): + return v.isoformat() + + +def timestamp_in(data): + if data in ("infinity", "-infinity"): + return data + + try: + pattern = "%Y-%m-%d %H:%M:%S.%f" if "." in data else "%Y-%m-%d %H:%M:%S" + return Datetime.strptime(data, pattern) + except ValueError: + return parse(data) + + +def timestamptz_in(data): + if data in ("infinity", "-infinity"): + return data + + try: + patt = "%Y-%m-%d %H:%M:%S.%f%z" if "." in data else "%Y-%m-%d %H:%M:%S%z" + return Datetime.strptime(f"{data}00", patt) + except ValueError: + return parse(data) + + +def unknown_out(v): + return str(v) + + +def vector_in(data): + return [int(v) for v in data.split()] + + +def uuid_out(v): + return str(v) + + +def uuid_in(data): + return UUID(data) + + +class ParserState(Enum): + InString = 1 + InEscape = 2 + InValue = 3 + Out = 4 + + +def _parse_array(data, adapter): + state = ParserState.Out + stack = [[]] + val = [] + for c in data: + if state == ParserState.InValue: + if c in ("}", ","): + value = "".join(val) + stack[-1].append(None if value == "NULL" else adapter(value)) + state = ParserState.Out + else: + val.append(c) + + if state == ParserState.Out: + if c == "{": + a = [] + stack[-1].append(a) + stack.append(a) + elif c == "}": + stack.pop() + elif c == ",": + pass + elif c == '"': + val = [] + state = ParserState.InString + else: + val = [c] + state = ParserState.InValue + + elif state == ParserState.InString: + if c == '"': + stack[-1].append(adapter("".join(val))) + state = ParserState.Out + elif c == "\\": + state = ParserState.InEscape + else: + val.append(c) + elif state == ParserState.InEscape: + val.append(c) + state = ParserState.InString + + return stack[0][0] + + +def _array_in(adapter): + def f(data): + return _parse_array(data, adapter) + + return f + + +bool_array_in = _array_in(bool_in) +bytes_array_in = _array_in(bytes_in) +cidr_array_in = _array_in(cidr_in) +date_array_in = _array_in(date_in) +inet_array_in = _array_in(inet_in) +int_array_in = _array_in(int) +interval_array_in = _array_in(interval_in) +json_array_in = _array_in(json_in) +float_array_in = _array_in(float) +numeric_array_in = _array_in(numeric_in) +string_array_in = _array_in(string_in) +time_array_in = _array_in(time_in) +timestamp_array_in = _array_in(timestamp_in) +timestamptz_array_in = _array_in(timestamptz_in) +uuid_array_in = _array_in(uuid_in) + + +def array_string_escape(v): + cs = [] + for c in v: + if c == "\\": + cs.append("\\") + elif c == '"': + cs.append("\\") + cs.append(c) + val = "".join(cs) + if ( + len(val) == 0 + or val == "NULL" + or any(c.isspace() for c in val) + or any(c in val for c in ("{", "}", ",", "\\")) + ): + val = f'"{val}"' + return val + + +def array_out(ar): + result = [] + for v in ar: + if isinstance(v, list): + val = array_out(v) + + elif isinstance(v, tuple): + val = f'"{composite_out(v)}"' + + elif v is None: + val = "NULL" + + elif isinstance(v, dict): + val = array_string_escape(json_out(v)) + + elif isinstance(v, (bytes, bytearray)): + val = f'"\\{bytes_out(v)}"' + + elif isinstance(v, str): + val = array_string_escape(v) + + else: + val = make_param(PY_TYPES, v) + + result.append(val) + + return f'{{{",".join(result)}}}' + + +def composite_out(ar): + result = [] + for v in ar: + if isinstance(v, list): + val = array_out(v) + + elif isinstance(v, tuple): + val = composite_out(v) + + elif v is None: + val = "" + + elif isinstance(v, dict): + val = array_string_escape(json_out(v)) + + elif isinstance(v, (bytes, bytearray)): + val = f'"\\{bytes_out(v)}"' + + elif isinstance(v, str): + val = array_string_escape(v) + + else: + val = make_param(PY_TYPES, v) + + result.append(val) + + return f'({",".join(result)})' + + +def _range_in(elem_func): + def range_in(data): + if data == "empty": + return Range(is_empty=True) + else: + le, ue = [None if v == "" else elem_func(v) for v in data[1:-1].split(",")] + return Range(le, ue, bounds=f"{data[0]}{data[-1]}") + + return range_in + + +daterange_in = _range_in(date_in) +int4range_in = _range_in(int) +int8range_in = _range_in(int) +numrange_in = _range_in(Decimal) + + +def ts_in(data): + return timestamp_in(data[1:-1]) + + +def tstz_in(data): + return timestamptz_in(data[1:-1]) + + +tsrange_in = _range_in(ts_in) +tstzrange_in = _range_in(tstz_in) + + +def _multirange_in(adapter): + def f(data): + in_range = False + result = [] + val = [] + for c in data: + if in_range: + val.append(c) + if c in "])": + value = "".join(val) + val.clear() + result.append(adapter(value)) + in_range = False + elif c in "[(": + val.append(c) + in_range = True + + return result + + return f + + +datemultirange_in = _multirange_in(daterange_in) +int4multirange_in = _multirange_in(int4range_in) +int8multirange_in = _multirange_in(int8range_in) +nummultirange_in = _multirange_in(numrange_in) +tsmultirange_in = _multirange_in(tsrange_in) +tstzmultirange_in = _multirange_in(tstzrange_in) + + +def record_in(data): + state = ParserState.Out + results = [] + val = [] + for c in data: + if state == ParserState.InValue: + if c in (")", ","): + value = "".join(val) + val.clear() + results.append(None if value == "" else value) + state = ParserState.Out + else: + val.append(c) + + if state == ParserState.Out: + if c in "(),": + pass + elif c == '"': + state = ParserState.InString + else: + val.append(c) + state = ParserState.InValue + + elif state == ParserState.InString: + if c == '"': + results.append("".join(val)) + val.clear() + state = ParserState.Out + elif c == "\\": + state = ParserState.InEscape + else: + val.append(c) + + elif state == ParserState.InEscape: + val.append(c) + state = ParserState.InString + + return tuple(results) + + +PY_PG = { + Date: DATE, + Decimal: NUMERIC, + IPv4Address: INET, + IPv6Address: INET, + IPv4Network: INET, + IPv6Network: INET, + PGInterval: INTERVAL, + Time: TIME, + Timedelta: INTERVAL, + UUID: UUID_TYPE, + bool: BOOLEAN, + bytearray: BYTES, + dict: JSONB, + float: FLOAT, + type(None): NULLTYPE, + bytes: BYTES, + str: TEXT, +} + + +PY_TYPES = { + Date: date_out, # date + Datetime: datetime_out, + Decimal: numeric_out, # numeric + Enum: enum_out, # enum + IPv4Address: inet_out, # inet + IPv6Address: inet_out, # inet + IPv4Network: inet_out, # inet + IPv6Network: inet_out, # inet + PGInterval: interval_out, # interval + Range: range_out, # range types + Time: time_out, # time + Timedelta: interval_out, # interval + UUID: uuid_out, # uuid + bool: bool_out, # bool + bytearray: bytes_out, # bytea + dict: json_out, # jsonb + float: float_out, # float8 + type(None): null_out, # null + bytes: bytes_out, # bytea + str: string_out, # unknown + int: int_out, + list: array_out, + tuple: composite_out, +} + + +PG_TYPES = { + BIGINT: int, # int8 + BIGINT_ARRAY: int_array_in, # int8[] + BOOLEAN: bool_in, # bool + BOOLEAN_ARRAY: bool_array_in, # bool[] + BYTES: bytes_in, # bytea + BYTES_ARRAY: bytes_array_in, # bytea[] + CHAR: string_in, # char + CHAR_ARRAY: string_array_in, # char[] + CIDR_ARRAY: cidr_array_in, # cidr[] + CSTRING: string_in, # cstring + CSTRING_ARRAY: string_array_in, # cstring[] + DATE: date_in, # date + DATE_ARRAY: date_array_in, # date[] + DATEMULTIRANGE: datemultirange_in, # datemultirange + DATERANGE: daterange_in, # daterange + FLOAT: float, # float8 + FLOAT_ARRAY: float_array_in, # float8[] + INET: inet_in, # inet + INET_ARRAY: inet_array_in, # inet[] + INT4MULTIRANGE: int4multirange_in, # int4multirange + INT4RANGE: int4range_in, # int4range + INT8MULTIRANGE: int8multirange_in, # int8multirange + INT8RANGE: int8range_in, # int8range + INTEGER: int, # int4 + INTEGER_ARRAY: int_array_in, # int4[] + JSON: json_in, # json + JSON_ARRAY: json_array_in, # json[] + JSONB: json_in, # jsonb + JSONB_ARRAY: json_array_in, # jsonb[] + MACADDR: string_in, # MACADDR type + MONEY: string_in, # money + MONEY_ARRAY: string_array_in, # money[] + NAME: string_in, # name + NAME_ARRAY: string_array_in, # name[] + NUMERIC: numeric_in, # numeric + NUMERIC_ARRAY: numeric_array_in, # numeric[] + NUMRANGE: numrange_in, # numrange + NUMMULTIRANGE: nummultirange_in, # nummultirange + OID: int, # oid + POINT: point_in, # point + INTERVAL: interval_in, # interval + INTERVAL_ARRAY: interval_array_in, # interval[] + REAL: float, # float4 + REAL_ARRAY: float_array_in, # float4[] + RECORD: record_in, # record + SMALLINT: int, # int2 + SMALLINT_ARRAY: int_array_in, # int2[] + SMALLINT_VECTOR: vector_in, # int2vector + TEXT: string_in, # text + TEXT_ARRAY: string_array_in, # text[] + TIME: time_in, # time + TIME_ARRAY: time_array_in, # time[] + INTERVAL: interval_in, # interval + TIMESTAMP: timestamp_in, # timestamp + TIMESTAMP_ARRAY: timestamp_array_in, # timestamp + TIMESTAMPTZ: timestamptz_in, # timestamptz + TIMESTAMPTZ_ARRAY: timestamptz_array_in, # timestamptz + TSMULTIRANGE: tsmultirange_in, # tsmultirange + TSRANGE: tsrange_in, # tsrange + TSTZMULTIRANGE: tstzmultirange_in, # tstzmultirange + TSTZRANGE: tstzrange_in, # tstzrange + UNKNOWN: string_in, # unknown + UUID_ARRAY: uuid_array_in, # uuid[] + UUID_TYPE: uuid_in, # uuid + VARCHAR: string_in, # varchar + VARCHAR_ARRAY: string_array_in, # varchar[] + XID: int, # xid +} + + +# PostgreSQL encodings: +# https://www.postgresql.org/docs/current/multibyte.html +# +# Python encodings: +# https://docs.python.org/3/library/codecs.html +# +# Commented out encodings don't require a name change between PostgreSQL and +# Python. If the py side is None, then the encoding isn't supported. +PG_PY_ENCODINGS = { + # Not supported: + "mule_internal": None, + "euc_tw": None, + # Name fine as-is: + # "euc_jp", + # "euc_jis_2004", + # "euc_kr", + # "gb18030", + # "gbk", + # "johab", + # "sjis", + # "shift_jis_2004", + # "uhc", + # "utf8", + # Different name: + "euc_cn": "gb2312", + "iso_8859_5": "is8859_5", + "iso_8859_6": "is8859_6", + "iso_8859_7": "is8859_7", + "iso_8859_8": "is8859_8", + "koi8": "koi8_r", + "latin1": "iso8859-1", + "latin2": "iso8859_2", + "latin3": "iso8859_3", + "latin4": "iso8859_4", + "latin5": "iso8859_9", + "latin6": "iso8859_10", + "latin7": "iso8859_13", + "latin8": "iso8859_14", + "latin9": "iso8859_15", + "sql_ascii": "ascii", + "win866": "cp886", + "win874": "cp874", + "win1250": "cp1250", + "win1251": "cp1251", + "win1252": "cp1252", + "win1253": "cp1253", + "win1254": "cp1254", + "win1255": "cp1255", + "win1256": "cp1256", + "win1257": "cp1257", + "win1258": "cp1258", + "unicode": "utf-8", # Needed for Amazon Redshift +} + + +def make_param(py_types, value): + try: + func = py_types[type(value)] + except KeyError: + func = str + for k, v in py_types.items(): + try: + if isinstance(value, k): + func = v + break + except TypeError: + pass + + return func(value) + + +def make_params(py_types, values): + return tuple([make_param(py_types, v) for v in values]) + + +def identifier(sql): + if not isinstance(sql, str): + raise InterfaceError("identifier must be a str") + + if len(sql) == 0: + raise InterfaceError("identifier must be > 0 characters in length") + + quote = not sql[0].isalpha() + + for c in sql[1:]: + if not (c.isalpha() or c.isdecimal() or c in "_$"): + if c == "\u0000": + raise InterfaceError( + "identifier cannot contain the code zero character" + ) + quote = True + break + + if quote: + sql = sql.replace('"', '""') + return f'"{sql}"' + else: + return sql + + +def literal(value): + if value is None: + return "NULL" + elif isinstance(value, bool): + return "TRUE" if value else "FALSE" + elif isinstance(value, (int, float, Decimal)): + return str(value) + elif isinstance(value, (bytes, bytearray)): + return f"X'{value.hex()}'" + elif isinstance(value, Date): + return f"'{date_out(value)}'" + elif isinstance(value, Time): + return f"'{time_out(value)}'" + elif isinstance(value, Datetime): + return f"'{datetime_out(value)}'" + elif isinstance(value, Timedelta): + return f"'{interval_out(value)}'" + else: + val = str(value).replace("'", "''") + return f"'{val}'" diff --git a/contrib/python/pg8000/pg8000/core.py b/contrib/python/pg8000/pg8000/core.py new file mode 100644 index 00000000000..54ef496ddd9 --- /dev/null +++ b/contrib/python/pg8000/pg8000/core.py @@ -0,0 +1,870 @@ +import codecs +import socket +from collections import defaultdict, deque +from hashlib import md5 +from importlib.metadata import version +from io import IOBase, TextIOBase +from itertools import count +from struct import Struct + +import scramp + +from pg8000.converters import ( + PG_PY_ENCODINGS, + PG_TYPES, + PY_TYPES, + make_params, + string_in, +) +from pg8000.exceptions import DatabaseError, Error, InterfaceError + + +ver = version("pg8000") + + +def pack_funcs(fmt): + struc = Struct(f"!{fmt}") + return struc.pack, struc.unpack_from + + +i_pack, i_unpack = pack_funcs("i") +h_pack, h_unpack = pack_funcs("h") +ii_pack, ii_unpack = pack_funcs("ii") +ihihih_pack, ihihih_unpack = pack_funcs("ihihih") +ci_pack, ci_unpack = pack_funcs("ci") +bh_pack, bh_unpack = pack_funcs("bh") +cccc_pack, cccc_unpack = pack_funcs("cccc") + + +# Copyright (c) 2007-2009, Mathieu Fenniak +# Copyright (c) The Contributors +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +__author__ = "Mathieu Fenniak" + + +NULL_BYTE = b"\x00" + + +# Message codes +NOTICE_RESPONSE = b"N" +AUTHENTICATION_REQUEST = b"R" +PARAMETER_STATUS = b"S" +BACKEND_KEY_DATA = b"K" +READY_FOR_QUERY = b"Z" +ROW_DESCRIPTION = b"T" +ERROR_RESPONSE = b"E" +DATA_ROW = b"D" +COMMAND_COMPLETE = b"C" +PARSE_COMPLETE = b"1" +BIND_COMPLETE = b"2" +CLOSE_COMPLETE = b"3" +PORTAL_SUSPENDED = b"s" +NO_DATA = b"n" +PARAMETER_DESCRIPTION = b"t" +NOTIFICATION_RESPONSE = b"A" +COPY_DONE = b"c" +COPY_DATA = b"d" +COPY_IN_RESPONSE = b"G" +COPY_OUT_RESPONSE = b"H" +EMPTY_QUERY_RESPONSE = b"I" + +BIND = b"B" +PARSE = b"P" +QUERY = b"Q" +EXECUTE = b"E" +FLUSH = b"H" +SYNC = b"S" +PASSWORD = b"p" +DESCRIBE = b"D" +TERMINATE = b"X" +CLOSE = b"C" + + +def _create_message(code, data=b""): + return code + i_pack(len(data) + 4) + data + + +FLUSH_MSG = _create_message(FLUSH) +SYNC_MSG = _create_message(SYNC) +TERMINATE_MSG = _create_message(TERMINATE) +COPY_DONE_MSG = _create_message(COPY_DONE) +EXECUTE_MSG = _create_message(EXECUTE, NULL_BYTE + i_pack(0)) + +# DESCRIBE constants +STATEMENT = b"S" +PORTAL = b"P" + +# ErrorResponse codes +RESPONSE_SEVERITY = "S" # always present +RESPONSE_SEVERITY = "V" # always present +RESPONSE_CODE = "C" # always present +RESPONSE_MSG = "M" # always present +RESPONSE_DETAIL = "D" +RESPONSE_HINT = "H" +RESPONSE_POSITION = "P" +RESPONSE__POSITION = "p" +RESPONSE__QUERY = "q" +RESPONSE_WHERE = "W" +RESPONSE_FILE = "F" +RESPONSE_LINE = "L" +RESPONSE_ROUTINE = "R" + +IDLE = b"I" +IN_TRANSACTION = b"T" +IN_FAILED_TRANSACTION = b"E" + + +def _flush(sock): + try: + sock.flush() + except OSError as e: + raise InterfaceError("network error") from e + + +def _read(sock, size): + got = 0 + buff = [] + try: + while got < size: + block = sock.read(size - got) + if block == b"": + raise InterfaceError("network error") + buff.append(block) + got += len(block) + except OSError as e: + raise InterfaceError("network error") from e + + return b"".join(buff) + + +def _write(sock, d): + try: + sock.write(d) + except OSError as e: + raise InterfaceError("network error") from e + + +def _make_socket( + unix_sock, sock, host, port, timeout, source_address, tcp_keepalive, ssl_context +): + if unix_sock is not None: + if sock is not None: + raise InterfaceError("If unix_sock is provided, sock must be None") + + try: + if not hasattr(socket, "AF_UNIX"): + raise InterfaceError( + "attempt to connect to unix socket on unsupported platform" + ) + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.settimeout(timeout) + sock.connect(unix_sock) + if tcp_keepalive: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + except socket.error as e: + if sock is not None: + sock.close() + raise InterfaceError("communication error") from e + + elif sock is not None: + pass + + elif host is not None: + try: + sock = socket.create_connection((host, port), timeout, source_address) + except socket.error as e: + raise InterfaceError( + f"Can't create a connection to host {host} and port {port} " + f"(timeout is {timeout} and source_address is {source_address})." + ) from e + + if tcp_keepalive: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + + else: + raise InterfaceError("one of host, sock or unix_sock must be provided") + + channel_binding = None + if ssl_context is not None: + try: + import ssl + + if ssl_context is True: + ssl_context = ssl.create_default_context() + + request_ssl = getattr(ssl_context, "request_ssl", True) + + if request_ssl: + # Int32(8) - Message length, including self. + # Int32(80877103) - The SSL request code. + sock.sendall(ii_pack(8, 80877103)) + resp = sock.recv(1) + if resp != b"S": + raise InterfaceError("Server refuses SSL") + + sock = ssl_context.wrap_socket(sock, server_hostname=host) + + if request_ssl: + channel_binding = scramp.make_channel_binding( + "tls-server-end-point", sock + ) + + except ImportError: + raise InterfaceError( + "SSL required but ssl module not available in this python " + "installation." + ) + return channel_binding, sock + + +class CoreConnection: + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def __init__( + self, + user, + host="localhost", + database=None, + port=5432, + password=None, + source_address=None, + unix_sock=None, + ssl_context=None, + timeout=None, + tcp_keepalive=True, + application_name=None, + replication=None, + sock=None, + ): + self._client_encoding = "utf8" + self._commands_with_count = ( + b"INSERT", + b"DELETE", + b"UPDATE", + b"MOVE", + b"FETCH", + b"COPY", + b"SELECT", + ) + self.notifications = deque(maxlen=100) + self.notices = deque(maxlen=100) + self.parameter_statuses = deque(maxlen=100) + + if user is None: + raise InterfaceError("The 'user' connection parameter cannot be None") + + init_params = { + "user": user, + "database": database, + "application_name": application_name, + "replication": replication, + } + + for k, v in tuple(init_params.items()): + if isinstance(v, str): + init_params[k] = v.encode("utf8") + elif v is None: + del init_params[k] + elif not isinstance(v, (bytes, bytearray)): + raise InterfaceError(f"The parameter {k} can't be of type {type(v)}.") + + self.user = init_params["user"] + + if isinstance(password, str): + self.password = password.encode("utf8") + else: + self.password = password + + self._xid = None + self._statement_nums = set() + + self._caches = {} + + self.channel_binding, self._usock = _make_socket( + unix_sock, + sock, + host, + port, + timeout, + source_address, + tcp_keepalive, + ssl_context, + ) + + self._sock = self._usock.makefile(mode="rwb") + + self._backend_key_data = None + + self.pg_types = defaultdict(lambda: string_in, PG_TYPES) + self.py_types = dict(PY_TYPES) + + self.message_types = { + NOTICE_RESPONSE: self.handle_NOTICE_RESPONSE, + AUTHENTICATION_REQUEST: self.handle_AUTHENTICATION_REQUEST, + PARAMETER_STATUS: self.handle_PARAMETER_STATUS, + BACKEND_KEY_DATA: self.handle_BACKEND_KEY_DATA, + READY_FOR_QUERY: self.handle_READY_FOR_QUERY, + ROW_DESCRIPTION: self.handle_ROW_DESCRIPTION, + ERROR_RESPONSE: self.handle_ERROR_RESPONSE, + EMPTY_QUERY_RESPONSE: self.handle_EMPTY_QUERY_RESPONSE, + DATA_ROW: self.handle_DATA_ROW, + COMMAND_COMPLETE: self.handle_COMMAND_COMPLETE, + PARSE_COMPLETE: self.handle_PARSE_COMPLETE, + BIND_COMPLETE: self.handle_BIND_COMPLETE, + CLOSE_COMPLETE: self.handle_CLOSE_COMPLETE, + PORTAL_SUSPENDED: self.handle_PORTAL_SUSPENDED, + NO_DATA: self.handle_NO_DATA, + PARAMETER_DESCRIPTION: self.handle_PARAMETER_DESCRIPTION, + NOTIFICATION_RESPONSE: self.handle_NOTIFICATION_RESPONSE, + COPY_DONE: self.handle_COPY_DONE, + COPY_DATA: self.handle_COPY_DATA, + COPY_IN_RESPONSE: self.handle_COPY_IN_RESPONSE, + COPY_OUT_RESPONSE: self.handle_COPY_OUT_RESPONSE, + } + + # Int32 - Message length, including self. + # Int32(196608) - Protocol version number. Version 3.0. + # Any number of key/value pairs, terminated by a zero byte: + # String - A parameter name (user, database, or options) + # String - Parameter value + protocol = 196608 + val = bytearray(i_pack(protocol)) + + for k, v in init_params.items(): + val.extend(k.encode("ascii") + NULL_BYTE + v + NULL_BYTE) + val.append(0) + _write(self._sock, i_pack(len(val) + 4)) + _write(self._sock, val) + _flush(self._sock) + + try: + code = None + context = Context(None) + while code not in (READY_FOR_QUERY, ERROR_RESPONSE): + code, data_len = ci_unpack(_read(self._sock, 5)) + + self.message_types[code](_read(self._sock, data_len - 4), context) + + if context.error is not None: + raise context.error + + except Error as e: + self.close() + raise e + + self._transaction_status = None + + def register_out_adapter(self, typ, out_func): + self.py_types[typ] = out_func + + def register_in_adapter(self, oid, in_func): + self.pg_types[oid] = in_func + + def handle_ERROR_RESPONSE(self, data, context): + msg = { + s[:1].decode("ascii"): s[1:].decode(self._client_encoding, errors="replace") + for s in data.split(NULL_BYTE) + if s != b"" + } + + context.error = DatabaseError(msg) + + def handle_EMPTY_QUERY_RESPONSE(self, data, context): + pass + + def handle_CLOSE_COMPLETE(self, data, context): + pass + + def handle_PARSE_COMPLETE(self, data, context): + # Byte1('1') - Identifier. + # Int32(4) - Message length, including self. + pass + + def handle_BIND_COMPLETE(self, data, context): + pass + + def handle_PORTAL_SUSPENDED(self, data, context): + pass + + def handle_PARAMETER_DESCRIPTION(self, data, context): + """https://www.postgresql.org/docs/current/protocol-message-formats.html""" + + # count = h_unpack(data)[0] + # context.parameter_oids = unpack_from("!" + "i" * count, data, 2) + + def handle_COPY_DONE(self, data, context): + pass + + def handle_COPY_OUT_RESPONSE(self, data, context): + """https://www.postgresql.org/docs/current/protocol-message-formats.html""" + + is_binary, num_cols = bh_unpack(data) + # column_formats = unpack_from('!' + 'h' * num_cols, data, 3) + + if context.stream is None: + raise InterfaceError( + "An output stream is required for the COPY OUT response." + ) + + elif isinstance(context.stream, TextIOBase): + if is_binary: + raise InterfaceError( + "The COPY OUT stream is binary, but the stream parameter is text." + ) + else: + decode = codecs.getdecoder(self._client_encoding) + + def w(data): + context.stream.write(decode(data)[0]) + + context.stream_write = w + + else: + context.stream_write = context.stream.write + + def handle_COPY_DATA(self, data, context): + context.stream_write(data) + + def handle_COPY_IN_RESPONSE(self, data, context): + """https://www.postgresql.org/docs/current/protocol-message-formats.html""" + is_binary, num_cols = bh_unpack(data) + # column_formats = unpack_from('!' + 'h' * num_cols, data, 3) + + if context.stream is None: + raise InterfaceError( + "The 'stream' parameter is required for the COPY IN response. The " + "'stream' parameter can be an I/O stream or an iterable." + ) + + if isinstance(context.stream, IOBase): + if isinstance(context.stream, TextIOBase): + if is_binary: + raise InterfaceError( + "The COPY IN stream is binary, but the stream parameter is a " + "text stream." + ) + + else: + + def ri(bffr): + bffr.clear() + bffr.extend( + context.stream.read(4096).encode(self._client_encoding) + ) + return len(bffr) + + readinto = ri + else: + readinto = context.stream.readinto + + bffr = bytearray(8192) + while True: + bytes_read = readinto(bffr) + if bytes_read == 0: + break + _write(self._sock, COPY_DATA) + _write(self._sock, i_pack(bytes_read + 4)) + _write(self._sock, bffr[:bytes_read]) + _flush(self._sock) + + else: + for k in context.stream: + if isinstance(k, str): + if is_binary: + raise InterfaceError( + "The COPY IN stream is binary, but the stream parameter " + "is an iterable with str type items." + ) + b = k.encode(self._client_encoding) + else: + b = k + + self._send_message(COPY_DATA, b) + _flush(self._sock) + + # Send CopyDone + _write(self._sock, COPY_DONE_MSG) + _write(self._sock, SYNC_MSG) + _flush(self._sock) + + def handle_NOTIFICATION_RESPONSE(self, data, context): + """https://www.postgresql.org/docs/current/protocol-message-formats.html""" + backend_pid = i_unpack(data)[0] + idx = 4 + null_idx = data.find(NULL_BYTE, idx) + channel = data[idx:null_idx].decode("ascii") + payload = data[null_idx + 1 : -1].decode("ascii") + + self.notifications.append((backend_pid, channel, payload)) + + def close(self): + """Closes the database connection. + + This function is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + + if self._usock is None: + raise InterfaceError("connection is closed") + + try: + _write(self._sock, TERMINATE_MSG) + _flush(self._sock) + finally: + try: + self._usock.close() + except socket.error as e: + raise InterfaceError("network error") from e + finally: + self._sock = None + self._usock = None + + def handle_AUTHENTICATION_REQUEST(self, data, context): + """https://www.postgresql.org/docs/current/protocol-message-formats.html""" + + auth_code = i_unpack(data)[0] + if auth_code == 0: + pass + elif auth_code == 3: + if self.password is None: + raise InterfaceError( + "server requesting password authentication, but no password was " + "provided" + ) + self._send_message(PASSWORD, self.password + NULL_BYTE) + _flush(self._sock) + + elif auth_code == 5: + salt = b"".join(cccc_unpack(data, 4)) + if self.password is None: + raise InterfaceError( + "server requesting MD5 password authentication, but no password " + "was provided" + ) + pwd = b"md5" + md5( + md5(self.password + self.user).hexdigest().encode("ascii") + salt + ).hexdigest().encode("ascii") + self._send_message(PASSWORD, pwd + NULL_BYTE) + _flush(self._sock) + + elif auth_code == 10: + # AuthenticationSASL + mechanisms = [m.decode("ascii") for m in data[4:-2].split(NULL_BYTE)] + + self.auth = scramp.ScramClient( + mechanisms, + self.user.decode("utf8"), + self.password.decode("utf8"), + channel_binding=self.channel_binding, + ) + + init = self.auth.get_client_first().encode("utf8") + mech = self.auth.mechanism_name.encode("ascii") + NULL_BYTE + + # SASLInitialResponse + self._send_message(PASSWORD, mech + i_pack(len(init)) + init) + _flush(self._sock) + + elif auth_code == 11: + # AuthenticationSASLContinue + self.auth.set_server_first(data[4:].decode("utf8")) + + # SASLResponse + msg = self.auth.get_client_final().encode("utf8") + self._send_message(PASSWORD, msg) + _flush(self._sock) + + elif auth_code == 12: + # AuthenticationSASLFinal + self.auth.set_server_final(data[4:].decode("utf8")) + + elif auth_code in (2, 4, 6, 7, 8, 9): + raise InterfaceError( + f"Authentication method {auth_code} not supported by pg8000." + ) + else: + raise InterfaceError( + f"Authentication method {auth_code} not recognized by pg8000." + ) + + def handle_READY_FOR_QUERY(self, data, context): + self._transaction_status = data + + def handle_BACKEND_KEY_DATA(self, data, context): + self._backend_key_data = data + + def handle_ROW_DESCRIPTION(self, data, context): + count = h_unpack(data)[0] + idx = 2 + columns = [] + input_funcs = [] + for i in range(count): + name = data[idx : data.find(NULL_BYTE, idx)] + idx += len(name) + 1 + field = dict( + zip( + ( + "table_oid", + "column_attrnum", + "type_oid", + "type_size", + "type_modifier", + "format", + ), + ihihih_unpack(data, idx), + ) + ) + field["name"] = name.decode(self._client_encoding) + idx += 18 + columns.append(field) + input_funcs.append(self.pg_types[field["type_oid"]]) + + context.columns = columns + context.input_funcs = input_funcs + if context.rows is None: + context.rows = [] + + def send_PARSE(self, statement_name_bin, statement, oids=()): + val = bytearray(statement_name_bin) + val.extend(statement.encode(self._client_encoding) + NULL_BYTE) + val.extend(h_pack(len(oids))) + for oid in oids: + val.extend(i_pack(0 if oid == -1 else oid)) + + self._send_message(PARSE, val) + _write(self._sock, FLUSH_MSG) + + def send_DESCRIBE_STATEMENT(self, statement_name_bin): + self._send_message(DESCRIBE, STATEMENT + statement_name_bin) + _write(self._sock, FLUSH_MSG) + + def send_QUERY(self, sql): + self._send_message(QUERY, sql.encode(self._client_encoding) + NULL_BYTE) + + def execute_simple(self, statement): + context = Context(statement) + + self.send_QUERY(statement) + _flush(self._sock) + self.handle_messages(context) + + return context + + def execute_unnamed(self, statement, vals=(), oids=(), stream=None): + context = Context(statement, stream=stream) + + self.send_PARSE(NULL_BYTE, statement, oids) + _write(self._sock, SYNC_MSG) + _flush(self._sock) + self.handle_messages(context) + self.send_DESCRIBE_STATEMENT(NULL_BYTE) + + _write(self._sock, SYNC_MSG) + + try: + _flush(self._sock) + except AttributeError as e: + if self._sock is None: + raise InterfaceError("connection is closed") + else: + raise e + params = make_params(self.py_types, vals) + self.send_BIND(NULL_BYTE, params) + self.handle_messages(context) + self.send_EXECUTE() + + _write(self._sock, SYNC_MSG) + _flush(self._sock) + self.handle_messages(context) + + return context + + def prepare_statement(self, statement, oids=None): + for i in count(): + statement_name = f"pg8000_statement_{i}" + statement_name_bin = statement_name.encode("ascii") + NULL_BYTE + if statement_name_bin not in self._statement_nums: + self._statement_nums.add(statement_name_bin) + break + + self.send_PARSE(statement_name_bin, statement, oids) + self.send_DESCRIBE_STATEMENT(statement_name_bin) + _write(self._sock, SYNC_MSG) + + try: + _flush(self._sock) + except AttributeError as e: + if self._sock is None: + raise InterfaceError("connection is closed") + else: + raise e + + context = Context(statement) + self.handle_messages(context) + + return statement_name_bin, context.columns, context.input_funcs + + def execute_named( + self, statement_name_bin, params, columns, input_funcs, statement + ): + context = Context(columns=columns, input_funcs=input_funcs, statement=statement) + + self.send_BIND(statement_name_bin, params) + self.send_EXECUTE() + _write(self._sock, SYNC_MSG) + _flush(self._sock) + self.handle_messages(context) + return context + + def _send_message(self, code, data): + try: + _write(self._sock, code) + _write(self._sock, i_pack(len(data) + 4)) + _write(self._sock, data) + except ValueError as e: + if str(e) == "write to closed file": + raise InterfaceError("connection is closed") + else: + raise e + except AttributeError: + raise InterfaceError("connection is closed") + + def send_BIND(self, statement_name_bin, params): + """https://www.postgresql.org/docs/current/protocol-message-formats.html""" + + retval = bytearray( + NULL_BYTE + statement_name_bin + h_pack(0) + h_pack(len(params)) + ) + + for value in params: + if value is None: + retval.extend(i_pack(-1)) + else: + val = value.encode(self._client_encoding) + retval.extend(i_pack(len(val))) + retval.extend(val) + retval.extend(h_pack(0)) + + self._send_message(BIND, retval) + _write(self._sock, FLUSH_MSG) + + def send_EXECUTE(self): + """https://www.postgresql.org/docs/current/protocol-message-formats.html""" + _write(self._sock, EXECUTE_MSG) + _write(self._sock, FLUSH_MSG) + + def handle_NO_DATA(self, msg, context): + pass + + def handle_COMMAND_COMPLETE(self, data, context): + if self._transaction_status == IN_FAILED_TRANSACTION and context.error is None: + sql = context.statement.split()[0].rstrip(";").upper() + if sql != "ROLLBACK": + context.error = InterfaceError("in failed transaction block") + + values = data[:-1].split(b" ") + try: + row_count = int(values[-1]) + if context.row_count == -1: + context.row_count = row_count + else: + context.row_count += row_count + except ValueError: + pass + + def handle_DATA_ROW(self, data, context): + idx = 2 + row = [] + for func in context.input_funcs: + vlen = i_unpack(data, idx)[0] + idx += 4 + if vlen == -1: + v = None + else: + v = func(str(data[idx : idx + vlen], encoding=self._client_encoding)) + idx += vlen + row.append(v) + context.rows.append(row) + + def handle_messages(self, context): + code = None + + while code != READY_FOR_QUERY: + code, data_len = ci_unpack(_read(self._sock, 5)) + + self.message_types[code](_read(self._sock, data_len - 4), context) + + if context.error is not None: + raise context.error + + def close_prepared_statement(self, statement_name_bin): + """https://www.postgresql.org/docs/current/protocol-message-formats.html""" + self._send_message(CLOSE, STATEMENT + statement_name_bin) + _write(self._sock, FLUSH_MSG) + _write(self._sock, SYNC_MSG) + _flush(self._sock) + context = Context(None) + self.handle_messages(context) + self._statement_nums.remove(statement_name_bin) + + def handle_NOTICE_RESPONSE(self, data, context): + """https://www.postgresql.org/docs/current/protocol-message-formats.html""" + self.notices.append({s[0:1]: s[1:] for s in data.split(NULL_BYTE)}) + + def handle_PARAMETER_STATUS(self, data, context): + pos = data.find(NULL_BYTE) + key, value = data[:pos], data[pos + 1 : -1] + self.parameter_statuses.append((key, value)) + if key == b"client_encoding": + encoding = value.decode("ascii").lower() + self._client_encoding = PG_PY_ENCODINGS.get(encoding, encoding) + + elif key == b"integer_datetimes": + if value == b"on": + pass + + else: + pass + + elif key == b"server_version": + pass + + +class Context: + def __init__(self, statement, stream=None, columns=None, input_funcs=None): + self.statement = statement + self.rows = None if columns is None else [] + self.row_count = -1 + self.columns = columns + self.stream = stream + self.input_funcs = [] if input_funcs is None else input_funcs + self.error = None diff --git a/contrib/python/pg8000/pg8000/dbapi.py b/contrib/python/pg8000/pg8000/dbapi.py new file mode 100644 index 00000000000..91abb684868 --- /dev/null +++ b/contrib/python/pg8000/pg8000/dbapi.py @@ -0,0 +1,976 @@ +from datetime import ( + date as Date, + datetime as Datetime, + time as Time, +) +from itertools import count, islice +from time import localtime +from warnings import warn + +from pg8000.converters import ( + BIGINT, + BOOLEAN, + BOOLEAN_ARRAY, + BYTES, + CHAR, + CHAR_ARRAY, + DATE, + FLOAT, + FLOAT_ARRAY, + INET, + INT2VECTOR, + INTEGER, + INTEGER_ARRAY, + INTERVAL, + JSON, + JSONB, + MACADDR, + NAME, + NAME_ARRAY, + NULLTYPE, + NUMERIC, + NUMERIC_ARRAY, + OID, + PGInterval, + PY_PG, + STRING, + TEXT, + TEXT_ARRAY, + TIME, + TIMESTAMP, + TIMESTAMPTZ, + UNKNOWN, + UUID_TYPE, + VARCHAR, + VARCHAR_ARRAY, + XID, +) +from pg8000.core import ( + Context, + CoreConnection, + IN_FAILED_TRANSACTION, + IN_TRANSACTION, + ver, +) +from pg8000.exceptions import DatabaseError, Error, InterfaceError +from pg8000.types import Range + + +__version__ = ver + +# Copyright (c) 2007-2009, Mathieu Fenniak +# Copyright (c) The Contributors +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +__author__ = "Mathieu Fenniak" + + +ROWID = OID + +apilevel = "2.0" +"""The DBAPI level supported, currently "2.0". + +This property is part of the `DBAPI 2.0 specification +<http://www.python.org/dev/peps/pep-0249/>`_. +""" + +threadsafety = 1 +"""Integer constant stating the level of thread safety the DBAPI interface +supports. This DBAPI module supports sharing of the module only. Connections +and cursors my not be shared between threads. This gives pg8000 a threadsafety +value of 1. + +This property is part of the `DBAPI 2.0 specification +<http://www.python.org/dev/peps/pep-0249/>`_. +""" + +paramstyle = "format" + + +BINARY = bytes + + +def PgDate(year, month, day): + """Construct an object holding a date value. + + This function is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + + :rtype: :class:`datetime.date` + """ + return Date(year, month, day) + + +def PgTime(hour, minute, second): + """Construct an object holding a time value. + + This function is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + + :rtype: :class:`datetime.time` + """ + return Time(hour, minute, second) + + +def Timestamp(year, month, day, hour, minute, second): + """Construct an object holding a timestamp value. + + This function is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + + :rtype: :class:`datetime.datetime` + """ + return Datetime(year, month, day, hour, minute, second) + + +def DateFromTicks(ticks): + """Construct an object holding a date value from the given ticks value + (number of seconds since the epoch). + + This function is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + + :rtype: :class:`datetime.date` + """ + return Date(*localtime(ticks)[:3]) + + +def TimeFromTicks(ticks): + """Construct an object holding a time value from the given ticks value + (number of seconds since the epoch). + + This function is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + + :rtype: :class:`datetime.time` + """ + return Time(*localtime(ticks)[3:6]) + + +def TimestampFromTicks(ticks): + """Construct an object holding a timestamp value from the given ticks value + (number of seconds since the epoch). + + This function is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + + :rtype: :class:`datetime.datetime` + """ + return Timestamp(*localtime(ticks)[:6]) + + +def Binary(value): + """Construct an object holding binary data. + + This function is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + + """ + return value + + +def connect( + user, + host="localhost", + database=None, + port=5432, + password=None, + source_address=None, + unix_sock=None, + ssl_context=None, + timeout=None, + tcp_keepalive=True, + application_name=None, + replication=None, + sock=None, +): + return Connection( + user, + host=host, + database=database, + port=port, + password=password, + source_address=source_address, + unix_sock=unix_sock, + ssl_context=ssl_context, + timeout=timeout, + tcp_keepalive=tcp_keepalive, + application_name=application_name, + replication=replication, + sock=sock, + ) + + +apilevel = "2.0" +"""The DBAPI level supported, currently "2.0". + +This property is part of the `DBAPI 2.0 specification +<http://www.python.org/dev/peps/pep-0249/>`_. +""" + +threadsafety = 1 +"""Integer constant stating the level of thread safety the DBAPI interface +supports. This DBAPI module supports sharing of the module only. Connections +and cursors my not be shared between threads. This gives pg8000 a threadsafety +value of 1. + +This property is part of the `DBAPI 2.0 specification +<http://www.python.org/dev/peps/pep-0249/>`_. +""" + +paramstyle = "format" + + +def convert_paramstyle(style, query, args): + # I don't see any way to avoid scanning the query string char by char, + # so we might as well take that careful approach and create a + # state-based scanner. We'll use int variables for the state. + OUTSIDE = 0 # outside quoted string + INSIDE_SQ = 1 # inside single-quote string '...' + INSIDE_QI = 2 # inside quoted identifier "..." + INSIDE_ES = 3 # inside escaped single-quote string, E'...' + INSIDE_PN = 4 # inside parameter name eg. :name + INSIDE_CO = 5 # inside inline comment eg. -- + INSIDE_DQ = 6 # inside escaped dollar-quote string, $$...$$ + + in_quote_escape = False + in_param_escape = False + placeholders = [] + output_query = [] + param_idx = map(lambda x: "$" + str(x), count(1)) + state = OUTSIDE + prev_c = None + for i, c in enumerate(query): + next_c = query[i + 1] if i + 1 < len(query) else None + + if state == OUTSIDE: + if c == "'": + output_query.append(c) + if prev_c == "E": + state = INSIDE_ES + else: + state = INSIDE_SQ + elif c == '"': + output_query.append(c) + state = INSIDE_QI + elif c == "-": + output_query.append(c) + if prev_c == "-": + state = INSIDE_CO + elif c == "$": + output_query.append(c) + if prev_c == "$": + state = INSIDE_DQ + elif style == "qmark" and c == "?": + output_query.append(next(param_idx)) + elif ( + style == "numeric" and c == ":" and next_c not in ":=" and prev_c != ":" + ): + # Treat : as beginning of parameter name if and only + # if it's the only : around + # Needed to properly process type conversions + # i.e. sum(x)::float + output_query.append("$") + elif style == "named" and c == ":" and next_c not in ":=" and prev_c != ":": + # Same logic for : as in numeric parameters + state = INSIDE_PN + placeholders.append("") + elif style == "pyformat" and c == "%" and next_c == "(": + state = INSIDE_PN + placeholders.append("") + elif style in ("format", "pyformat") and c == "%": + style = "format" + if in_param_escape: + in_param_escape = False + output_query.append(c) + else: + if next_c == "%": + in_param_escape = True + elif next_c == "s": + state = INSIDE_PN + output_query.append(next(param_idx)) + else: + raise InterfaceError( + "Only %s and %% are supported in the query." + ) + else: + output_query.append(c) + + elif state == INSIDE_SQ: + if c == "'": + if in_quote_escape: + in_quote_escape = False + else: + if next_c == "'": + in_quote_escape = True + else: + state = OUTSIDE + output_query.append(c) + + elif state == INSIDE_QI: + if c == '"': + state = OUTSIDE + output_query.append(c) + + elif state == INSIDE_ES: + if c == "'" and prev_c != "\\": + # check for escaped single-quote + state = OUTSIDE + output_query.append(c) + + elif state == INSIDE_DQ: + if c == "$" and prev_c == "$": + state = OUTSIDE + output_query.append(c) + + elif state == INSIDE_PN: + if style == "named": + placeholders[-1] += c + if next_c is None or (not next_c.isalnum() and next_c != "_"): + state = OUTSIDE + try: + pidx = placeholders.index(placeholders[-1], 0, -1) + output_query.append("$" + str(pidx + 1)) + del placeholders[-1] + except ValueError: + output_query.append("$" + str(len(placeholders))) + elif style == "pyformat": + if prev_c == ")" and c == "s": + state = OUTSIDE + try: + pidx = placeholders.index(placeholders[-1], 0, -1) + output_query.append("$" + str(pidx + 1)) + del placeholders[-1] + except ValueError: + output_query.append("$" + str(len(placeholders))) + elif c in "()": + pass + else: + placeholders[-1] += c + elif style == "format": + state = OUTSIDE + + elif state == INSIDE_CO: + output_query.append(c) + if c == "\n": + state = OUTSIDE + + prev_c = c + + if style in ("numeric", "qmark", "format"): + vals = args + else: + vals = tuple(args[p] for p in placeholders) + + return "".join(output_query), vals + + +class Cursor: + def __init__(self, connection): + self._c = connection + self.arraysize = 1 + + self._context = None + self._row_iter = None + + self._input_oids = () + + @property + def connection(self): + warn("DB-API extension cursor.connection used", stacklevel=3) + return self._c + + @property + def rowcount(self): + context = self._context + if context is None: + return -1 + + return context.row_count + + @property + def description(self): + context = self._context + if context is None: + return None + + row_desc = context.columns + if row_desc is None: + return None + if len(row_desc) == 0: + return None + columns = [] + for col in row_desc: + columns.append((col["name"], col["type_oid"], None, None, None, None, None)) + return columns + + ## + # Executes a database operation. Parameters may be provided as a sequence + # or mapping and will be bound to variables in the operation. + # <p> + # Stability: Part of the DBAPI 2.0 specification. + def execute(self, operation, args=(), stream=None): + """Executes a database operation. Parameters may be provided as a + sequence, or as a mapping, depending upon the value of + :data:`pg8000.paramstyle`. + + This method is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + + :param operation: + The SQL statement to execute. + + :param args: + If :data:`paramstyle` is ``qmark``, ``numeric``, or ``format``, + this argument should be an array of parameters to bind into the + statement. If :data:`paramstyle` is ``named``, the argument should + be a dict mapping of parameters. If the :data:`paramstyle` is + ``pyformat``, the argument value may be either an array or a + mapping. + + :param stream: This is a pg8000 extension for use with the PostgreSQL + `COPY + <http://www.postgresql.org/docs/current/static/sql-copy.html>`_ + command. For a COPY FROM the parameter must be a readable file-like + object, and for COPY TO it must be writable. + + .. versionadded:: 1.9.11 + """ + try: + if not self._c._in_transaction and not self._c.autocommit: + self._c.execute_simple("begin transaction") + + if len(args) == 0 and stream is None: + self._context = self._c.execute_simple(operation) + else: + statement, vals = convert_paramstyle(paramstyle, operation, args) + self._context = self._c.execute_unnamed( + statement, vals=vals, oids=self._input_oids, stream=stream + ) + + if self._context.rows is None: + self._row_iter = None + else: + self._row_iter = iter(self._context.rows) + self._input_oids = () + except AttributeError as e: + if self._c is None: + raise InterfaceError("Cursor closed") + elif self._c._sock is None: + raise InterfaceError("connection is closed") + else: + raise e + + self.input_types = [] + + def executemany(self, operation, param_sets): + """Prepare a database operation, and then execute it against all + parameter sequences or mappings provided. + + This method is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + + :param operation: + The SQL statement to execute + :param parameter_sets: + A sequence of parameters to execute the statement with. The values + in the sequence should be sequences or mappings of parameters, the + same as the args argument of the :meth:`execute` method. + """ + rowcounts = [] + input_oids = self._input_oids + for parameters in param_sets: + self._input_oids = input_oids + self.execute(operation, parameters) + rowcounts.append(self._context.row_count) + + if len(rowcounts) == 0: + self._context = Context(None) + elif -1 in rowcounts: + self._context.row_count = -1 + else: + self._context.row_count = sum(rowcounts) + + def callproc(self, procname, parameters=None): + args = [] if parameters is None else parameters + operation = f"CALL {procname}(" + ", ".join(["%s" for _ in args]) + ")" + + try: + statement, vals = convert_paramstyle("format", operation, args) + + self._context = self._c.execute_unnamed(statement, vals=vals) + + if self._context.rows is None: + self._row_iter = None + else: + self._row_iter = iter(self._context.rows) + + except AttributeError as e: + if self._c is None: + raise InterfaceError("Cursor closed") + elif self._c._sock is None: + raise InterfaceError("connection is closed") + else: + raise e + + def fetchone(self): + """Fetch the next row of a query result set. + + This method is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + + :returns: + A row as a sequence of field values, or ``None`` if no more rows + are available. + """ + try: + return next(self) + except StopIteration: + return None + except TypeError: + raise ProgrammingError("attempting to use unexecuted cursor") + + def __iter__(self): + """A cursor object is iterable to retrieve the rows from a query. + + This is a DBAPI 2.0 extension. + """ + return self + + def __next__(self): + try: + return next(self._row_iter) + except AttributeError: + if self._context is None: + raise ProgrammingError("A query hasn't been issued.") + else: + raise + except StopIteration as e: + if self._context is None: + raise ProgrammingError("A query hasn't been issued.") + elif len(self._context.columns) == 0: + raise ProgrammingError("no result set") + else: + raise e + + def fetchmany(self, num=None): + """Fetches the next set of rows of a query result. + + This method is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + + :param size: + + The number of rows to fetch when called. If not provided, the + :attr:`arraysize` attribute value is used instead. + + :returns: + + A sequence, each entry of which is a sequence of field values + making up a row. If no more rows are available, an empty sequence + will be returned. + """ + try: + return tuple(islice(self, self.arraysize if num is None else num)) + except TypeError: + raise ProgrammingError("attempting to use unexecuted cursor") + + def fetchall(self): + """Fetches all remaining rows of a query result. + + This method is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + + :returns: + + A sequence, each entry of which is a sequence of field values + making up a row. + """ + try: + return tuple(self) + except TypeError: + raise ProgrammingError("attempting to use unexecuted cursor") + + def close(self): + """Closes the cursor. + + This method is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + self._c = None + + def setinputsizes(self, *sizes): + """This method is part of the `DBAPI 2.0 specification""" + oids = [] + for size in sizes: + if isinstance(size, int): + oid = size + else: + try: + oid = PY_PG[size] + except KeyError: + oid = UNKNOWN + oids.append(oid) + + self._input_oids = oids + + def setoutputsize(self, size, column=None): + """This method is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_, however, it is not + implemented by pg8000. + """ + pass + + +class Connection(CoreConnection): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.autocommit = False + + # DBAPI Extension: supply exceptions as attributes on the connection + Warning = property(lambda self: self._getError(Warning)) + Error = property(lambda self: self._getError(Error)) + InterfaceError = property(lambda self: self._getError(InterfaceError)) + DatabaseError = property(lambda self: self._getError(DatabaseError)) + OperationalError = property(lambda self: self._getError(OperationalError)) + IntegrityError = property(lambda self: self._getError(IntegrityError)) + InternalError = property(lambda self: self._getError(InternalError)) + ProgrammingError = property(lambda self: self._getError(ProgrammingError)) + NotSupportedError = property(lambda self: self._getError(NotSupportedError)) + + def _getError(self, error): + warn(f"DB-API extension connection.{error.__name__} used", stacklevel=3) + return error + + @property + def _in_transaction(self): + return self._transaction_status in (IN_TRANSACTION, IN_FAILED_TRANSACTION) + + def cursor(self): + """Creates a :class:`Cursor` object bound to this + connection. + + This function is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + return Cursor(self) + + def commit(self): + """Commits the current database transaction. + + This function is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + self.execute_unnamed("commit") + + def rollback(self): + """Rolls back the current database transaction. + + This function is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + if not self._in_transaction: + return + self.execute_unnamed("rollback") + + def xid(self, format_id, global_transaction_id, branch_qualifier): + """Create a Transaction IDs (only global_transaction_id is used in pg) + format_id and branch_qualifier are not used in postgres + global_transaction_id may be any string identifier supported by + postgres returns a tuple + (format_id, global_transaction_id, branch_qualifier)""" + return (format_id, global_transaction_id, branch_qualifier) + + def tpc_begin(self, xid): + """Begins a TPC transaction with the given transaction ID xid. + + This method should be called outside of a transaction (i.e. nothing may + have executed since the last .commit() or .rollback()). + + Furthermore, it is an error to call .commit() or .rollback() within the + TPC transaction. A ProgrammingError is raised, if the application calls + .commit() or .rollback() during an active TPC transaction. + + This function is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + self._xid = xid + if self.autocommit: + self.execute_unnamed("begin transaction") + + def tpc_prepare(self): + """Performs the first phase of a transaction started with .tpc_begin(). + A ProgrammingError is be raised if this method is called outside of a + TPC transaction. + + After calling .tpc_prepare(), no statements can be executed until + .tpc_commit() or .tpc_rollback() have been called. + + This function is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + self.execute_unnamed("PREPARE TRANSACTION '%s';" % (self._xid[1],)) + + def tpc_commit(self, xid=None): + """When called with no arguments, .tpc_commit() commits a TPC + transaction previously prepared with .tpc_prepare(). + + If .tpc_commit() is called prior to .tpc_prepare(), a single phase + commit is performed. A transaction manager may choose to do this if + only a single resource is participating in the global transaction. + + When called with a transaction ID xid, the database commits the given + transaction. If an invalid transaction ID is provided, a + ProgrammingError will be raised. This form should be called outside of + a transaction, and is intended for use in recovery. + + On return, the TPC transaction is ended. + + This function is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + if xid is None: + xid = self._xid + + if xid is None: + raise ProgrammingError("Cannot tpc_commit() without a TPC transaction!") + + try: + previous_autocommit_mode = self.autocommit + self.autocommit = True + if xid in self.tpc_recover(): + self.execute_unnamed("COMMIT PREPARED '%s';" % (xid[1],)) + else: + # a single-phase commit + self.commit() + finally: + self.autocommit = previous_autocommit_mode + self._xid = None + + def tpc_rollback(self, xid=None): + """When called with no arguments, .tpc_rollback() rolls back a TPC + transaction. It may be called before or after .tpc_prepare(). + + When called with a transaction ID xid, it rolls back the given + transaction. If an invalid transaction ID is provided, a + ProgrammingError is raised. This form should be called outside of a + transaction, and is intended for use in recovery. + + On return, the TPC transaction is ended. + + This function is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + if xid is None: + xid = self._xid + + if xid is None: + raise ProgrammingError( + "Cannot tpc_rollback() without a TPC prepared transaction!" + ) + + try: + previous_autocommit_mode = self.autocommit + self.autocommit = True + if xid in self.tpc_recover(): + # a two-phase rollback + self.execute_unnamed("ROLLBACK PREPARED '%s';" % (xid[1],)) + else: + # a single-phase rollback + self.rollback() + finally: + self.autocommit = previous_autocommit_mode + self._xid = None + + def tpc_recover(self): + """Returns a list of pending transaction IDs suitable for use with + .tpc_commit(xid) or .tpc_rollback(xid). + + This function is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + try: + previous_autocommit_mode = self.autocommit + self.autocommit = True + curs = self.cursor() + curs.execute("select gid FROM pg_prepared_xacts") + return [self.xid(0, row[0], "") for row in curs.fetchall()] + finally: + self.autocommit = previous_autocommit_mode + + +class Warning(Exception): + """Generic exception raised for important database warnings like data + truncations. This exception is not currently used by pg8000. + + This exception is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + + pass + + +class DataError(DatabaseError): + """Generic exception raised for errors that are due to problems with the + processed data. This exception is not currently raised by pg8000. + + This exception is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + + pass + + +class OperationalError(DatabaseError): + """ + Generic exception raised for errors that are related to the database's + operation and not necessarily under the control of the programmer. This + exception is currently never raised by pg8000. + + This exception is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + + pass + + +class IntegrityError(DatabaseError): + """ + Generic exception raised when the relational integrity of the database is + affected. This exception is not currently raised by pg8000. + + This exception is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + + pass + + +class InternalError(DatabaseError): + """Generic exception raised when the database encounters an internal error. + This is currently only raised when unexpected state occurs in the pg8000 + interface itself, and is typically the result of a interface bug. + + This exception is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + + pass + + +class ProgrammingError(DatabaseError): + """Generic exception raised for programming errors. For example, this + exception is raised if more parameter fields are in a query string than + there are available parameters. + + This exception is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + + pass + + +class NotSupportedError(DatabaseError): + """Generic exception raised in case a method or database API was used which + is not supported by the database. + + This exception is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + + pass + + +class ArrayContentNotSupportedError(NotSupportedError): + """ + Raised when attempting to transmit an array where the base type is not + supported for binary data transfer by the interface. + """ + + pass + + +__all__ = [ + "BIGINT", + "BINARY", + "BOOLEAN", + "BOOLEAN_ARRAY", + "BYTES", + "Binary", + "CHAR", + "CHAR_ARRAY", + "Connection", + "Cursor", + "DATE", + "DataError", + "DatabaseError", + "Date", + "DateFromTicks", + "Error", + "FLOAT", + "FLOAT_ARRAY", + "INET", + "INT2VECTOR", + "INTEGER", + "INTEGER_ARRAY", + "INTERVAL", + "IntegrityError", + "InterfaceError", + "InternalError", + "JSON", + "JSONB", + "MACADDR", + "NAME", + "NAME_ARRAY", + "NULLTYPE", + "NUMERIC", + "NUMERIC_ARRAY", + "NotSupportedError", + "OID", + "OperationalError", + "PGInterval", + "ProgrammingError", + "ROWID", + "Range", + "STRING", + "TEXT", + "TEXT_ARRAY", + "TIME", + "TIMESTAMP", + "TIMESTAMPTZ", + "Time", + "TimeFromTicks", + "Timestamp", + "TimestampFromTicks", + "UNKNOWN", + "UUID_TYPE", + "VARCHAR", + "VARCHAR_ARRAY", + "Warning", + "XID", + "connect", +] diff --git a/contrib/python/pg8000/pg8000/exceptions.py b/contrib/python/pg8000/pg8000/exceptions.py new file mode 100644 index 00000000000..47f75cb54a9 --- /dev/null +++ b/contrib/python/pg8000/pg8000/exceptions.py @@ -0,0 +1,32 @@ +class Error(Exception): + """Generic exception that is the base exception of all other error + exceptions. + + This exception is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + + pass + + +class InterfaceError(Error): + """Generic exception raised for errors that are related to the database + interface rather than the database itself. For example, if the interface + attempts to use an SSL connection but the server refuses, an InterfaceError + will be raised. + + This exception is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + + pass + + +class DatabaseError(Error): + """Generic exception raised for errors that are related to the database. + + This exception is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + + pass diff --git a/contrib/python/pg8000/pg8000/legacy.py b/contrib/python/pg8000/pg8000/legacy.py new file mode 100644 index 00000000000..6dddccd88d0 --- /dev/null +++ b/contrib/python/pg8000/pg8000/legacy.py @@ -0,0 +1,822 @@ +from datetime import date as Date, time as Time +from itertools import islice +from warnings import warn + +import pg8000 +from pg8000.converters import ( + BIGINT, + BOOLEAN, + BOOLEAN_ARRAY, + BYTES, + CHAR, + CHAR_ARRAY, + DATE, + FLOAT, + FLOAT_ARRAY, + INET, + INT2VECTOR, + INTEGER, + INTEGER_ARRAY, + INTERVAL, + JSON, + JSONB, + MACADDR, + NAME, + NAME_ARRAY, + NULLTYPE, + NUMERIC, + NUMERIC_ARRAY, + OID, + PGInterval, + PY_PG, + Range, + STRING, + TEXT, + TEXT_ARRAY, + TIME, + TIMESTAMP, + TIMESTAMPTZ, + UNKNOWN, + UUID_TYPE, + VARCHAR, + VARCHAR_ARRAY, + XID, + interval_in as timedelta_in, + make_params, + pg_interval_in as pginterval_in, + pg_interval_out as pginterval_out, +) +from pg8000.core import ( + Context, + CoreConnection, + IN_FAILED_TRANSACTION, + IN_TRANSACTION, + ver, +) +from pg8000.dbapi import ( + BINARY, + Binary, + DataError, + DateFromTicks, + IntegrityError, + InternalError, + NotSupportedError, + OperationalError, + ProgrammingError, + TimeFromTicks, + Timestamp, + TimestampFromTicks, + Warning, + convert_paramstyle, +) +from pg8000.exceptions import DatabaseError, Error, InterfaceError + +__version__ = ver + +# Copyright (c) 2007-2009, Mathieu Fenniak +# Copyright (c) The Contributors +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +__author__ = "Mathieu Fenniak" + + +BIGINTEGER = BIGINT +DATETIME = TIMESTAMP +NUMBER = DECIMAL = NUMERIC +DECIMAL_ARRAY = NUMERIC_ARRAY +ROWID = OID +TIMEDELTA = INTERVAL + + +def connect( + user, + host="localhost", + database=None, + port=5432, + password=None, + source_address=None, + unix_sock=None, + ssl_context=None, + timeout=None, + tcp_keepalive=True, + application_name=None, + replication=None, +): + return Connection( + user, + host=host, + database=database, + port=port, + password=password, + source_address=source_address, + unix_sock=unix_sock, + ssl_context=ssl_context, + timeout=timeout, + tcp_keepalive=tcp_keepalive, + application_name=application_name, + replication=replication, + ) + + +apilevel = "2.0" +"""The DBAPI level supported, currently "2.0". + +This property is part of the `DBAPI 2.0 specification +<http://www.python.org/dev/peps/pep-0249/>`_. +""" + +threadsafety = 1 +"""Integer constant stating the level of thread safety the DBAPI interface +supports. This DBAPI module supports sharing of the module only. Connections +and cursors my not be shared between threads. This gives pg8000 a threadsafety +value of 1. + +This property is part of the `DBAPI 2.0 specification +<http://www.python.org/dev/peps/pep-0249/>`_. +""" + +paramstyle = "format" + + +class Cursor: + def __init__(self, connection, paramstyle=None): + self._c = connection + self.arraysize = 1 + if paramstyle is None: + self.paramstyle = pg8000.paramstyle + else: + self.paramstyle = paramstyle + + self._context = None + self._row_iter = None + + self._input_oids = () + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + @property + def connection(self): + warn("DB-API extension cursor.connection used", stacklevel=3) + return self._c + + @property + def rowcount(self): + context = self._context + if context is None: + return -1 + + return context.row_count + + description = property(lambda self: self._getDescription()) + + def _getDescription(self): + context = self._context + if context is None: + return None + row_desc = context.columns + if row_desc is None: + return None + if len(row_desc) == 0: + return None + columns = [] + for col in row_desc: + columns.append((col["name"], col["type_oid"], None, None, None, None, None)) + return columns + + ## + # Executes a database operation. Parameters may be provided as a sequence + # or mapping and will be bound to variables in the operation. + # <p> + # Stability: Part of the DBAPI 2.0 specification. + def execute(self, operation, args=(), stream=None): + """Executes a database operation. Parameters may be provided as a + sequence, or as a mapping, depending upon the value of + :data:`pg8000.paramstyle`. + + This method is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + + :param operation: + The SQL statement to execute. + + :param args: + If :data:`paramstyle` is ``qmark``, ``numeric``, or ``format``, + this argument should be an array of parameters to bind into the + statement. If :data:`paramstyle` is ``named``, the argument should + be a dict mapping of parameters. If the :data:`paramstyle` is + ``pyformat``, the argument value may be either an array or a + mapping. + + :param stream: This is a pg8000 extension for use with the PostgreSQL + `COPY + <http://www.postgresql.org/docs/current/static/sql-copy.html>`_ + command. For a COPY FROM the parameter must be a readable file-like + object, and for COPY TO it must be writable. + + .. versionadded:: 1.9.11 + """ + try: + if not self._c._in_transaction and not self._c.autocommit: + self._c.execute_simple("begin transaction") + + if len(args) == 0 and stream is None: + self._context = self._c.execute_simple(operation) + else: + statement, vals = convert_paramstyle(self.paramstyle, operation, args) + self._context = self._c.execute_unnamed( + statement, vals=vals, oids=self._input_oids, stream=stream + ) + + rows = [] if self._context.rows is None else self._context.rows + self._row_iter = iter(rows) + + self._input_oids = () + except AttributeError as e: + if self._c is None: + raise InterfaceError("Cursor closed") + elif self._c._sock is None: + raise InterfaceError("connection is closed") + else: + raise e + except DatabaseError as e: + msg = e.args[0] + if isinstance(msg, dict): + response_code = msg["C"] + + if response_code == "28000": + cls = InterfaceError + elif response_code == "23505": + cls = IntegrityError + else: + cls = ProgrammingError + + raise cls(msg) + else: + raise ProgrammingError(msg) + + self.input_types = [] + return self + + def executemany(self, operation, param_sets): + """Prepare a database operation, and then execute it against all + parameter sequences or mappings provided. + + This method is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + + :param operation: + The SQL statement to execute + :param parameter_sets: + A sequence of parameters to execute the statement with. The values + in the sequence should be sequences or mappings of parameters, the + same as the args argument of the :meth:`execute` method. + """ + rowcounts = [] + input_oids = self._input_oids + for parameters in param_sets: + self._input_oids = input_oids + self.execute(operation, parameters) + rowcounts.append(self._context.row_count) + + if len(rowcounts) == 0: + self._context = Context(None) + elif -1 in rowcounts: + self._context.row_count = -1 + else: + self._context.row_count = sum(rowcounts) + + return self + + def fetchone(self): + """Fetch the next row of a query result set. + + This method is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + + :returns: + A row as a sequence of field values, or ``None`` if no more rows + are available. + """ + try: + return next(self) + except StopIteration: + return None + except TypeError: + raise ProgrammingError("attempting to use unexecuted cursor") + except AttributeError: + raise ProgrammingError("attempting to use unexecuted cursor") + + def fetchmany(self, num=None): + """Fetches the next set of rows of a query result. + + This method is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + + :param size: + + The number of rows to fetch when called. If not provided, the + :attr:`arraysize` attribute value is used instead. + + :returns: + + A sequence, each entry of which is a sequence of field values + making up a row. If no more rows are available, an empty sequence + will be returned. + """ + try: + return tuple(islice(self, self.arraysize if num is None else num)) + except TypeError: + raise ProgrammingError("attempting to use unexecuted cursor") + + def fetchall(self): + """Fetches all remaining rows of a query result. + + This method is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + + :returns: + + A sequence, each entry of which is a sequence of field values + making up a row. + """ + try: + return tuple(self) + except TypeError: + raise ProgrammingError("attempting to use unexecuted cursor") + + def close(self): + """Closes the cursor. + + This method is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + self._c = None + + def __iter__(self): + """A cursor object is iterable to retrieve the rows from a query. + + This is a DBAPI 2.0 extension. + """ + return self + + def setinputsizes(self, *sizes): + """This method is part of the `DBAPI 2.0 specification""" + oids = [] + for size in sizes: + if isinstance(size, int): + oid = size + else: + try: + oid = PY_PG[size] + except KeyError: + oid = UNKNOWN + oids.append(oid) + + self._input_oids = oids + + def setoutputsize(self, size, column=None): + """This method is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_, however, it is not + implemented by pg8000. + """ + pass + + def __next__(self): + try: + return next(self._row_iter) + except AttributeError: + if self._context is None: + raise ProgrammingError("A query hasn't been issued.") + else: + raise + except StopIteration as e: + if self._context is None: + raise ProgrammingError("A query hasn't been issued.") + elif len(self._context.columns) == 0: + raise ProgrammingError("no result set") + else: + raise e + + +class Connection(CoreConnection): + # DBAPI Extension: supply exceptions as attributes on the connection + Warning = property(lambda self: self._getError(Warning)) + Error = property(lambda self: self._getError(Error)) + InterfaceError = property(lambda self: self._getError(InterfaceError)) + DatabaseError = property(lambda self: self._getError(DatabaseError)) + OperationalError = property(lambda self: self._getError(OperationalError)) + IntegrityError = property(lambda self: self._getError(IntegrityError)) + InternalError = property(lambda self: self._getError(InternalError)) + ProgrammingError = property(lambda self: self._getError(ProgrammingError)) + NotSupportedError = property(lambda self: self._getError(NotSupportedError)) + + def __init__(self, *args, **kwargs): + try: + super().__init__(*args, **kwargs) + except DatabaseError as e: + msg = e.args[0] + if isinstance(msg, dict): + response_code = msg["C"] + + if response_code == "28000": + cls = InterfaceError + elif response_code == "23505": + cls = IntegrityError + else: + cls = ProgrammingError + + raise cls(msg) + else: + raise ProgrammingError(msg) + + self._run_cursor = Cursor(self, paramstyle="named") + self.autocommit = False + + def _getError(self, error): + warn("DB-API extension connection.%s used" % error.__name__, stacklevel=3) + return error + + def cursor(self): + """Creates a :class:`Cursor` object bound to this + connection. + + This function is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + return Cursor(self) + + @property + def description(self): + return self._run_cursor._getDescription() + + @property + def _in_transaction(self): + return self._transaction_status in (IN_TRANSACTION, IN_FAILED_TRANSACTION) + + def commit(self): + """Commits the current database transaction. + + This function is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + self.execute_unnamed("commit") + + def rollback(self): + """Rolls back the current database transaction. + + This function is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + if not self._in_transaction: + return + self.execute_unnamed("rollback") + + def run(self, sql, stream=None, **params): + self._run_cursor.execute(sql, params, stream=stream) + if self._run_cursor._context.rows is None: + return tuple() + else: + return tuple(self._run_cursor._context.rows) + + def prepare(self, operation): + return PreparedStatement(self, operation) + + def xid(self, format_id, global_transaction_id, branch_qualifier): + """Create a Transaction IDs (only global_transaction_id is used in pg) + format_id and branch_qualifier are not used in postgres + global_transaction_id may be any string identifier supported by + postgres returns a tuple + (format_id, global_transaction_id, branch_qualifier)""" + return (format_id, global_transaction_id, branch_qualifier) + + def tpc_begin(self, xid): + """Begins a TPC transaction with the given transaction ID xid. + + This method should be called outside of a transaction (i.e. nothing may + have executed since the last .commit() or .rollback()). + + Furthermore, it is an error to call .commit() or .rollback() within the + TPC transaction. A ProgrammingError is raised, if the application calls + .commit() or .rollback() during an active TPC transaction. + + This function is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + self._xid = xid + if self.autocommit: + self.execute_unnamed("begin transaction") + + def tpc_prepare(self): + """Performs the first phase of a transaction started with .tpc_begin(). + A ProgrammingError is be raised if this method is called outside of a + TPC transaction. + + After calling .tpc_prepare(), no statements can be executed until + .tpc_commit() or .tpc_rollback() have been called. + + This function is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + q = "PREPARE TRANSACTION '%s';" % (self._xid[1],) + self.execute_unnamed(q) + + def tpc_commit(self, xid=None): + """When called with no arguments, .tpc_commit() commits a TPC + transaction previously prepared with .tpc_prepare(). + + If .tpc_commit() is called prior to .tpc_prepare(), a single phase + commit is performed. A transaction manager may choose to do this if + only a single resource is participating in the global transaction. + + When called with a transaction ID xid, the database commits the given + transaction. If an invalid transaction ID is provided, a + ProgrammingError will be raised. This form should be called outside of + a transaction, and is intended for use in recovery. + + On return, the TPC transaction is ended. + + This function is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + if xid is None: + xid = self._xid + + if xid is None: + raise ProgrammingError("Cannot tpc_commit() without a TPC transaction!") + + try: + previous_autocommit_mode = self.autocommit + self.autocommit = True + if xid in self.tpc_recover(): + self.execute_unnamed("COMMIT PREPARED '%s';" % (xid[1],)) + else: + # a single-phase commit + self.commit() + finally: + self.autocommit = previous_autocommit_mode + self._xid = None + + def tpc_rollback(self, xid=None): + """When called with no arguments, .tpc_rollback() rolls back a TPC + transaction. It may be called before or after .tpc_prepare(). + + When called with a transaction ID xid, it rolls back the given + transaction. If an invalid transaction ID is provided, a + ProgrammingError is raised. This form should be called outside of a + transaction, and is intended for use in recovery. + + On return, the TPC transaction is ended. + + This function is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + if xid is None: + xid = self._xid + + if xid is None: + raise ProgrammingError( + "Cannot tpc_rollback() without a TPC prepared transaction!" + ) + + try: + previous_autocommit_mode = self.autocommit + self.autocommit = True + if xid in self.tpc_recover(): + # a two-phase rollback + self.execute_unnamed("ROLLBACK PREPARED '%s';" % (xid[1],)) + else: + # a single-phase rollback + self.rollback() + finally: + self.autocommit = previous_autocommit_mode + self._xid = None + + def tpc_recover(self): + """Returns a list of pending transaction IDs suitable for use with + .tpc_commit(xid) or .tpc_rollback(xid). + + This function is part of the `DBAPI 2.0 specification + <http://www.python.org/dev/peps/pep-0249/>`_. + """ + try: + previous_autocommit_mode = self.autocommit + self.autocommit = True + curs = self.cursor() + curs.execute("select gid FROM pg_prepared_xacts") + return [self.xid(0, row[0], "") for row in curs] + finally: + self.autocommit = previous_autocommit_mode + + +def to_statement(query): + OUTSIDE = 0 # outside quoted string + INSIDE_SQ = 1 # inside single-quote string '...' + INSIDE_QI = 2 # inside quoted identifier "..." + INSIDE_ES = 3 # inside escaped single-quote string, E'...' + INSIDE_PN = 4 # inside parameter name eg. :name + INSIDE_CO = 5 # inside inline comment eg. -- + + in_quote_escape = False + placeholders = [] + output_query = [] + state = OUTSIDE + prev_c = None + for i, c in enumerate(query): + if i + 1 < len(query): + next_c = query[i + 1] + else: + next_c = None + + if state == OUTSIDE: + if c == "'": + output_query.append(c) + if prev_c == "E": + state = INSIDE_ES + else: + state = INSIDE_SQ + elif c == '"': + output_query.append(c) + state = INSIDE_QI + elif c == "-": + output_query.append(c) + if prev_c == "-": + state = INSIDE_CO + elif c == ":" and next_c not in ":=" and prev_c != ":": + state = INSIDE_PN + placeholders.append("") + else: + output_query.append(c) + + elif state == INSIDE_SQ: + if c == "'": + if in_quote_escape: + in_quote_escape = False + else: + if next_c == "'": + in_quote_escape = True + else: + state = OUTSIDE + output_query.append(c) + + elif state == INSIDE_QI: + if c == '"': + state = OUTSIDE + output_query.append(c) + + elif state == INSIDE_ES: + if c == "'" and prev_c != "\\": + # check for escaped single-quote + state = OUTSIDE + output_query.append(c) + + elif state == INSIDE_PN: + placeholders[-1] += c + if next_c is None or (not next_c.isalnum() and next_c != "_"): + state = OUTSIDE + try: + pidx = placeholders.index(placeholders[-1], 0, -1) + output_query.append("$" + str(pidx + 1)) + del placeholders[-1] + except ValueError: + output_query.append("$" + str(len(placeholders))) + + elif state == INSIDE_CO: + output_query.append(c) + if c == "\n": + state = OUTSIDE + + prev_c = c + + def make_vals(args): + return tuple(args[p] for p in placeholders) + + return "".join(output_query), make_vals + + +class PreparedStatement: + def __init__(self, con, operation): + self.con = con + self.operation = operation + statement, self.make_args = to_statement(operation) + self.name_bin, self.row_desc, self.input_funcs = con.prepare_statement( + statement, () + ) + + def run(self, **vals): + params = make_params(self.con.py_types, self.make_args(vals)) + + try: + if not self.con._in_transaction and not self.con.autocommit: + self.con.execute_unnamed("begin transaction") + self._context = self.con.execute_named( + self.name_bin, params, self.row_desc, self.input_funcs, self.operation + ) + except AttributeError as e: + if self.con is None: + raise InterfaceError("Cursor closed") + elif self.con._sock is None: + raise InterfaceError("connection is closed") + else: + raise e + + return tuple() if self._context.rows is None else tuple(self._context.rows) + + def close(self): + self.con.close_prepared_statement(self.name_bin) + self.con = None + + +__all__ = [ + "BIGINTEGER", + "BINARY", + "BOOLEAN", + "BOOLEAN_ARRAY", + "BYTES", + "Binary", + "CHAR", + "CHAR_ARRAY", + "Connection", + "Cursor", + "DATE", + "DATETIME", + "DECIMAL", + "DECIMAL_ARRAY", + "DataError", + "DatabaseError", + "Date", + "DateFromTicks", + "Error", + "FLOAT", + "FLOAT_ARRAY", + "INET", + "INT2VECTOR", + "INTEGER", + "INTEGER_ARRAY", + "INTERVAL", + "IntegrityError", + "InterfaceError", + "InternalError", + "JSON", + "JSONB", + "MACADDR", + "NAME", + "NAME_ARRAY", + "NULLTYPE", + "NUMBER", + "NotSupportedError", + "OID", + "OperationalError", + "PGInterval", + "ProgrammingError", + "ROWID", + "Range", + "STRING", + "TEXT", + "TEXT_ARRAY", + "TIME", + "TIMEDELTA", + "TIMESTAMP", + "TIMESTAMPTZ", + "Time", + "TimeFromTicks", + "Timestamp", + "TimestampFromTicks", + "UNKNOWN", + "UUID_TYPE", + "VARCHAR", + "VARCHAR_ARRAY", + "Warning", + "XID", + "connect", + "pginterval_in", + "pginterval_out", + "timedelta_in", +] diff --git a/contrib/python/pg8000/pg8000/native.py b/contrib/python/pg8000/pg8000/native.py new file mode 100644 index 00000000000..77e70e9af1b --- /dev/null +++ b/contrib/python/pg8000/pg8000/native.py @@ -0,0 +1,293 @@ +from collections import defaultdict +from enum import Enum, auto + +from pg8000.converters import ( + BIGINT, + BOOLEAN, + BOOLEAN_ARRAY, + BYTES, + CHAR, + CHAR_ARRAY, + DATE, + FLOAT, + FLOAT_ARRAY, + INET, + INT2VECTOR, + INTEGER, + INTEGER_ARRAY, + INTERVAL, + JSON, + JSONB, + MACADDR, + NAME, + NAME_ARRAY, + NULLTYPE, + NUMERIC, + NUMERIC_ARRAY, + OID, + PGInterval, + STRING, + TEXT, + TEXT_ARRAY, + TIME, + TIMESTAMP, + TIMESTAMPTZ, + UNKNOWN, + UUID_TYPE, + VARCHAR, + VARCHAR_ARRAY, + XID, + identifier, + literal, + make_params, +) +from pg8000.core import CoreConnection, ver +from pg8000.exceptions import DatabaseError, Error, InterfaceError +from pg8000.types import Range + +__version__ = ver + +# Copyright (c) 2007-2009, Mathieu Fenniak +# Copyright (c) The Contributors +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + + +class State(Enum): + OUT = auto() # outside quoted string + IN_SQ = auto() # inside single-quote string '...' + IN_QI = auto() # inside quoted identifier "..." + IN_ES = auto() # inside escaped single-quote string, E'...' + IN_PN = auto() # inside parameter name eg. :name + IN_CO = auto() # inside inline comment eg. -- + IN_DQ = auto() # inside dollar-quoted string eg. $$...$$ + + +def to_statement(query): + in_quote_escape = False + placeholders = [] + output_query = [] + state = State.OUT + prev_c = None + for i, c in enumerate(query): + if i + 1 < len(query): + next_c = query[i + 1] + else: + next_c = None + + if state == State.OUT: + if c == "'": + output_query.append(c) + if prev_c == "E": + state = State.IN_ES + else: + state = State.IN_SQ + elif c == '"': + output_query.append(c) + state = State.IN_QI + elif c == "-": + output_query.append(c) + if prev_c == "-": + state = State.IN_CO + elif c == "$": + output_query.append(c) + if prev_c == "$": + state = State.IN_DQ + elif c == ":" and next_c not in ":=" and prev_c != ":": + state = State.IN_PN + placeholders.append("") + else: + output_query.append(c) + + elif state == State.IN_SQ: + if c == "'": + if in_quote_escape: + in_quote_escape = False + elif next_c == "'": + in_quote_escape = True + else: + state = State.OUT + output_query.append(c) + + elif state == State.IN_QI: + if c == '"': + state = State.OUT + output_query.append(c) + + elif state == State.IN_ES: + if c == "'" and prev_c != "\\": + # check for escaped single-quote + state = State.OUT + output_query.append(c) + + elif state == State.IN_PN: + placeholders[-1] += c + if next_c is None or (not next_c.isalnum() and next_c != "_"): + state = State.OUT + try: + pidx = placeholders.index(placeholders[-1], 0, -1) + output_query.append(f"${pidx + 1}") + del placeholders[-1] + except ValueError: + output_query.append(f"${len(placeholders)}") + + elif state == State.IN_CO: + output_query.append(c) + if c == "\n": + state = State.OUT + + elif state == State.IN_DQ: + output_query.append(c) + if c == "$" and prev_c == "$": + state = State.OUT + + prev_c = c + + for reserved in ("types", "stream"): + if reserved in placeholders: + raise InterfaceError( + f"The name '{reserved}' can't be used as a placeholder because it's " + f"used for another purpose." + ) + + def make_vals(args): + vals = [] + for p in placeholders: + try: + vals.append(args[p]) + except KeyError: + raise InterfaceError( + f"There's a placeholder '{p}' in the query, but no matching " + f"keyword argument." + ) + return tuple(vals) + + return "".join(output_query), make_vals + + +class Connection(CoreConnection): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._context = None + + @property + def columns(self): + context = self._context + if context is None: + return None + return context.columns + + @property + def row_count(self): + context = self._context + if context is None: + return None + return context.row_count + + def run(self, sql, stream=None, types=None, **params): + if len(params) == 0 and stream is None: + self._context = self.execute_simple(sql) + else: + statement, make_vals = to_statement(sql) + oids = () if types is None else make_vals(defaultdict(lambda: None, types)) + self._context = self.execute_unnamed( + statement, make_vals(params), oids=oids, stream=stream + ) + return self._context.rows + + def prepare(self, sql): + return PreparedStatement(self, sql) + + +class PreparedStatement: + def __init__(self, con, sql, types=None): + self.con = con + self.statement, self.make_vals = to_statement(sql) + oids = () if types is None else self.make_vals(defaultdict(lambda: None, types)) + self.name_bin, self.cols, self.input_funcs = con.prepare_statement( + self.statement, oids + ) + + @property + def columns(self): + return self._context.columns + + def run(self, stream=None, **params): + params = make_params(self.con.py_types, self.make_vals(params)) + + self._context = self.con.execute_named( + self.name_bin, params, self.cols, self.input_funcs, self.statement + ) + + return self._context.rows + + def close(self): + self.con.close_prepared_statement(self.name_bin) + + +__all__ = [ + "BIGINT", + "BOOLEAN", + "BOOLEAN_ARRAY", + "BYTES", + "CHAR", + "CHAR_ARRAY", + "DATE", + "DatabaseError", + "Error", + "FLOAT", + "FLOAT_ARRAY", + "INET", + "INT2VECTOR", + "INTEGER", + "INTEGER_ARRAY", + "INTERVAL", + "InterfaceError", + "JSON", + "JSONB", + "MACADDR", + "NAME", + "NAME_ARRAY", + "NULLTYPE", + "NUMERIC", + "NUMERIC_ARRAY", + "OID", + "PGInterval", + "Range", + "STRING", + "TEXT", + "TEXT_ARRAY", + "TIME", + "TIMESTAMP", + "TIMESTAMPTZ", + "UNKNOWN", + "UUID_TYPE", + "VARCHAR", + "VARCHAR_ARRAY", + "XID", + "identifier", + "literal", +] diff --git a/contrib/python/pg8000/pg8000/types.py b/contrib/python/pg8000/pg8000/types.py new file mode 100644 index 00000000000..c04fba627a6 --- /dev/null +++ b/contrib/python/pg8000/pg8000/types.py @@ -0,0 +1,305 @@ +from datetime import timedelta as Timedelta + + +class PGInterval: + UNIT_MAP = { + "millennia": "millennia", + "millennium": "millennia", + "centuries": "centuries", + "century": "centuries", + "decades": "decades", + "decade": "decades", + "years": "years", + "year": "years", + "months": "months", + "month": "months", + "mon": "months", + "mons": "months", + "weeks": "weeks", + "week": "weeks", + "days": "days", + "day": "days", + "hours": "hours", + "hour": "hours", + "minutes": "minutes", + "minute": "minutes", + "mins": "minutes", + "secs": "seconds", + "seconds": "seconds", + "second": "seconds", + "microseconds": "microseconds", + "microsecond": "microseconds", + } + + ISO_LOOKUP = { + True: { + "Y": "years", + "M": "months", + "D": "days", + }, + False: { + "H": "hours", + "M": "minutes", + "S": "seconds", + }, + } + + @classmethod + def from_str_iso_8601(cls, interval_str): + # P[n]Y[n]M[n]DT[n]H[n]M[n]S + kwargs = {} + lookup = cls.ISO_LOOKUP[True] + val = [] + + for c in interval_str[1:]: + if c == "T": + lookup = cls.ISO_LOOKUP[False] + elif c.isdigit() or c in ("-", "."): + val.append(c) + else: + val_str = "".join(val) + name = lookup[c] + v = float(val_str) if name == "seconds" else int(val_str) + kwargs[name] = v + val.clear() + + return cls(**kwargs) + + @classmethod + def from_str_postgres(cls, interval_str): + """Parses both the postgres and postgres_verbose formats""" + + t = {} + + curr_val = None + for k in interval_str.split(): + if ":" in k: + hours_str, minutes_str, seconds_str = k.split(":") + hours = int(hours_str) + if hours != 0: + t["hours"] = hours + minutes = int(minutes_str) + if minutes != 0: + t["minutes"] = minutes + + seconds = float(seconds_str) + + if seconds != 0: + t["seconds"] = seconds + + elif k == "@": + continue + + elif k == "ago": + for k, v in tuple(t.items()): + t[k] = -1 * v + + else: + try: + curr_val = int(k) + except ValueError: + t[cls.UNIT_MAP[k]] = curr_val + + return cls(**t) + + @classmethod + def from_str_sql_standard(cls, interval_str): + """YYYY-MM + or + DD HH:MM:SS.F + or + YYYY-MM DD HH:MM:SS.F + """ + month_part = None + day_parts = None + parts = interval_str.split() + + if len(parts) == 1: + month_part = parts[0] + elif len(parts) == 2: + day_parts = parts + else: + month_part = parts[0] + day_parts = parts[1:] + + kwargs = {} + + if month_part is not None: + if month_part.startswith("-"): + sign = -1 + p = month_part[1:] + else: + sign = 1 + p = month_part + + kwargs["years"], kwargs["months"] = [int(v) * sign for v in p.split("-")] + + if day_parts is not None: + kwargs["days"] = int(day_parts[0]) + time_part = day_parts[1] + + if time_part.startswith("-"): + sign = -1 + p = time_part[1:] + else: + sign = 1 + p = time_part + + hours, minutes, seconds = p.split(":") + kwargs["hours"] = int(hours) * sign + kwargs["minutes"] = int(minutes) * sign + kwargs["seconds"] = float(seconds) * sign + + return cls(**kwargs) + + @classmethod + def from_str(cls, interval_str): + if interval_str.startswith("P"): + return cls.from_str_iso_8601(interval_str) + elif interval_str.startswith("@"): + return cls.from_str_postgres(interval_str) + else: + parts = interval_str.split() + if (len(parts) > 1 and parts[1][0].isalpha()) or ( + len(parts) == 1 and ":" in parts[0] + ): + return cls.from_str_postgres(interval_str) + else: + return cls.from_str_sql_standard(interval_str) + + def __init__( + self, + millennia=None, + centuries=None, + decades=None, + years=None, + months=None, + weeks=None, + days=None, + hours=None, + minutes=None, + seconds=None, + microseconds=None, + ): + self.millennia = millennia + self.centuries = centuries + self.decades = decades + self.years = years + self.months = months + self.weeks = weeks + self.days = days + self.hours = hours + self.minutes = minutes + self.seconds = seconds + self.microseconds = microseconds + + def __repr__(self): + return f"<PGInterval {self}>" + + def _value_dict(self): + return { + k: v + for k, v in ( + ("millennia", self.millennia), + ("centuries", self.centuries), + ("decades", self.decades), + ("years", self.years), + ("months", self.months), + ("weeks", self.weeks), + ("days", self.days), + ("hours", self.hours), + ("minutes", self.minutes), + ("seconds", self.seconds), + ("microseconds", self.microseconds), + ) + if v is not None + } + + def __str__(self): + return " ".join(f"{v} {n}" for n, v in self._value_dict().items()) + + def normalize(self): + months = 0 + if self.months is not None: + months += self.months + if self.years is not None: + months += self.years * 12 + + days = 0 + if self.days is not None: + days += self.days + if self.weeks is not None: + days += self.weeks * 7 + + seconds = 0 + if self.hours is not None: + seconds += self.hours * 60 * 60 + if self.minutes is not None: + seconds += self.minutes * 60 + if self.seconds is not None: + seconds += self.seconds + if self.microseconds is not None: + seconds += self.microseconds / 1000000 + + return PGInterval(months=months, days=days, seconds=seconds) + + def __eq__(self, other): + if isinstance(other, PGInterval): + s = self.normalize() + o = other.normalize() + return s.months == o.months and s.days == o.days and s.seconds == o.seconds + else: + return False + + def to_timedelta(self): + pairs = self._value_dict() + overlap = pairs.keys() & { + "weeks", + "months", + "years", + "decades", + "centuries", + "millennia", + } + if len(overlap) > 0: + raise ValueError( + "Can't fit the interval fields {overlap} into a datetime.timedelta." + ) + + return Timedelta(**pairs) + + +class Range: + def __init__( + self, + lower=None, + upper=None, + bounds="[)", + is_empty=False, + ): + self.lower = lower + self.upper = upper + self.bounds = bounds + self.is_empty = is_empty + + def __eq__(self, other): + if isinstance(other, Range): + if self.is_empty or other.is_empty: + return self.is_empty == other.is_empty + else: + return ( + self.lower == other.lower + and self.upper == other.upper + and self.bounds == other.bounds + ) + return False + + def __str__(self): + if self.is_empty: + return "empty" + else: + le, ue = ["" if v is None else v for v in (self.lower, self.upper)] + return f"{self.bounds[0]}{le},{ue}{self.bounds[1]}" + + def __repr__(self): + return f"<Range {self}>" diff --git a/contrib/python/pg8000/ya.make b/contrib/python/pg8000/ya.make new file mode 100644 index 00000000000..741227b3e43 --- /dev/null +++ b/contrib/python/pg8000/ya.make @@ -0,0 +1,34 @@ +# Generated by devtools/yamaker (pypi). + +PY3_LIBRARY() + +VERSION(1.30.2) + +LICENSE(BSD-3-Clause) + +PEERDIR( + contrib/python/python-dateutil + contrib/python/scramp +) + +NO_LINT() + +PY_SRCS( + TOP_LEVEL + pg8000/__init__.py + pg8000/converters.py + pg8000/core.py + pg8000/dbapi.py + pg8000/exceptions.py + pg8000/legacy.py + pg8000/native.py + pg8000/types.py +) + +RESOURCE_FILES( + PREFIX contrib/python/pg8000/ + .dist-info/METADATA + .dist-info/top_level.txt +) + +END() |
