summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>2018-07-13 18:17:32 +0100
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>2018-10-04 12:46:10 +0100
commit4aa02b7855994e60224cb435f893f6ee8760d3d8 (patch)
tree115da191fe7491ff8db6c1eb201f3b1f75a49f13
parent695c757dc3ea352e9f92e0ebc8d9d88d1a1ce97d (diff)
downloadpsycopg2-4aa02b7855994e60224cb435f893f6ee8760d3d8.tar.gz
sql.Identifier can wrap a sequence of strings to represent qualified namesidentifier-sequence
Close #732.
-rw-r--r--NEWS2
-rw-r--r--doc/src/sql.rst12
-rw-r--r--lib/sql.py46
-rwxr-xr-xtests/test_sql.py28
4 files changed, 73 insertions, 15 deletions
diff --git a/NEWS b/NEWS
index 206265a..eabb839 100644
--- a/NEWS
+++ b/NEWS
@@ -7,6 +7,8 @@ What's new in psycopg 2.8
New features:
- 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
(:ticket:`#773`).
- `~psycopg2.extras.DictCursor` and `~psycopg2.extras.RealDictCursor` rows
diff --git a/doc/src/sql.rst b/doc/src/sql.rst
index fe807c6..9cd18e7 100644
--- a/doc/src/sql.rst
+++ b/doc/src/sql.rst
@@ -77,16 +77,26 @@ to cursor methods such as `~cursor.execute()`, `~cursor.executemany()`,
.. 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
.. autoattribute:: wrapped
+
.. autoclass:: Placeholder
.. autoattribute:: name
+
.. autoclass:: Composed
.. autoattribute:: seq
diff --git a/lib/sql.py b/lib/sql.py
index 7ba9295..241b827 100644
--- a/lib/sql.py
+++ b/lib/sql.py
@@ -290,7 +290,7 @@ class SQL(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
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))
"foo", "ba'r", "ba""z"
+ Multiple strings can be passed to the object to represent a qualified name,
+ i.e. a dot-separated sequence of identifiers.
+
+ 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, string):
- if not isinstance(string, string_types):
- raise TypeError("SQL identifiers must be strings")
+ 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__(string)
+ super(Identifier, self).__init__(strings)
@property
- def string(self):
- """The string wrapped by the `Identifier`."""
+ def strings(self):
+ """A tuple with the strings wrapped by the `Identifier`."""
return self._wrapped
+ @property
+ def string(self):
+ """The string wrapped by the `Identifier`.
+ """
+ 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):
- return ext.quote_ident(self._wrapped, context)
+ return '.'.join(ext.quote_ident(s, context) for s in self._wrapped)
class Literal(Composable):
diff --git a/tests/test_sql.py b/tests/test_sql.py
index 81b22a4..cc9bba2 100755
--- a/tests/test_sql.py
+++ b/tests/test_sql.py
@@ -24,9 +24,8 @@
import datetime as dt
import unittest
-from .testutils import (ConnectingTestCase,
- skip_before_postgres, skip_before_python, skip_copy_if_green,
- StringIO)
+from .testutils import (
+ ConnectingTestCase, skip_before_postgres, skip_copy_if_green, StringIO)
import psycopg2
from psycopg2 import sql
@@ -181,26 +180,43 @@ class IdentifierTests(ConnectingTestCase):
def test_init(self):
self.assert_(isinstance(sql.Identifier('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, 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.assertRaises(AttributeError,
+ getattr, sql.Identifier('foo', 'bar'), 'string')
def test_repr(self):
obj = sql.Identifier("fo'o")
self.assertEqual(repr(obj), 'Identifier("fo\'o")')
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):
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') != 'foo')
self.assert_(sql.Identifier('foo') != sql.SQL('foo'))
def test_as_str(self):
- self.assertEqual(sql.Identifier('foo').as_string(self.conn), '"foo"')
- self.assertEqual(sql.Identifier("fo'o").as_string(self.conn), '"fo\'o"')
+ self.assertEqual(
+ 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):
self.assert_(not hasattr(sql.Identifier('foo'), 'join'))