Skip to content

Commit

Permalink
Revert to name of prepared query as positional arg
Browse files Browse the repository at this point in the history
Unnamed prepared queries are not really useful due to their limited
lifetime. To discourage usage of unnamed statements and have a more
consistent API, we switched back from keyword-only to positional arg.
  • Loading branch information
Cito committed Jan 4, 2019
1 parent 2ee2400 commit f21b867
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 63 deletions.
30 changes: 14 additions & 16 deletions docs/contents/pg/db_wrapper.rst
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ Example::
query_prepared -- execute a prepared statement
----------------------------------------------

.. method:: DB.query_prepared([arg1, [arg2, ...]], [name=...])
.. method:: DB.query_prepared(name, [arg1, [arg2, ...]])

Execute a prepared statement

Expand All @@ -524,9 +524,8 @@ query_prepared -- execute a prepared statement
:raises pg.OperationalError: prepared statement does not exist

This methods works like the :meth:`DB.query` method, except that instead of
passing the SQL command, you pass the name of a prepared statement via the
keyword-only argument *name*. If you don't pass a name, the unnamed
statement will be executed, if you created one before.
passing the SQL command, you pass the name of a prepared statement. If you
pass an empty name, the unnamed statement will be executed.

You must have created the corresponding named or unnamed statement with
the :meth:`DB.prepare` method before, otherwise an :exc:`pg.OperationalError`
Expand All @@ -537,7 +536,7 @@ will be raised.
prepare -- create a prepared statement
--------------------------------------

.. method:: DB.prepare(command, [name])
.. method:: DB.prepare(name, command)

Create a prepared statement

Expand All @@ -550,10 +549,10 @@ prepare -- create a prepared statement

This method creates a prepared statement for the given command with the
given name for later execution with the :meth:`DB.query_prepared` method.
The name can be empty or left out to create an unnamed statement, in which
case any pre-existing unnamed statement is automatically replaced;
otherwise a :exc:`pg.ProgrammingError` is raised if the statement name is
already defined in the current database session.
The name can be empty to create an unnamed statement, in which case any
pre-existing unnamed statement is automatically replaced; otherwise a
:exc:`pg.ProgrammingError` is raised if the statement name is already
defined in the current database session.

The SQL command may optionally contain positional parameters of the form
``$1``, ``$2``, etc instead of literal data. The corresponding values
Expand All @@ -562,21 +561,20 @@ as positional arguments.

Example::

db.prepare("update employees set phone=$2 where ein=$1",
name='update employees')
db.prepare('change phone',
"update employees set phone=$2 where ein=$1",
while True:
ein = input("Employee ID? ")
if not ein:
break
phone = input("Phone? ")
rows = db.query_prepared(ein, phone,
name='update employees).getresult()[0][0]
db.query_prepared('change phone', ein, phone)

.. note::

The DB wrapper sometimes issues parameterized queries behind the scenes
(for instance to find unknown database types) which could replace the
unnamed statement. So we advice to always name prepared statements.
We recommend always using named queries, since unnamed queries have a
limited lifetime and can be automatically replaced or destroyed by
various operations on the database.

.. versionadded:: 5.1

Expand Down
23 changes: 10 additions & 13 deletions pg.py
Original file line number Diff line number Diff line change
Expand Up @@ -1869,18 +1869,15 @@ def query_formatted(self, command,
return self.query(*self.adapter.format_query(
command, parameters, types, inline))

def query_prepared(self, *args, **kwargs):
def query_prepared(self, name, *args):
"""Execute a prepared SQL statement.
This works like the query() method, except that instead of passing
the SQL command, you pass the name of a prepared statement via
the keyword-only argument `name`. If you don't pass a name, the
unnamed statement will be executed, if you created one before.
the SQL command, you pass the name of a prepared statement. If you
pass an empty name, the unnamed statement will be executed.
"""
if not self.db:
raise _int_error('Connection is not valid')
# use kwargs because Python 2 does not support keyword-only arguments
name = kwargs.get('name')
if name is None:
name = ''
if args:
Expand All @@ -1889,18 +1886,18 @@ def query_prepared(self, *args, **kwargs):
self._do_debug('EXECUTE', name)
return self.db.query_prepared(name)

def prepare(self, command, name=None):
def prepare(self, name, command):
"""Create a prepared SQL statement.
This creates a prepared statement for the given command with the
the given name for later execution with the query_prepared() method.
The name can be empty or left out to create an unnamed statement,
in which case any pre-existing unnamed statement is automatically
replaced; otherwise it is an error if the statement name is already
defined in the current database session.
If any parameters are used, they can be referred to in the query as
numbered parameters of the form $1.
The name can be empty to create an unnamed statement, in which case
any pre-existing unnamed statement is automatically replaced;
otherwise it is an error if the statement name is already
defined in the current database session. We recommend always using
named queries, since unnamed queries have a limited lifetime and
can be automatically replaced or destroyed by various operations.
"""
if not self.db:
raise _int_error('Connection is not valid')
Expand Down
73 changes: 39 additions & 34 deletions tests/test_classic_dbwrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -971,73 +971,78 @@ def testQueryFormattedWithoutParams(self):

def testPrepare(self):
p = self.db.prepare
self.assertIsNone(p("select 'hello'", 'my query'))
self.assertIsNone(p("select 'world'", 'my other query'))
self.assertIsNone(p('my query', "select 'hello'"))
self.assertIsNone(p('my other query', "select 'world'"))
self.assertRaises(pg.ProgrammingError,
p, 'my query', "select 'hello, too'")

def testPrepareUnnamed(self):
p = self.db.prepare
self.assertIsNone(p("select null"))
self.assertIsNone(p("select null", None))
self.assertIsNone(p("select null", ''))
self.assertIsNone(p("select null", name=None))
self.assertIsNone(p("select null", name=''))
self.assertIsNone(p('', "select null"))
self.assertIsNone(p(None, "select null"))

def testQueryPreparedWithoutParams(self):
f = self.db.query_prepared
self.assertRaises(pg.OperationalError, f, 'q')
p = self.db.prepare
p("select 17", name='q1')
p("select 42", name='q2')
r = f(name='q1').getresult()[0][0]
p('q1', "select 17")
p('q2', "select 42")
r = f('q1').getresult()[0][0]
self.assertEqual(r, 17)
r = f(name='q2').getresult()[0][0]
r = f('q2').getresult()[0][0]
self.assertEqual(r, 42)

def testQueryPreparedWithParams(self):
p = self.db.prepare
p("select 1 + $1 + $2 + $3", name='sum')
p("select initcap($1) || ', ' || $2 || '!'", name='cat')
p('sum', "select 1 + $1 + $2 + $3")
p('cat', "select initcap($1) || ', ' || $2 || '!'")
f = self.db.query_prepared
r = f(2, 3, 5, name='sum').getresult()[0][0]
r = f('sum', 2, 3, 5).getresult()[0][0]
self.assertEqual(r, 11)
r = f('hello', 'world', name='cat').getresult()[0][0]
r = f('cat', 'hello', 'world').getresult()[0][0]
self.assertEqual(r, 'Hello, world!')

def testQueryPreparedUnnamedWithOutParams(self):
f = self.db.query_prepared
self.assertRaises(pg.OperationalError, f)
self.assertRaises(pg.OperationalError, f, None)
self.assertRaises(pg.OperationalError, f, '')
p = self.db.prepare
# make sure all types are known so that we will not
# generate other anonymous queries in the background
p("select 'no name'::varchar")
r = f().getresult()[0][0]
self.assertEqual(r, 'no name')
r = f(name=None).getresult()[0][0]
self.assertEqual(r, 'no name')
r = f(name='').getresult()[0][0]
self.assertEqual(r, 'no name')
p('', "select 'empty'::varchar")
r = f(None).getresult()[0][0]
self.assertEqual(r, 'empty')
r = f('').getresult()[0][0]
self.assertEqual(r, 'empty')
p(None, "select 'none'::varchar")
r = f(None).getresult()[0][0]
self.assertEqual(r, 'none')
r = f('').getresult()[0][0]
self.assertEqual(r, 'none')

def testQueryPreparedUnnamedWithParams(self):
p = self.db.prepare
p("select 1 + $1 + $2")
p('', "select 1 + $1 + $2")
f = self.db.query_prepared
r = f(2, 3).getresult()[0][0]
r = f('', 2, 3).getresult()[0][0]
self.assertEqual(r, 6)
r = f(2, 3, name=None).getresult()[0][0]
self.assertEqual(r, 6)
r = f(2, 3, name='').getresult()[0][0]
r = f(None, 2, 3).getresult()[0][0]
self.assertEqual(r, 6)
p(None, "select 2 + $1 + $2")
f = self.db.query_prepared
r = f('', 3, 4).getresult()[0][0]
self.assertEqual(r, 9)
r = f(None, 3, 4).getresult()[0][0]
self.assertEqual(r, 9)

def testDescribePrepared(self):
self.db.prepare("select 1 as first, 2 as second", 'count')
self.db.prepare('count', "select 1 as first, 2 as second")
f = self.db.describe_prepared
r = f('count').listfields()
self.assertEqual(r, ('first', 'second'))

def testDescribePreparedUnnamed(self):
self.db.prepare("select null as anon")
self.db.prepare('', "select null as anon")
f = self.db.describe_prepared
r = f().listfields()
self.assertEqual(r, ('anon',))
Expand All @@ -1052,14 +1057,14 @@ def testDeletePrepared(self):
e = pg.OperationalError
self.assertRaises(e, f, 'myquery')
p = self.db.prepare
p("select 1", 'q1')
p("select 2", 'q2')
p('q1', "select 1")
p('q2', "select 2")
f('q1')
f('q2')
self.assertRaises(e, f, 'q1')
self.assertRaises(e, f, 'q2')
p("select 1", 'q1')
p("select 2", 'q2')
p('q1', "select 1")
p('q2', "select 2")
f()
self.assertRaises(e, f, 'q1')
self.assertRaises(e, f, 'q2')
Expand Down

0 comments on commit f21b867

Please sign in to comment.