mirror of
https://github.com/psycopg/psycopg2.git
synced 2024-11-26 02:43:43 +03:00
sql.Identifier can wrap a sequence of strings to represent qualified names
Close #732.
This commit is contained in:
parent
695c757dc3
commit
4aa02b7855
2
NEWS
2
NEWS
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
48
lib/sql.py
48
lib/sql.py
|
@ -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):
|
||||||
|
|
|
@ -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'))
|
||||||
|
|
Loading…
Reference in New Issue
Block a user