summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorJacob Kaplan-Moss <jacob@jacobian.org>2011-04-20 20:42:07 +0000
committerJacob Kaplan-Moss <jacob@jacobian.org>2011-04-20 20:42:07 +0000
commit8f0f73c7b8b110489a1a127cc47e3cabb0eea646 (patch)
tree7a1376e9031d6d56037f26be29e2e60310c1941f /django
parent99c179442782ea83c52452296d5769950f23a75a (diff)
Fixed #2705: added a `select_for_update()` clause to querysets.
A number of people worked on this patch over the years -- Hawkeye, Colin Grady, KBS, sakyamuni, anih, jdemoor, and Issak Kelly. Thanks to them all, and apologies if I missed anyone. Special thanks to Dan Fairs for picking it up again at the end and seeing this through to commit. git-svn-id: http://code.djangoproject.com/svn/django/trunk@16058 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'django')
-rw-r--r--django/db/backends/__init__.py11
-rw-r--r--django/db/backends/mysql/base.py2
-rw-r--r--django/db/backends/oracle/base.py2
-rw-r--r--django/db/backends/postgresql_psycopg2/base.py3
-rw-r--r--django/db/models/manager.py3
-rw-r--r--django/db/models/query.py13
-rw-r--r--django/db/models/sql/compiler.py15
-rw-r--r--django/db/models/sql/query.py5
8 files changed, 54 insertions, 0 deletions
diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py
index 4b845d5135..b64fb01b03 100644
--- a/django/db/backends/__init__.py
+++ b/django/db/backends/__init__.py
@@ -279,6 +279,8 @@ class BaseDatabaseFeatures(object):
# integer primary keys.
related_fields_match_type = False
allow_sliced_subqueries = True
+ has_select_for_update = False
+ has_select_for_update_nowait = False
# Does the default test database allow multiple connections?
# Usually an indication that the test database is in-memory
@@ -476,6 +478,15 @@ class BaseDatabaseOperations(object):
"""
return []
+ def for_update_sql(self, nowait=False):
+ """
+ Returns the FOR UPDATE SQL clause to lock rows for an update operation.
+ """
+ if nowait:
+ return 'FOR UPDATE NOWAIT'
+ else:
+ return 'FOR UPDATE'
+
def fulltext_search_sql(self, field_name):
"""
Returns the SQL WHERE clause to use in order to perform a full-text
diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py
index 630b9805c3..16067c6807 100644
--- a/django/db/backends/mysql/base.py
+++ b/django/db/backends/mysql/base.py
@@ -124,6 +124,8 @@ class DatabaseFeatures(BaseDatabaseFeatures):
allows_group_by_pk = True
related_fields_match_type = True
allow_sliced_subqueries = False
+ has_select_for_update = True
+ has_select_for_update_nowait = False
supports_forward_references = False
supports_long_model_names = False
supports_microsecond_precision = False
diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py
index dc70a1018c..bcb906762b 100644
--- a/django/db/backends/oracle/base.py
+++ b/django/db/backends/oracle/base.py
@@ -70,6 +70,8 @@ class DatabaseFeatures(BaseDatabaseFeatures):
needs_datetime_string_cast = False
interprets_empty_strings_as_nulls = True
uses_savepoints = True
+ has_select_for_update = True
+ has_select_for_update_nowait = True
can_return_id_from_insert = True
allow_sliced_subqueries = False
supports_subqueries_in_group_by = False
diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py
index e0990a1089..67e28774fe 100644
--- a/django/db/backends/postgresql_psycopg2/base.py
+++ b/django/db/backends/postgresql_psycopg2/base.py
@@ -70,6 +70,9 @@ class DatabaseFeatures(BaseDatabaseFeatures):
requires_rollback_on_dirty_transaction = True
has_real_datatype = True
can_defer_constraint_checks = True
+ has_select_for_update = True
+ has_select_for_update_nowait = True
+
class DatabaseWrapper(BaseDatabaseWrapper):
vendor = 'postgresql'
diff --git a/django/db/models/manager.py b/django/db/models/manager.py
index 9528d66812..4fa4c4a52d 100644
--- a/django/db/models/manager.py
+++ b/django/db/models/manager.py
@@ -164,6 +164,9 @@ class Manager(object):
def order_by(self, *args, **kwargs):
return self.get_query_set().order_by(*args, **kwargs)
+ def select_for_update(self, *args, **kwargs):
+ return self.get_query_set().select_for_update(*args, **kwargs)
+
def select_related(self, *args, **kwargs):
return self.get_query_set().select_related(*args, **kwargs)
diff --git a/django/db/models/query.py b/django/db/models/query.py
index fbd391971f..8e33765fdf 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -435,6 +435,7 @@ class QuerySet(object):
del_query._for_write = True
# Disable non-supported fields.
+ del_query.query.select_for_update = False
del_query.query.select_related = False
del_query.query.clear_ordering()
@@ -583,6 +584,18 @@ class QuerySet(object):
else:
return self._filter_or_exclude(None, **filter_obj)
+ def select_for_update(self, **kwargs):
+ """
+ Returns a new QuerySet instance that will select objects with a
+ FOR UPDATE lock.
+ """
+ # Default to false for nowait
+ nowait = kwargs.pop('nowait', False)
+ obj = self._clone()
+ obj.query.select_for_update = True
+ obj.query.select_for_update_nowait = nowait
+ return obj
+
def select_related(self, *fields, **kwargs):
"""
Returns a new QuerySet instance that will select related objects.
diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py
index d425c8b32b..ace9cf4c88 100644
--- a/django/db/models/sql/compiler.py
+++ b/django/db/models/sql/compiler.py
@@ -1,11 +1,13 @@
from django.core.exceptions import FieldError
from django.db import connections
+from django.db import transaction
from django.db.backends.util import truncate_name
from django.db.models.sql.constants import *
from django.db.models.sql.datastructures import EmptyResultSet
from django.db.models.sql.expressions import SQLEvaluator
from django.db.models.sql.query import get_proxied_model, get_order_dir, \
select_related_descend, Query
+from django.db.utils import DatabaseError
class SQLCompiler(object):
def __init__(self, query, connection, using):
@@ -117,6 +119,14 @@ class SQLCompiler(object):
result.append('LIMIT %d' % val)
result.append('OFFSET %d' % self.query.low_mark)
+ if self.query.select_for_update and self.connection.features.has_select_for_update:
+ # If we've been asked for a NOWAIT query but the backend does not support it,
+ # raise a DatabaseError otherwise we could get an unexpected deadlock.
+ nowait = self.query.select_for_update_nowait
+ if nowait and not self.connection.features.has_select_for_update_nowait:
+ raise DatabaseError('NOWAIT is not supported on this database backend.')
+ result.append(self.connection.ops.for_update_sql(nowait=nowait))
+
return ' '.join(result), tuple(params)
def as_nested_sql(self):
@@ -677,6 +687,11 @@ class SQLCompiler(object):
resolve_columns = hasattr(self, 'resolve_columns')
fields = None
has_aggregate_select = bool(self.query.aggregate_select)
+ # Set transaction dirty if we're using SELECT FOR UPDATE to ensure
+ # a subsequent commit/rollback is executed, so any database locks
+ # are released.
+ if self.query.select_for_update and transaction.is_managed(self.using):
+ transaction.set_dirty(self.using)
for rows in self.execute_sql(MULTI):
for row in rows:
if resolve_columns:
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
index 59f7bcbf14..f41272cf0f 100644
--- a/django/db/models/sql/query.py
+++ b/django/db/models/sql/query.py
@@ -125,6 +125,8 @@ class Query(object):
self.order_by = []
self.low_mark, self.high_mark = 0, None # Used for offset/limit
self.distinct = False
+ self.select_for_update = False
+ self.select_for_update_nowait = False
self.select_related = False
self.related_select_cols = []
@@ -254,6 +256,8 @@ class Query(object):
obj.order_by = self.order_by[:]
obj.low_mark, obj.high_mark = self.low_mark, self.high_mark
obj.distinct = self.distinct
+ obj.select_for_update = self.select_for_update
+ obj.select_for_update_nowait = self.select_for_update_nowait
obj.select_related = self.select_related
obj.related_select_cols = []
obj.aggregates = copy.deepcopy(self.aggregates, memo=memo)
@@ -360,6 +364,7 @@ class Query(object):
query.clear_ordering(True)
query.clear_limits()
+ query.select_for_update = False
query.select_related = False
query.related_select_cols = []
query.related_select_fields = []