sql.Identifier can wrap a sequence of strings to represent qualified names

Close #732.
This commit is contained in:
Daniele Varrazzo 2018-07-13 18:17:32 +01:00
parent 695c757dc3
commit 4aa02b7855
4 changed files with 74 additions and 16 deletions

2
NEWS
View File

@ -7,6 +7,8 @@ What's new in psycopg 2.8
New features: New features:
- Added `~psycopg2.extensions.encrypt_password()` function (:ticket:`#576`). - Added `~psycopg2.extensions.encrypt_password()` function (:ticket:`#576`).
- `~psycopg2.sql.Identifier` can represent qualified names in SQL composition
(:ticket:`#732`).
- `!str()` on `~psycopg2.extras.Range` produces a human-readable representation - `!str()` on `~psycopg2.extras.Range` produces a human-readable representation
(:ticket:`#773`). (:ticket:`#773`).
- `~psycopg2.extras.DictCursor` and `~psycopg2.extras.RealDictCursor` rows - `~psycopg2.extras.DictCursor` and `~psycopg2.extras.RealDictCursor` rows

View File

@ -77,16 +77,26 @@ to cursor methods such as `~cursor.execute()`, `~cursor.executemany()`,
.. autoclass:: Identifier .. autoclass:: Identifier
.. autoattribute:: string .. versionchanged:: 2.8
added support for multiple strings.
.. autoattribute:: strings
.. versionadded:: 2.8
previous verions only had a `!string` attribute. The attribute
still exists but is deprecate and will only work if the
`!Identifier` wraps a single string.
.. autoclass:: Literal .. autoclass:: Literal
.. autoattribute:: wrapped .. autoattribute:: wrapped
.. autoclass:: Placeholder .. autoclass:: Placeholder
.. autoattribute:: name .. autoattribute:: name
.. autoclass:: Composed .. autoclass:: Composed
.. autoattribute:: seq .. autoattribute:: seq

View File

@ -290,7 +290,7 @@ class SQL(Composable):
class Identifier(Composable): class Identifier(Composable):
""" """
A `Composable` representing an SQL identifer. A `Composable` representing an SQL identifer or a dot-separated sequence.
Identifiers usually represent names of database objects, such as tables or Identifiers usually represent names of database objects, such as tables or
fields. PostgreSQL identifiers follow `different rules`__ than SQL string fields. PostgreSQL identifiers follow `different rules`__ than SQL string
@ -307,20 +307,50 @@ class Identifier(Composable):
>>> print(sql.SQL(', ').join([t1, t2, t3]).as_string(conn)) >>> print(sql.SQL(', ').join([t1, t2, t3]).as_string(conn))
"foo", "ba'r", "ba""z" "foo", "ba'r", "ba""z"
""" Multiple strings can be passed to the object to represent a qualified name,
def __init__(self, string): i.e. a dot-separated sequence of identifiers.
if not isinstance(string, string_types):
raise TypeError("SQL identifiers must be strings")
super(Identifier, self).__init__(string) Example::
>>> query = sql.SQL("select {} from {}").format(
... sql.Identifier("table", "field"),
... sql.Identifier("schema", "table"))
>>> print(query.as_string(conn))
select "table"."field" from "schema"."table"
"""
def __init__(self, *strings):
if not strings:
raise TypeError("Identifier cannot be empty")
for s in strings:
if not isinstance(s, string_types):
raise TypeError("SQL identifier parts must be strings")
super(Identifier, self).__init__(strings)
@property
def strings(self):
"""A tuple with the strings wrapped by the `Identifier`."""
return self._wrapped
@property @property
def string(self): def string(self):
"""The string wrapped by the `Identifier`.""" """The string wrapped by the `Identifier`.
return self._wrapped """
if len(self._wrapped) == 1:
return self._wrapped[0]
else:
raise AttributeError(
"the Identifier wraps more than one than one string")
def __repr__(self):
return "%s(%s)" % (
self.__class__.__name__,
', '.join(map(repr, self._wrapped)))
def as_string(self, context): def as_string(self, context):
return ext.quote_ident(self._wrapped, context) return '.'.join(ext.quote_ident(s, context) for s in self._wrapped)
class Literal(Composable): class Literal(Composable):

View File

@ -24,9 +24,8 @@
import datetime as dt import datetime as dt
import unittest import unittest
from .testutils import (ConnectingTestCase, from .testutils import (
skip_before_postgres, skip_before_python, skip_copy_if_green, ConnectingTestCase, skip_before_postgres, skip_copy_if_green, StringIO)
StringIO)
import psycopg2 import psycopg2
from psycopg2 import sql from psycopg2 import sql
@ -181,26 +180,43 @@ class IdentifierTests(ConnectingTestCase):
def test_init(self): def test_init(self):
self.assert_(isinstance(sql.Identifier('foo'), sql.Identifier)) self.assert_(isinstance(sql.Identifier('foo'), sql.Identifier))
self.assert_(isinstance(sql.Identifier(u'foo'), sql.Identifier)) self.assert_(isinstance(sql.Identifier(u'foo'), sql.Identifier))
self.assert_(isinstance(sql.Identifier('foo', 'bar', 'baz'), sql.Identifier))
self.assertRaises(TypeError, sql.Identifier)
self.assertRaises(TypeError, sql.Identifier, 10) self.assertRaises(TypeError, sql.Identifier, 10)
self.assertRaises(TypeError, sql.Identifier, dt.date(2016, 12, 31)) self.assertRaises(TypeError, sql.Identifier, dt.date(2016, 12, 31))
def test_string(self): def test_strings(self):
self.assertEqual(sql.Identifier('foo').strings, ('foo',))
self.assertEqual(sql.Identifier('foo', 'bar').strings, ('foo', 'bar'))
# Legacy method
self.assertEqual(sql.Identifier('foo').string, 'foo') self.assertEqual(sql.Identifier('foo').string, 'foo')
self.assertRaises(AttributeError,
getattr, sql.Identifier('foo', 'bar'), 'string')
def test_repr(self): def test_repr(self):
obj = sql.Identifier("fo'o") obj = sql.Identifier("fo'o")
self.assertEqual(repr(obj), 'Identifier("fo\'o")') self.assertEqual(repr(obj), 'Identifier("fo\'o")')
self.assertEqual(repr(obj), str(obj)) self.assertEqual(repr(obj), str(obj))
obj = sql.Identifier("fo'o", 'ba"r')
self.assertEqual(repr(obj), 'Identifier("fo\'o", \'ba"r\')')
self.assertEqual(repr(obj), str(obj))
def test_eq(self): def test_eq(self):
self.assert_(sql.Identifier('foo') == sql.Identifier('foo')) self.assert_(sql.Identifier('foo') == sql.Identifier('foo'))
self.assert_(sql.Identifier('foo', 'bar') == sql.Identifier('foo', 'bar'))
self.assert_(sql.Identifier('foo') != sql.Identifier('bar')) self.assert_(sql.Identifier('foo') != sql.Identifier('bar'))
self.assert_(sql.Identifier('foo') != 'foo') self.assert_(sql.Identifier('foo') != 'foo')
self.assert_(sql.Identifier('foo') != sql.SQL('foo')) self.assert_(sql.Identifier('foo') != sql.SQL('foo'))
def test_as_str(self): def test_as_str(self):
self.assertEqual(sql.Identifier('foo').as_string(self.conn), '"foo"') self.assertEqual(
self.assertEqual(sql.Identifier("fo'o").as_string(self.conn), '"fo\'o"') sql.Identifier('foo').as_string(self.conn), '"foo"')
self.assertEqual(
sql.Identifier('foo', 'bar').as_string(self.conn), '"foo"."bar"')
self.assertEqual(
sql.Identifier("fo'o", 'ba"r').as_string(self.conn), '"fo\'o"."ba""r"')
def test_join(self): def test_join(self):
self.assert_(not hasattr(sql.Identifier('foo'), 'join')) self.assert_(not hasattr(sql.Identifier('foo'), 'join'))