diff --git a/doc/src/advanced.rst b/doc/src/advanced.rst index d4baf419..f7f4cd77 100644 --- a/doc/src/advanced.rst +++ b/doc/src/advanced.rst @@ -89,14 +89,33 @@ by the `psycopg2.extensions.adapt()` function. The `~cursor.execute()` method adapts its arguments to the `~psycopg2.extensions.ISQLQuote` protocol. Objects that conform to this protocol expose a `!getquoted()` method returning the SQL representation -of the object as a string. +of the object as a string (the method must return `!bytes` in Python 3). +Optionally the conform object may expose a +`~psycopg2.extensions.ISQLQuote.prepare()` method. -The easiest way to adapt an object to an SQL string is to register an adapter -function via the `~psycopg2.extensions.register_adapter()` function. The -adapter function must take the value to be adapted as argument and return a -conform object. A convenient object is the `~psycopg2.extensions.AsIs` -wrapper, whose `!getquoted()` result is simply the `!str()`\ ing -conversion of the wrapped object. +There are two basic ways to have a Python object adapted to SQL: + +- the object itself is conform, or knows how to make itself conform. Such + object must expose a `__conform__()` method that will be called with the + protocol object as argument. The object can check that the protocol is + `!ISQLQuote`, in which case it can return `!self` (if the object also + implements `!getquoted()`) or a suitable wrapper object. This option is + viable if you are the author of the object and if the object is specifically + designed for the database (i.e. having Psycopg as a dependency and polluting + its interface with the required methods doesn't bother you). For a simple + example you can take a look to the source code for the + `psycopg2.extras.Inet` object. + +- If implementing the `!ISQLQuote` interface directly in the object is not an + option, you can use an adaptation function, taking the object to be adapted + as argument and returning a conforming object. The adapter must be + registered via the `~psycopg2.extensions.register_adapter()` function. A + simple example wrapper is the `!psycopg2.extras.UUID_adapter` used by the + `~psycopg2.extras.register_uuid()` function. + +A convenient object to write adapters is the `~psycopg2.extensions.AsIs` +wrapper, whose `!getquoted()` result is simply the `!str()`\ ing conversion of +the wrapped object. .. index:: single: Example; Types adaptation diff --git a/doc/src/extensions.rst b/doc/src/extensions.rst index 8fee890e..87f3dd0c 100644 --- a/doc/src/extensions.rst +++ b/doc/src/extensions.rst @@ -189,8 +189,9 @@ deal with Python objects adaptation: .. method:: getquoted() Subclasses or other conforming objects should return a valid SQL - string representing the wrapped object. The `!ISQLQuote` - implementation does nothing. + string representing the wrapped object. In Python 3 the SQL must be + returned in a `!bytes` object. The `!ISQLQuote` implementation does + nothing. .. method:: prepare(conn) diff --git a/doc/src/faq.rst b/doc/src/faq.rst index c271b625..9172e351 100644 --- a/doc/src/faq.rst +++ b/doc/src/faq.rst @@ -94,6 +94,18 @@ Psycopg converts :sql:`decimal`\/\ :sql:`numeric` database types into Python `!D documentation. If you find `!psycopg2.extensions.DECIMAL` not avalable, use `!psycopg2._psycopg.DECIMAL` instead. +Transferring binary data from PostgreSQL 9.0 doesn't work. + PostgreSQL 9.0 uses by default `the "hex" format`__ to transfer + :sql:`bytea` data: the format can't be parsed by the libpq 8.4 and + earlier. Three options to solve the problem are: + + - set the bytea_output__ parameter to ``escape`` in the server; + - use ``SET bytea_output TO escape`` in the client before reading binary + data; + - upgrade the libpq library on the client to at least 9.0. + + .. __: http://www.postgresql.org/docs/9.0/static/datatype-binary.html + .. __: http://www.postgresql.org/docs/9.0/static/runtime-config-client.html#GUC-BYTEA-OUTPUT Best practices -------------- diff --git a/doc/src/usage.rst b/doc/src/usage.rst index 36bd36be..60a2428c 100644 --- a/doc/src/usage.rst +++ b/doc/src/usage.rst @@ -233,15 +233,37 @@ the SQL string that would be sent to the database. .. index:: pair: Strings; Adaptation single: Unicode; Adaptation + +- String types: `!str`, `!unicode` are converted in SQL string syntax. + `!unicode` objects (`!str` in Python 3) are encoded in the connection + `~connection.encoding` to be sent to the backend: trying to send a character + not supported by the encoding will result in an error. Received data can be + converted either as `!str` or `!unicode`: see :ref:`unicode-handling` for + received, either `!str` or `!unicode` + +.. index:: single: Buffer; Adaptation single: bytea; Adaptation single: Binary string -- String types: `!str`, `!unicode` are converted in SQL string - syntax. `!buffer` is converted in PostgreSQL binary string syntax, - suitable for :sql:`bytea` fields. When reading textual fields, either - `!str` or `!unicode` can be received: see - :ref:`unicode-handling`. +- Binary types: Python types such as `!bytes`, `!bytearray`, `!buffer`, + `!memoryview` are converted in PostgreSQL binary string syntax, suitable for + :sql:`bytea` fields. Received data is returned as `!buffer` (in Python 2) or + `!memoryview` (in Python 3). + + .. warning:: + + PostgreSQL 9 uses by default `a new "hex" format`__ to emit :sql:`bytea` + fields. Unfortunately this format can't be parsed by libpq versions + before 9.0. This means that using a library client with version lesser + than 9.0 to talk with a server 9.0 or later you may have problems + receiving :sql:`bytea` data. To work around this problem you can set the + `bytea_output`__ parameter to ``escape``, either in the server + configuration or in the client session using a query such as ``SET + bytea_output TO escape;`` before trying to receive binary data. + + .. __: http://www.postgresql.org/docs/9.0/static/datatype-binary.html + .. __: http://www.postgresql.org/docs/9.0/static/runtime-config-client.html#GUC-BYTEA-OUTPUT .. index:: single: Adaptation; Date/Time objects @@ -338,8 +360,8 @@ defined on the database connection (the `PostgreSQL encoding`__, available in .. __: http://www.postgresql.org/docs/9.0/static/multibyte.html .. __: http://docs.python.org/library/codecs.html#standard-encodings -When reading data from the database, the strings returned are usually 8 bit -`!str` objects encoded in the database client encoding:: +When reading data from the database, in Python 2 the strings returned are +usually 8 bit `!str` objects encoded in the database client encoding:: >>> print conn.encoding UTF8 @@ -356,9 +378,10 @@ When reading data from the database, the strings returned are usually 8 bit >>> print type(x), repr(x) '\xe0\xe8\xec\xf2\xf9\xa4' -In order to obtain `!unicode` objects instead, it is possible to -register a typecaster so that PostgreSQL textual types are automatically -*decoded* using the current client encoding:: +In Python 3 instead the strings are automatically *decoded* in the connection +`~connection.encoding`, as the `!str` object can represent Unicode characters. +In Python 2 you must register a :ref:`typecaster +` in order to receive `!unicode` objects:: >>> psycopg2.extensions.register_type(psycopg2.extensions.UNICODE, cur) @@ -375,9 +398,9 @@ the connection or globally: see the function .. note:: - If you want to receive uniformly all your database input in Unicode, you - can register the related typecasters globally as soon as Psycopg is - imported:: + In Python 2, if you want to receive uniformly all your database input in + Unicode, you can register the related typecasters globally as soon as + Psycopg is imported:: import psycopg2 import psycopg2.extensions diff --git a/lib/extras.py b/lib/extras.py index 69da5264..a25f4201 100644 --- a/lib/extras.py +++ b/lib/extras.py @@ -835,14 +835,22 @@ class CompositeCaster(object): # Store the transaction status of the connection to revert it after use conn_status = conn.status + # Use the correct schema + if '.' in name: + schema, tname = name.split('.', 1) + else: + tname = name + schema = 'public' + # get the type oid and attributes curs.execute("""\ SELECT t.oid, attname, atttypid FROM pg_type t JOIN pg_namespace ns ON typnamespace = ns.oid JOIN pg_attribute a ON attrelid = typrelid -WHERE typname = %s and nspname = 'public'; -""", (name, )) +WHERE typname = %s and nspname = %s +ORDER BY attnum; +""", (tname, schema)) recs = curs.fetchall() @@ -858,7 +866,7 @@ WHERE typname = %s and nspname = 'public'; type_oid = recs[0][0] type_attrs = [ (r[1], r[2]) for r in recs ] - return CompositeCaster(name, type_oid, type_attrs) + return CompositeCaster(tname, type_oid, type_attrs) def register_composite(name, conn_or_curs, globally=False): """Register a typecaster to convert a composite type into a tuple. diff --git a/tests/testconfig.py b/tests/testconfig.py index f83ded84..667a0fb4 100644 --- a/tests/testconfig.py +++ b/tests/testconfig.py @@ -1,36 +1,36 @@ -# Configure the test suite from the env variables. - -import os - -dbname = os.environ.get('PSYCOPG2_TESTDB', 'psycopg2_test') -dbhost = os.environ.get('PSYCOPG2_TESTDB_HOST', None) -dbport = os.environ.get('PSYCOPG2_TESTDB_PORT', None) -dbuser = os.environ.get('PSYCOPG2_TESTDB_USER', None) -dbpass = os.environ.get('PSYCOPG2_TESTDB_PASSWORD', None) - -# Check if we want to test psycopg's green path. -green = os.environ.get('PSYCOPG2_TEST_GREEN', None) -if green: - if green == '1': - from psycopg2.extras import wait_select as wait_callback - elif green == 'eventlet': - from eventlet.support.psycopg2_patcher import eventlet_wait_callback \ - as wait_callback - else: - raise ValueError("please set 'PSYCOPG2_TEST_GREEN' to a valid value") - - import psycopg2.extensions - psycopg2.extensions.set_wait_callback(wait_callback) - -# Construct a DSN to connect to the test database: -dsn = 'dbname=%s' % dbname -if dbhost is not None: - dsn += ' host=%s' % dbhost -if dbport is not None: - dsn += ' port=%s' % dbport -if dbuser is not None: - dsn += ' user=%s' % dbuser -if dbpass is not None: - dsn += ' password=%s' % dbpass - - +# Configure the test suite from the env variables. + +import os + +dbname = os.environ.get('PSYCOPG2_TESTDB', 'psycopg2_test') +dbhost = os.environ.get('PSYCOPG2_TESTDB_HOST', None) +dbport = os.environ.get('PSYCOPG2_TESTDB_PORT', None) +dbuser = os.environ.get('PSYCOPG2_TESTDB_USER', None) +dbpass = os.environ.get('PSYCOPG2_TESTDB_PASSWORD', None) + +# Check if we want to test psycopg's green path. +green = os.environ.get('PSYCOPG2_TEST_GREEN', None) +if green: + if green == '1': + from psycopg2.extras import wait_select as wait_callback + elif green == 'eventlet': + from eventlet.support.psycopg2_patcher import eventlet_wait_callback \ + as wait_callback + else: + raise ValueError("please set 'PSYCOPG2_TEST_GREEN' to a valid value") + + import psycopg2.extensions + psycopg2.extensions.set_wait_callback(wait_callback) + +# Construct a DSN to connect to the test database: +dsn = 'dbname=%s' % dbname +if dbhost is not None: + dsn += ' host=%s' % dbhost +if dbport is not None: + dsn += ' port=%s' % dbport +if dbuser is not None: + dsn += ' user=%s' % dbuser +if dbpass is not None: + dsn += ' password=%s' % dbpass + + diff --git a/tests/types_extras.py b/tests/types_extras.py index b5f1ce4b..88949298 100755 --- a/tests/types_extras.py +++ b/tests/types_extras.py @@ -525,6 +525,24 @@ class AdaptTypeTestCase(unittest.TestCase): conn1.close() conn2.close() + @skip_if_no_composite + def test_composite_namespace(self): + curs = self.conn.cursor() + curs.execute(""" + select nspname from pg_namespace + where nspname = 'typens'; + """) + if not curs.fetchone(): + curs.execute("create schema typens;") + self.conn.commit() + + self._create_type("typens.typens_ii", + [("a", "integer"), ("b", "integer")]) + t = psycopg2.extras.register_composite( + "typens.typens_ii", self.conn) + curs.execute("select (4,8)::typens.typens_ii") + self.assertEqual(curs.fetchone()[0], (4,8)) + def _create_type(self, name, fields): curs = self.conn.cursor() try: @@ -534,11 +552,16 @@ class AdaptTypeTestCase(unittest.TestCase): curs.execute("create type %s as (%s);" % (name, ", ".join(["%s %s" % p for p in fields]))) + if '.' in name: + schema, name = name.split('.') + else: + schema = 'public' + curs.execute("""\ SELECT t.oid FROM pg_type t JOIN pg_namespace ns ON typnamespace = ns.oid - WHERE typname = %s and nspname = 'public'; - """, (name,)) + WHERE typname = %s and nspname = %s; + """, (name, schema)) oid = curs.fetchone()[0] self.conn.commit() return oid