Skip to content

Commit 0797c34

Browse files
author
Mike Dirolf
committed
ensure_index
1 parent fa90822 commit 0797c34

File tree

5 files changed

+166
-5
lines changed

5 files changed

+166
-5
lines changed

pymongo/collection.py

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ def _gen_index_name(self, keys):
321321
"""
322322
return u"_".join([u"%s_%s" % item for item in keys])
323323

324-
def create_index(self, key_or_list, direction=None, unique=False):
324+
def create_index(self, key_or_list, direction=None, unique=False, ttl=300):
325325
"""Creates an index on this collection.
326326
327327
Takes either a single key and a direction, or a list of (key, direction)
@@ -331,27 +331,76 @@ def create_index(self, key_or_list, direction=None, unique=False):
331331
332332
:Parameters:
333333
- `key_or_list`: a single key or a list of (key, direction) pairs
334-
specifying the index to ensure
334+
specifying the index to create
335335
- `direction` (optional): must be included if key_or_list is a single
336336
key, otherwise must be None
337337
- `unique` (optional): should this index guarantee uniqueness?
338+
- `ttl` (optional): time window (in seconds) during which this index
339+
will be recognized by subsequent calls to `ensure_index` - see
340+
documentation for `ensure_index` for details
338341
"""
339342
to_save = SON()
340343
keys = pymongo._index_list(key_or_list, direction)
341-
to_save["name"] = self._gen_index_name(keys)
344+
name = self._gen_index_name(keys)
345+
to_save["name"] = name
342346
to_save["ns"] = self.full_name()
343347
to_save["key"] = pymongo._index_document(keys)
344348
to_save["unique"] = unique
345349

346-
self.__database.system.indexes.save(to_save, False)
350+
self.database().connection()._cache_index(self.__database.name(),
351+
self.name(),
352+
name, ttl)
353+
354+
self.database().system.indexes.save(to_save, False)
347355
return to_save["name"]
348356

357+
def ensure_index(self, key_or_list, direction=None, unique=False, ttl=300):
358+
"""Ensures that an index exists on this collection.
359+
360+
Takes either a single key and a direction, or a list of (key, direction)
361+
pairs. The key(s) must be an instance of (str, unicode), and the
362+
direction(s) must be one of (`pymongo.ASCENDING`, `pymongo.DESCENDING`).
363+
364+
Unlike `create_index`, which attempts to create an index
365+
unconditionally, `ensure_index` takes advantage of some caching within
366+
the driver such that it only attempts to create indexes that might
367+
not already exist. When an index is created (or ensured) by PyMongo
368+
it is "remembered" for `ttl` seconds. Repeated calls to `ensure_index`
369+
within that time limit will be lightweight - they will not attempt to
370+
actually create the index.
371+
372+
Care must be taken when the database is being accessed through multiple
373+
connections at once. If an index is created using PyMongo and then
374+
deleted using another connection any call to `ensure_index` within the
375+
cache window will fail to re-create the missing index.
376+
377+
Returns the name of the created index if an index is actually created.
378+
Returns None if the index already exists.
379+
380+
:Parameters:
381+
- `key_or_list`: a single key or a list of (key, direction) pairs
382+
specifying the index to ensure
383+
- `direction` (optional): must be included if key_or_list is a single
384+
key, otherwise must be None
385+
- `unique` (optional): should this index guarantee uniqueness?
386+
- `ttl` (optional): time window (in seconds) during which this index
387+
will be recognized by subsequent calls to `ensure_index`
388+
"""
389+
keys = pymongo._index_list(key_or_list, direction)
390+
name = self._gen_index_name(keys)
391+
if self.database().connection()._cache_index(self.__database.name(),
392+
self.name(),
393+
name, ttl):
394+
return self.create_index(key_or_list, direction, unique, ttl)
395+
return None
396+
349397
def drop_indexes(self):
350398
"""Drops all indexes on this collection.
351399
352400
Can be used on non-existant collections or collections with no indexes.
353401
Raises OperationFailure on an error.
354402
"""
403+
self.database().connection()._purge_index(self.database().name(), self.name())
355404
self.drop_index(u"*")
356405

357406
def drop_index(self, index_or_name):
@@ -373,6 +422,7 @@ def drop_index(self, index_or_name):
373422
if not isinstance(name, types.StringTypes):
374423
raise TypeError("index_or_name must be an index name or list")
375424

425+
self.database().connection()._purge_index(self.database().name(), self.name(), name)
376426
self.__database._command(SON([("deleteIndexes",
377427
self.__collection_name),
378428
("index", name)]),

pymongo/connection.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import threading
2323
import random
2424
import errno
25+
import datetime
2526

2627
from errors import ConnectionFailure, InvalidName, OperationFailure, ConfigurationError
2728
from database import Database
@@ -108,6 +109,9 @@ def __init__(self, host=None, port=None, pool_size=None,
108109
self.__sockets = [None for _ in range(self.__pool_size)]
109110
self.__currently_resetting = False
110111

112+
# cache of existing indexes used by ensure_index ops
113+
self.__index_cache = {}
114+
111115
if _connect:
112116
self.__find_master()
113117

@@ -181,6 +185,59 @@ def __master(self, sock):
181185
port = int(strings[1])
182186
return (strings[0], port)
183187

188+
def _cache_index(self, database_name, collection_name, index_name, ttl):
189+
"""Add an index to the index cache for ensure_index operations.
190+
191+
Return True if the index has been newly cached or if the index had
192+
expired and is being re-cached.
193+
194+
Return False if the index exists and is valid.
195+
"""
196+
now = datetime.datetime.utcnow()
197+
expire = datetime.timedelta(seconds=ttl) + now
198+
199+
if database_name not in self.__index_cache:
200+
self.__index_cache[database_name] = {}
201+
self.__index_cache[database_name][collection_name] = {}
202+
self.__index_cache[database_name][collection_name][index_name] = expire
203+
return True
204+
205+
if collection_name not in self.__index_cache[database_name]:
206+
self.__index_cache[database_name][collection_name] = {}
207+
self.__index_cache[database_name][collection_name][index_name] = expire
208+
return True
209+
210+
if index_name in self.__index_cache[database_name][collection_name]:
211+
if now < self.__index_cache[database_name][collection_name][index_name]:
212+
return False
213+
214+
self.__index_cache[database_name][collection_name][index_name] = expire
215+
return True
216+
217+
def _purge_index(self, database_name, collection_name=None, index_name=None):
218+
"""Purge an index from the index cache.
219+
220+
If `index_name` is None purge an entire collection.
221+
222+
If `collection_name` is None purge an entire database.
223+
"""
224+
if not database_name in self.__index_cache:
225+
return
226+
227+
if collection_name is None:
228+
del self.__index_cache[database_name]
229+
return
230+
231+
if not collection_name in self.__index_cache[database_name]:
232+
return
233+
234+
if index_name is None:
235+
del self.__index_cache[database_name][collection_name]
236+
return
237+
238+
if index_name in self.__index_cache[database_name][collection_name]:
239+
del self.__index_cache[database_name][collection_name][index_name]
240+
184241
def host(self):
185242
"""Get the connection's current host.
186243
"""
@@ -574,6 +631,7 @@ def drop_database(self, name_or_database):
574631
if not isinstance(name, types.StringTypes):
575632
raise TypeError("name_or_database must be an instance of (Database, str, unicode)")
576633

634+
self._purge_index(name)
577635
self[name]._command({"dropDatabase": 1})
578636

579637
def __iter__(self):

pymongo/database.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ def drop_collection(self, name_or_collection):
196196
if not isinstance(name, types.StringTypes):
197197
raise TypeError("name_or_collection must be an instance of (Collection, str, unicode)")
198198

199+
self.connection()._purge_index(self.name(), name)
200+
199201
if name not in self.collection_names():
200202
return
201203

pymongo/master_slave_connection.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,3 +207,9 @@ def __iter__(self):
207207

208208
def next(self):
209209
raise TypeError("'MasterSlaveConnection' object is not iterable")
210+
211+
def _cache_index(self, database_name, collection_name, index_name, ttl):
212+
return self.__master._cache_index(database_name, collection_name, index_name, ttl)
213+
214+
def _purge_index(self, database_name, collection_name=None, index_name=None):
215+
return self.__master._purge_index(database_name, collection_name, index_name)

test/test_collection.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""Test the collection module."""
1616
import unittest
1717
import re
18+
import time
1819
import sys
1920
sys.path[0:0] = [""]
2021

@@ -29,7 +30,8 @@
2930

3031
class TestCollection(unittest.TestCase):
3132
def setUp(self):
32-
self.db = get_connection().pymongo_test
33+
self.connection = get_connection()
34+
self.db = self.connection.pymongo_test
3335

3436
def test_collection(self):
3537
self.assertRaises(TypeError, Collection, self.db, 5)
@@ -92,6 +94,49 @@ def test_create_index(self):
9294
(u"key", SON([(u"hello", -1),
9395
(u"world", 1)]))]) in list(db.system.indexes.find({"ns": u"pymongo_test.test"})))
9496

97+
def test_ensure_index(self):
98+
db = self.db
99+
100+
db.test.drop_indexes()
101+
self.assertEqual("hello_1", db.test.create_index("hello", ASCENDING))
102+
self.assertEqual("hello_1", db.test.create_index("hello", ASCENDING))
103+
104+
self.assertEqual("goodbye_1", db.test.ensure_index("goodbye", ASCENDING))
105+
self.assertEqual(None, db.test.ensure_index("goodbye", ASCENDING))
106+
107+
db.test.drop_indexes()
108+
self.assertEqual("goodbye_1", db.test.ensure_index("goodbye", ASCENDING))
109+
self.assertEqual(None, db.test.ensure_index("goodbye", ASCENDING))
110+
111+
db.test.drop_index("goodbye_1")
112+
self.assertEqual("goodbye_1", db.test.ensure_index("goodbye", ASCENDING))
113+
self.assertEqual(None, db.test.ensure_index("goodbye", ASCENDING))
114+
115+
db.drop_collection("test")
116+
self.assertEqual("goodbye_1", db.test.ensure_index("goodbye", ASCENDING))
117+
self.assertEqual(None, db.test.ensure_index("goodbye", ASCENDING))
118+
119+
db_name = self.db.name()
120+
self.connection.drop_database(self.db.name())
121+
self.assertEqual("goodbye_1", db.test.ensure_index("goodbye", ASCENDING))
122+
self.assertEqual(None, db.test.ensure_index("goodbye", ASCENDING))
123+
124+
db.test.drop_index("goodbye_1")
125+
self.assertEqual("goodbye_1", db.test.create_index("goodbye", ASCENDING))
126+
self.assertEqual(None, db.test.ensure_index("goodbye", ASCENDING))
127+
128+
db.test.drop_index("goodbye_1")
129+
self.assertEqual("goodbye_1", db.test.ensure_index("goodbye", ASCENDING,
130+
ttl=1))
131+
time.sleep(1.1)
132+
self.assertEqual("goodbye_1", db.test.ensure_index("goodbye", ASCENDING))
133+
134+
db.test.drop_index("goodbye_1")
135+
self.assertEqual("goodbye_1", db.test.create_index("goodbye", ASCENDING,
136+
ttl=1))
137+
time.sleep(1.1)
138+
self.assertEqual("goodbye_1", db.test.ensure_index("goodbye", ASCENDING))
139+
95140
def test_index_on_binary(self):
96141
db = self.db
97142
db.drop_collection("test")

0 commit comments

Comments
 (0)