Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Added support for index management

  • Loading branch information...
commit 7de62e25ef98e65d3ddf2b1b1fa05802195643ed 1 parent 5bb5385
@PlasticLizard PlasticLizard authored
View
146 lib/em-mongo/collection.rb
@@ -512,6 +512,96 @@ def stats
@db.command({:collstats => @name})
end
+ # Get information on the indexes for this collection.
+ #
+ # @return [Hash] a hash where the keys are index names.
+ #
+ # @core indexes
+ def index_information
+ db.index_information(@ns)
+ end
+
+ # Create a new index.
+ #
+ # @param [String, Array] spec
+ # should be either a single field name or an array of
+ # [field name, direction] pairs. Directions should be specified
+ # as Mongo::ASCENDING, Mongo::DESCENDING, or Mongo::GEO2D.
+ #
+ # Note that geospatial indexing only works with versions of MongoDB >= 1.3.3+. Keep in mind, too,
+ # that in order to geo-index a given field, that field must reference either an array or a sub-object
+ # where the first two values represent x- and y-coordinates. Examples can be seen below.
+ #
+ # Also note that it is permissible to create compound indexes that include a geospatial index as
+ # long as the geospatial index comes first.
+ #
+ # If your code calls create_index frequently, you can use Collection#ensure_index to cache these calls
+ # and thereby prevent excessive round trips to the database.
+ #
+ # @option opts [Boolean] :unique (false) if true, this index will enforce a uniqueness constraint.
+ # @option opts [Boolean] :background (false) indicate that the index should be built in the background. This
+ # feature is only available in MongoDB >= 1.3.2.
+ # @option opts [Boolean] :drop_dups (nil) If creating a unique index on a collection with pre-existing records,
+ # this option will keep the first document the database indexes and drop all subsequent with duplicate values.
+ # @option opts [Integer] :min (nil) specify the minimum longitude and latitude for a geo index.
+ # @option opts [Integer] :max (nil) specify the maximum longitude and latitude for a geo index.
+ #
+ # @example Creating a compound index:
+ # @posts.create_index([['subject', Mongo::ASCENDING], ['created_at', Mongo::DESCENDING]])
+ #
+ # @example Creating a geospatial index:
+ # @restaurants.create_index([['location', Mongo::GEO2D]])
+ #
+ # # Note that this will work only if 'location' represents x,y coordinates:
+ # {'location': [0, 50]}
+ # {'location': {'x' => 0, 'y' => 50}}
+ # {'location': {'latitude' => 0, 'longitude' => 50}}
+ #
+ # @example A geospatial index with alternate longitude and latitude:
+ # @restaurants.create_index([['location', Mongo::GEO2D]], :min => 500, :max => 500)
+ #
+ # @return [String] the name of the index created.
+ #
+ # @core indexes create_index-instance_method
+ def create_index(spec, opts={})
+ field_spec = parse_index_spec(spec)
+ opts = opts.dup
+ name = opts.delete(:name) || generate_index_name(field_spec)
+ name = name.to_s if name
+
+ generate_indexes(field_spec, name, opts)
+ name
+ end
+
+ # Drop a specified index.
+ #
+ # @param [String] name
+ #
+ # @core indexes
+ def drop_index(name)
+ if name.is_a?(Array)
+ response = RequestResponse.new
+ name_resp = index_name(name)
+ name_resp.callback do |name|
+ drop_resp = db.drop_index(@ns, name)
+ drop_resp.callback { response.succeed }
+ drop_resp.errback { |err| response.fail(err) }
+ end
+ name_resp.errback { |err| response.fail(err) }
+ response
+ else
+ db.drop_index(@ns, name)
+ end
+ end
+
+ # Drop all indexes.
+ #
+ # @core indexes
+ def drop_indexes
+ # Note: calling drop_indexes with no args will drop them all.
+ db.drop_index(@ns, '*')
+ end
+
protected
def normalize_hint_fields(hint)
@@ -559,5 +649,61 @@ def insert_documents(documents, collection_name=@name, check_keys=true)
documents.collect { |o| o[:_id] || o['_id'] }
end
+
+
+ def index_name(spec)
+ response = RequestResponse.new
+ field_spec = parse_index_spec(spec)
+ info_resp = index_information
+ info_resp.callback do |indexes|
+ found = indexes.values.find do |index|
+ index['key'] == field_spec
+ end
+ response.succeed( found ? found['name'] : nil )
+ end
+ info_resp.errback do |err|
+ response.fail err
+ end
+ response
+ end
+
+ def parse_index_spec(spec)
+ field_spec = BSON::OrderedHash.new
+ if spec.is_a?(String) || spec.is_a?(Symbol)
+ field_spec[spec.to_s] = 1
+ elsif spec.is_a?(Array) && spec.all? {|field| field.is_a?(Array) }
+ spec.each do |f|
+ if [EM::Mongo::ASCENDING, EM::Mongo::DESCENDING, EM::Mongo::GEO2D].include?(f[1])
+ field_spec[f[0].to_s] = f[1]
+ else
+ raise MongoArgumentError, "Invalid index field #{f[1].inspect}; " +
+ "should be one of Mongo::ASCENDING (1), Mongo::DESCENDING (-1) or Mongo::GEO2D ('2d')."
+ end
+ end
+ else
+ raise MongoArgumentError, "Invalid index specification #{spec.inspect}; " +
+ "should be either a string, symbol, or an array of arrays."
+ end
+ field_spec
+ end
+
+ def generate_indexes(field_spec, name, opts)
+ selector = {
+ :name => name,
+ :ns => "#{@db}.#{@ns}",
+ :key => field_spec
+ }
+ selector.merge!(opts)
+ insert_documents([selector], EM::Mongo::Database::SYSTEM_INDEX_COLLECTION, false)
+ end
+
+ def generate_index_name(spec)
+ indexes = []
+ spec.each_pair do |field, direction|
+ indexes.push("#{field}_#{direction}")
+ end
+ indexes.join("_")
+ end
+
end
end
View
6 lib/em-mongo/connection.rb
@@ -21,6 +21,12 @@ module EM::Mongo
OP_QUERY_AWAIT_DATA = 2 ** 5
OP_QUERY_EXHAUST = 2 ** 6
+ ASCENDING = 1
+ DESCENDING = -1
+ GEO2D = '2d'
+
+ DEFAULT_MAX_BSON_SIZE = 4 * 1024 * 1024
+
class EMConnection < EM::Connection
MAX_RETRIES = 5
View
107 spec/integration/collection_spec.rb
@@ -558,6 +558,113 @@
end
end
+ context "indexes" do
+ it "should create an index using symbols" do
+ @conn, @collection = connection_and_collection('test-collection')
+ @collection.create_index :foo, :name => :bar
+ @collection.index_information.callback do |info|
+ info['bar'].should_not be_nil
+ done
+ end
+ end
+
+ it "should create a geospatial index" do
+ @conn, @geo = connection_and_collection('geo')
+ @geo.save({'loc' => [-100, 100]})
+ @geo.create_index([['loc', EM::Mongo::GEO2D]])
+ @geo.index_information.callback do |info|
+ info['loc_2d'].should_not be_nil
+ done
+ end
+ end
+
+ it "should create a unique index" do
+ @conn, @collection = connection_and_collection('test-collection')
+ @collection.create_index([['a', EM::Mongo::ASCENDING]], :unique => true)
+ @collection.index_information.callback do |info|
+ info['a_1']['unique'].should == true
+ done
+ end
+ end
+
+ it "should create an index in the background" do
+ @conn, @collection = connection_and_collection('test-collection')
+ @collection.create_index([['b', EM::Mongo::ASCENDING]], :background => true)
+ @collection.index_information.callback do |info|
+ info['b_1']['background'].should == true
+ done
+ end
+ end
+
+ it "should require an array of arrays" do
+ @conn, @collection = connection_and_collection('test-collection')
+ proc { @collection.create_index(['c', EM::Mongo::ASCENDING]) }.should raise_error
+ done
+ end
+
+ it "should enforce proper index types" do
+ @conn, @collection = connection_and_collection('test-collection')
+ proc { @collection.create_index([['c', 'blah']]) }.should raise_error
+ done
+ end
+
+ it "should allow an alernate name to be specified" do
+ @conn, @collection = connection_and_collection('test-collection')
+ @collection.create_index :bar, :name => 'foo_index'
+ @collection.index_information.callback do |info|
+ info['foo_index'].should_not be_nil
+ done
+ end
+ end
+
+ it "should generate indexes in the proper order" do
+ @conn, @collection = connection_and_collection('test-collection')
+ @collection.should_receive(:insert_documents) do |sel, coll|
+ sel[0][:name].should == 'b_1_a_1'
+ end
+ @collection.create_index([['b',1],['a',1]])
+ done
+ end
+
+ it "should allow multiple calls to create_index" do
+ @conn, @collection = connection_and_collection('test-collection')
+ @collection.create_index([['a',1]]).should be_true
+ @collection.create_index([['a',1]]).should be_true
+ done
+ end
+
+ it "should allow the creation of multiple indexes" do
+ @conn, @collection = connection_and_collection('test-collection')
+ @collection.create_index([['a',1]]).should be_true
+ @collection.create_index([['b',1]]).should be_true
+ done
+ end
+
+ it "should return a properly ordered index info" do
+ @conn, @collection = connection_and_collection('test-collection')
+ @collection.create_index([['b',1],['a',1]])
+ @collection.index_information.callback do |info|
+ info['b_1_a_1'].should_not be_nil
+ done
+ end
+ end
+
+ it "should drop an index" do
+ @conn, @collection = connection_and_collection('test-collection')
+ @collection.create_index([['a',EM::Mongo::ASCENDING]])
+ @collection.index_information.callback do |info|
+ info['a_1'].should_not be_nil
+ @collection.drop_index([['a',EM::Mongo::ASCENDING]]).callback do
+ @collection.index_information.callback do |info|
+ info['a_1'].should be_nil
+ done
+ end
+ end
+ end
+
+ end
+ end
+
it 'should handle multiple pending queries' do
@conn, @coll = connection_and_collection
View
6 spec/integration/database_spec.rb
@@ -173,4 +173,10 @@
end
end
+ describe "Indexes" do
+ #Index functions are integration tested via the collection specs. Maybe the wrong order,
+ #but the collection index functions all call down to the database index functions, and the
+ #tests would simply duplicate eachother
+ end
+
end
View
10 spec/spec_helper.rb
@@ -12,10 +12,14 @@
require "em-spec/rspec"
-def connection_and_collection
+def connection_and_collection(collection_name=EM::Mongo::DEFAULT_NS)
conn = EMMongo::Connection.new
- conn.db.collection.remove
- return conn, conn.db.collection
+ return conn, collection(conn, collection_name)
+end
+
+def collection(conn, name)
+ conn.db.collection(name).remove
+ conn.db.collection(name)
end
def number_hash
Please sign in to comment.
Something went wrong with that request. Please try again.