ensure_index

This commit is contained in:
Mike Dirolf 2009-05-14 15:50:31 -04:00
parent fa9082209e
commit 0797c341ff
5 changed files with 166 additions and 5 deletions

View File

@ -321,7 +321,7 @@ class Collection(object):
"""
return u"_".join([u"%s_%s" % item for item in keys])
def create_index(self, key_or_list, direction=None, unique=False):
def create_index(self, key_or_list, direction=None, unique=False, ttl=300):
"""Creates an index on this collection.
Takes either a single key and a direction, or a list of (key, direction)
@ -331,27 +331,76 @@ class Collection(object):
:Parameters:
- `key_or_list`: a single key or a list of (key, direction) pairs
specifying the index to ensure
specifying the index to create
- `direction` (optional): must be included if key_or_list is a single
key, otherwise must be None
- `unique` (optional): should this index guarantee uniqueness?
- `ttl` (optional): time window (in seconds) during which this index
will be recognized by subsequent calls to `ensure_index` - see
documentation for `ensure_index` for details
"""
to_save = SON()
keys = pymongo._index_list(key_or_list, direction)
to_save["name"] = self._gen_index_name(keys)
name = self._gen_index_name(keys)
to_save["name"] = name
to_save["ns"] = self.full_name()
to_save["key"] = pymongo._index_document(keys)
to_save["unique"] = unique
self.__database.system.indexes.save(to_save, False)
self.database().connection()._cache_index(self.__database.name(),
self.name(),
name, ttl)
self.database().system.indexes.save(to_save, False)
return to_save["name"]
def ensure_index(self, key_or_list, direction=None, unique=False, ttl=300):
"""Ensures that an index exists on this collection.
Takes either a single key and a direction, or a list of (key, direction)
pairs. The key(s) must be an instance of (str, unicode), and the
direction(s) must be one of (`pymongo.ASCENDING`, `pymongo.DESCENDING`).
Unlike `create_index`, which attempts to create an index
unconditionally, `ensure_index` takes advantage of some caching within
the driver such that it only attempts to create indexes that might
not already exist. When an index is created (or ensured) by PyMongo
it is "remembered" for `ttl` seconds. Repeated calls to `ensure_index`
within that time limit will be lightweight - they will not attempt to
actually create the index.
Care must be taken when the database is being accessed through multiple
connections at once. If an index is created using PyMongo and then
deleted using another connection any call to `ensure_index` within the
cache window will fail to re-create the missing index.
Returns the name of the created index if an index is actually created.
Returns None if the index already exists.
:Parameters:
- `key_or_list`: a single key or a list of (key, direction) pairs
specifying the index to ensure
- `direction` (optional): must be included if key_or_list is a single
key, otherwise must be None
- `unique` (optional): should this index guarantee uniqueness?
- `ttl` (optional): time window (in seconds) during which this index
will be recognized by subsequent calls to `ensure_index`
"""
keys = pymongo._index_list(key_or_list, direction)
name = self._gen_index_name(keys)
if self.database().connection()._cache_index(self.__database.name(),
self.name(),
name, ttl):
return self.create_index(key_or_list, direction, unique, ttl)
return None
def drop_indexes(self):
"""Drops all indexes on this collection.
Can be used on non-existant collections or collections with no indexes.
Raises OperationFailure on an error.
"""
self.database().connection()._purge_index(self.database().name(), self.name())
self.drop_index(u"*")
def drop_index(self, index_or_name):
@ -373,6 +422,7 @@ class Collection(object):
if not isinstance(name, types.StringTypes):
raise TypeError("index_or_name must be an index name or list")
self.database().connection()._purge_index(self.database().name(), self.name(), name)
self.__database._command(SON([("deleteIndexes",
self.__collection_name),
("index", name)]),

View File

@ -22,6 +22,7 @@ import logging
import threading
import random
import errno
import datetime
from errors import ConnectionFailure, InvalidName, OperationFailure, ConfigurationError
from database import Database
@ -108,6 +109,9 @@ class Connection(object):
self.__sockets = [None for _ in range(self.__pool_size)]
self.__currently_resetting = False
# cache of existing indexes used by ensure_index ops
self.__index_cache = {}
if _connect:
self.__find_master()
@ -181,6 +185,59 @@ class Connection(object):
port = int(strings[1])
return (strings[0], port)
def _cache_index(self, database_name, collection_name, index_name, ttl):
"""Add an index to the index cache for ensure_index operations.
Return True if the index has been newly cached or if the index had
expired and is being re-cached.
Return False if the index exists and is valid.
"""
now = datetime.datetime.utcnow()
expire = datetime.timedelta(seconds=ttl) + now
if database_name not in self.__index_cache:
self.__index_cache[database_name] = {}
self.__index_cache[database_name][collection_name] = {}
self.__index_cache[database_name][collection_name][index_name] = expire
return True
if collection_name not in self.__index_cache[database_name]:
self.__index_cache[database_name][collection_name] = {}
self.__index_cache[database_name][collection_name][index_name] = expire
return True
if index_name in self.__index_cache[database_name][collection_name]:
if now < self.__index_cache[database_name][collection_name][index_name]:
return False
self.__index_cache[database_name][collection_name][index_name] = expire
return True
def _purge_index(self, database_name, collection_name=None, index_name=None):
"""Purge an index from the index cache.
If `index_name` is None purge an entire collection.
If `collection_name` is None purge an entire database.
"""
if not database_name in self.__index_cache:
return
if collection_name is None:
del self.__index_cache[database_name]
return
if not collection_name in self.__index_cache[database_name]:
return
if index_name is None:
del self.__index_cache[database_name][collection_name]
return
if index_name in self.__index_cache[database_name][collection_name]:
del self.__index_cache[database_name][collection_name][index_name]
def host(self):
"""Get the connection's current host.
"""
@ -574,6 +631,7 @@ class Connection(object):
if not isinstance(name, types.StringTypes):
raise TypeError("name_or_database must be an instance of (Database, str, unicode)")
self._purge_index(name)
self[name]._command({"dropDatabase": 1})
def __iter__(self):

View File

@ -196,6 +196,8 @@ class Database(object):
if not isinstance(name, types.StringTypes):
raise TypeError("name_or_collection must be an instance of (Collection, str, unicode)")
self.connection()._purge_index(self.name(), name)
if name not in self.collection_names():
return

View File

@ -207,3 +207,9 @@ class MasterSlaveConnection(object):
def next(self):
raise TypeError("'MasterSlaveConnection' object is not iterable")
def _cache_index(self, database_name, collection_name, index_name, ttl):
return self.__master._cache_index(database_name, collection_name, index_name, ttl)
def _purge_index(self, database_name, collection_name=None, index_name=None):
return self.__master._purge_index(database_name, collection_name, index_name)

View File

@ -15,6 +15,7 @@
"""Test the collection module."""
import unittest
import re
import time
import sys
sys.path[0:0] = [""]
@ -29,7 +30,8 @@ from pymongo.son import SON
class TestCollection(unittest.TestCase):
def setUp(self):
self.db = get_connection().pymongo_test
self.connection = get_connection()
self.db = self.connection.pymongo_test
def test_collection(self):
self.assertRaises(TypeError, Collection, self.db, 5)
@ -92,6 +94,49 @@ class TestCollection(unittest.TestCase):
(u"key", SON([(u"hello", -1),
(u"world", 1)]))]) in list(db.system.indexes.find({"ns": u"pymongo_test.test"})))
def test_ensure_index(self):
db = self.db
db.test.drop_indexes()
self.assertEqual("hello_1", db.test.create_index("hello", ASCENDING))
self.assertEqual("hello_1", db.test.create_index("hello", ASCENDING))
self.assertEqual("goodbye_1", db.test.ensure_index("goodbye", ASCENDING))
self.assertEqual(None, db.test.ensure_index("goodbye", ASCENDING))
db.test.drop_indexes()
self.assertEqual("goodbye_1", db.test.ensure_index("goodbye", ASCENDING))
self.assertEqual(None, db.test.ensure_index("goodbye", ASCENDING))
db.test.drop_index("goodbye_1")
self.assertEqual("goodbye_1", db.test.ensure_index("goodbye", ASCENDING))
self.assertEqual(None, db.test.ensure_index("goodbye", ASCENDING))
db.drop_collection("test")
self.assertEqual("goodbye_1", db.test.ensure_index("goodbye", ASCENDING))
self.assertEqual(None, db.test.ensure_index("goodbye", ASCENDING))
db_name = self.db.name()
self.connection.drop_database(self.db.name())
self.assertEqual("goodbye_1", db.test.ensure_index("goodbye", ASCENDING))
self.assertEqual(None, db.test.ensure_index("goodbye", ASCENDING))
db.test.drop_index("goodbye_1")
self.assertEqual("goodbye_1", db.test.create_index("goodbye", ASCENDING))
self.assertEqual(None, db.test.ensure_index("goodbye", ASCENDING))
db.test.drop_index("goodbye_1")
self.assertEqual("goodbye_1", db.test.ensure_index("goodbye", ASCENDING,
ttl=1))
time.sleep(1.1)
self.assertEqual("goodbye_1", db.test.ensure_index("goodbye", ASCENDING))
db.test.drop_index("goodbye_1")
self.assertEqual("goodbye_1", db.test.create_index("goodbye", ASCENDING,
ttl=1))
time.sleep(1.1)
self.assertEqual("goodbye_1", db.test.ensure_index("goodbye", ASCENDING))
def test_index_on_binary(self):
db = self.db
db.drop_collection("test")