diff --git a/README.rdoc b/README.rdoc index 23ca925..93b2fff 100644 --- a/README.rdoc +++ b/README.rdoc @@ -7,9 +7,9 @@ many more. require 'mongo' - include XGen::Mongo::Driver + include Mongo - db = Mongo.new('localhost').db('sample-db') + db = Mongo::Mongo.new('localhost').db('sample-db') coll = db.collection('test') coll.clear @@ -79,8 +79,8 @@ Here is some simple example code: require 'rubygems' # not required for Ruby 1.9 require 'mongo' - include XGen::Mongo::Driver - db = Mongo.new.db('my-db-name') + include Mongo + db = Mongo::Mongo.new.db('my-db-name') things = db.collection('things') things.clear @@ -149,8 +149,8 @@ using a PK factory lets you do so. You can tell the Ruby Mongo driver how to create primary keys by passing in the :pk option to the Mongo#db method. - include XGen::Mongo::Driver - db = Mongo.new.db('dbname', :pk => MyPKFactory.new) + include Mongo + db = Mongo::Mongo.new.db('dbname', :pk => MyPKFactory.new) A primary key factory object must respond to :create_pk, which should take a hash and return a hash which merges the original hash with any primary key @@ -164,7 +164,7 @@ Here is a sample primary key factory, taken from the tests: class TestPKFactory def create_pk(row) - row['_id'] ||= XGen::Mongo::Driver::ObjectID.new + row['_id'] ||= Mongo::ObjectID.new row end end @@ -178,7 +178,7 @@ ActiveRecord-like framework for non-Rails apps) and the AR Mongo adapter code def create_pk(row) return row if row[:_id] row.delete(:_id) # in case it exists but the value is nil - row['_id'] ||= XGen::Mongo::Driver::ObjectID.new + row['_id'] ||= Mongo::ObjectID.new row end end @@ -205,7 +205,7 @@ completely harmless; strict mode is a programmer convenience only. To turn on strict mode, either pass in :strict => true when obtaining a DB object or call the :strict= method: - db = XGen::Mongo::Driver::Mongo.new.db('dbname', :strict => true) + db = Mongo::Mongo.new.db('dbname', :strict => true) # I'm feeling lax db.strict = false # No, I'm not! diff --git a/bin/bson_benchmark.rb b/bin/bson_benchmark.rb index 2e54711..8f7acbc 100644 --- a/bin/bson_benchmark.rb +++ b/bin/bson_benchmark.rb @@ -3,7 +3,7 @@ $LOAD_PATH[0,0] = File.join(File.dirname(__FILE__), '..', 'lib') require 'mongo' -include XGen::Mongo::Driver +include Mongo TRIALS = 100000 diff --git a/bin/mongo_console b/bin/mongo_console index e0832c9..7478a6b 100755 --- a/bin/mongo_console +++ b/bin/mongo_console @@ -7,14 +7,14 @@ require 'irb' $LOAD_PATH[0,0] = File.join(File.dirname(__FILE__), '..', 'lib') require 'mongo' -include XGen::Mongo::Driver +include Mongo host = org_argv[0] || ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost' -port = org_argv[1] || ENV['MONGO_RUBY_DRIVER_PORT'] || XGen::Mongo::Driver::Mongo::DEFAULT_PORT +port = org_argv[1] || ENV['MONGO_RUBY_DRIVER_PORT'] || Mongo::Mongo::DEFAULT_PORT dbnm = org_argv[2] || ENV['MONGO_RUBY_DRIVER_DB'] || 'ruby-mongo-console' puts "Connecting to #{host}:#{port} (CONN) on with database #{dbnm} (DB)" -CONN = Mongo.new(host, port) +CONN = Mongo::Mongo.new(host, port) DB = CONN.db(dbnm) puts "Starting IRB session..." diff --git a/bin/standard_benchmark b/bin/standard_benchmark index 8e8bb96..8490097 100755 --- a/bin/standard_benchmark +++ b/bin/standard_benchmark @@ -5,7 +5,7 @@ $LOAD_PATH[0,0] = File.join(File.dirname(__FILE__), '..', 'lib') require 'mongo' -include XGen::Mongo::Driver +include Mongo TRIALS = 2 PER_TRIAL = 5000 @@ -50,9 +50,9 @@ def benchmark(str, proc, n, db, coll_name, object, setup=nil) end host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost' -port = ENV['MONGO_RUBY_DRIVER_PORT'] || XGen::Mongo::Driver::Mongo::DEFAULT_PORT +port = ENV['MONGO_RUBY_DRIVER_PORT'] || Mongo::Mongo::DEFAULT_PORT -connection = Mongo.new(host, port) +connection = Mongo::Mongo.new(host, port) connection.drop_database("benchmark") db = connection.db('benchmark') diff --git a/examples/admin.rb b/examples/admin.rb index ab35e7e..a7a37f6 100644 --- a/examples/admin.rb +++ b/examples/admin.rb @@ -2,13 +2,13 @@ $LOAD_PATH[0,0] = File.join(File.dirname(__FILE__), '..', 'lib') require 'mongo' require 'pp' -include XGen::Mongo::Driver +include Mongo host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost' -port = ENV['MONGO_RUBY_DRIVER_PORT'] || XGen::Mongo::Driver::Mongo::DEFAULT_PORT +port = ENV['MONGO_RUBY_DRIVER_PORT'] || Mongo::Mongo::DEFAULT_PORT puts "Connecting to #{host}:#{port}" -db = Mongo.new(host, port).db('ruby-mongo-examples') +db = Mongo::Mongo.new(host, port).db('ruby-mongo-examples') coll = db.create_collection('test') # Erase all records from collection, if any diff --git a/examples/benchmarks.rb b/examples/benchmarks.rb index 2a78632..5fe59b9 100644 --- a/examples/benchmarks.rb +++ b/examples/benchmarks.rb @@ -4,10 +4,10 @@ $LOAD_PATH[0,0] = File.join(File.dirname(__FILE__), '..', 'lib') require 'mongo' host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost' -port = ENV['MONGO_RUBY_DRIVER_PORT'] || XGen::Mongo::Driver::Mongo::DEFAULT_PORT +port = ENV['MONGO_RUBY_DRIVER_PORT'] || Mongo::Mongo::DEFAULT_PORT puts "Connecting to #{host}:#{port}" -db = XGen::Mongo::Driver::Mongo.new(host, port).db('ruby-mongo-examples') +db = Mongo::Mongo.new(host, port).db('ruby-mongo-examples') coll = db.collection('test') coll.clear diff --git a/examples/blog.rb b/examples/blog.rb index 8f1ac84..9daafaf 100644 --- a/examples/blog.rb +++ b/examples/blog.rb @@ -5,17 +5,17 @@ class Exception "%s: %s\n%s" % [self.class, message, (backtrace || []).join("\n") << "\n"] end end - + $LOAD_PATH[0,0] = File.join(File.dirname(__FILE__), '..', 'lib') require 'mongo' -include XGen::Mongo::Driver +include Mongo host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost' -port = ENV['MONGO_RUBY_DRIVER_PORT'] || XGen::Mongo::Driver::Mongo::DEFAULT_PORT +port = ENV['MONGO_RUBY_DRIVER_PORT'] || Mongo::Mongo::DEFAULT_PORT puts ">> Connecting to #{host}:#{port}" -DB = Mongo.new(host, port).db('ruby-mongo-blog') +DB = Mongo::Mongo.new(host, port).db('ruby-mongo-blog') LINE_SIZE = 120 puts "=" * LINE_SIZE diff --git a/examples/capped.rb b/examples/capped.rb index 5310ad8..786cc6a 100644 --- a/examples/capped.rb +++ b/examples/capped.rb @@ -1,13 +1,13 @@ $LOAD_PATH[0,0] = File.join(File.dirname(__FILE__), '..', 'lib') require 'mongo' -include XGen::Mongo::Driver +include Mongo host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost' -port = ENV['MONGO_RUBY_DRIVER_PORT'] || XGen::Mongo::Driver::Mongo::DEFAULT_PORT +port = ENV['MONGO_RUBY_DRIVER_PORT'] || Mongo::Mongo::DEFAULT_PORT puts "Connecting to #{host}:#{port}" -db = Mongo.new(host, port).db('ruby-mongo-examples') +db = Mongo::Mongo.new(host, port).db('ruby-mongo-examples') db.drop_collection('test') # A capped collection has a max size and optionally a max number of records. diff --git a/examples/cursor.rb b/examples/cursor.rb index a7896ed..6fc5377 100644 --- a/examples/cursor.rb +++ b/examples/cursor.rb @@ -2,13 +2,13 @@ $LOAD_PATH[0,0] = File.join(File.dirname(__FILE__), '..', 'lib') require 'mongo' require 'pp' -include XGen::Mongo::Driver +include Mongo host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost' -port = ENV['MONGO_RUBY_DRIVER_PORT'] || XGen::Mongo::Driver::Mongo::DEFAULT_PORT +port = ENV['MONGO_RUBY_DRIVER_PORT'] || Mongo::Mongo::DEFAULT_PORT puts "Connecting to #{host}:#{port}" -db = Mongo.new(host, port).db('ruby-mongo-examples') +db = Mongo::Mongo.new(host, port).db('ruby-mongo-examples') coll = db.collection('test') # Erase all records from collection, if any diff --git a/examples/gridfs.rb b/examples/gridfs.rb index 4217115..48fb1d6 100644 --- a/examples/gridfs.rb +++ b/examples/gridfs.rb @@ -2,14 +2,14 @@ $LOAD_PATH[0,0] = File.join(File.dirname(__FILE__), '..', 'lib') require 'mongo' require 'mongo/gridfs' -include XGen::Mongo::Driver -include XGen::Mongo::GridFS +include Mongo +include GridFS host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost' -port = ENV['MONGO_RUBY_DRIVER_PORT'] || XGen::Mongo::Driver::Mongo::DEFAULT_PORT +port = ENV['MONGO_RUBY_DRIVER_PORT'] || Mongo::Mongo::DEFAULT_PORT puts "Connecting to #{host}:#{port}" -db = Mongo.new(host, port).db('ruby-mongo-examples') +db = Mongo::Mongo.new(host, port).db('ruby-mongo-examples') def dump(db, fname) GridStore.open(db, fname, 'r') { |f| puts f.read } diff --git a/examples/index_test.rb b/examples/index_test.rb index c38a569..e528c09 100644 --- a/examples/index_test.rb +++ b/examples/index_test.rb @@ -3,22 +3,22 @@ class Exception "%s: %s\n%s" % [self.class, message, (backtrace || []).join("\n") << "\n"] end end - + $LOAD_PATH[0,0] = File.join(File.dirname(__FILE__), '..', 'lib') require 'mongo' -include XGen::Mongo::Driver +include Mongo host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost' -port = ENV['MONGO_RUBY_DRIVER_PORT'] || XGen::Mongo::Driver::Mongo::DEFAULT_PORT +port = ENV['MONGO_RUBY_DRIVER_PORT'] || Mongo::Mongo::DEFAULT_PORT puts ">> Connecting to #{host}:#{port}" -db = Mongo.new(host, port).db('ruby-mongo-index_test') +db = Mongo::Mongo.new(host, port).db('ruby-mongo-index_test') puts ">> Dropping collection test" begin res = db.drop_collection('test') - puts "dropped : #{res.inspect}" + puts "dropped : #{res.inspect}" rescue => e puts "Error: #{e.errmsg}" end @@ -26,7 +26,7 @@ end puts ">> Creating collection test" begin coll = db.collection('test') - puts "created : #{coll.inspect}" + puts "created : #{coll.inspect}" rescue => e puts "Error: #{e.errmsg}" end @@ -59,8 +59,8 @@ puts "created index: #{res.inspect}" puts ">> Gathering index information" begin - res = coll.index_information - puts "index_information : #{res.inspect}" + res = coll.index_information + puts "index_information : #{res.inspect}" rescue => e puts "Error: #{e.errmsg}" end @@ -76,7 +76,7 @@ end puts ">> Dropping index" begin - res = coll.drop_index "all" + res = coll.drop_index "all_1" puts "dropped : #{res.inspect}" rescue => e puts "Error: #{e.errmsg}" @@ -105,8 +105,8 @@ end puts ">> Gathering index information" begin - res = coll.index_information - puts "index_information : #{res.inspect}" + res = coll.index_information + puts "index_information : #{res.inspect}" rescue => e puts "Error: #{e.errmsg}" end diff --git a/examples/info.rb b/examples/info.rb index c6a986e..5d715f5 100644 --- a/examples/info.rb +++ b/examples/info.rb @@ -1,13 +1,13 @@ $LOAD_PATH[0,0] = File.join(File.dirname(__FILE__), '..', 'lib') require 'mongo' -include XGen::Mongo::Driver +include Mongo host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost' -port = ENV['MONGO_RUBY_DRIVER_PORT'] || XGen::Mongo::Driver::Mongo::DEFAULT_PORT +port = ENV['MONGO_RUBY_DRIVER_PORT'] || Mongo::Mongo::DEFAULT_PORT puts "Connecting to #{host}:#{port}" -db = Mongo.new(host, port).db('ruby-mongo-examples') +db = Mongo::Mongo.new(host, port).db('ruby-mongo-examples') coll = db.collection('test') # Erase all records from collection, if any diff --git a/examples/queries.rb b/examples/queries.rb index 06eb5cf..cb19a1f 100644 --- a/examples/queries.rb +++ b/examples/queries.rb @@ -2,13 +2,13 @@ $LOAD_PATH[0,0] = File.join(File.dirname(__FILE__), '..', 'lib') require 'mongo' require 'pp' -include XGen::Mongo::Driver +include Mongo host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost' -port = ENV['MONGO_RUBY_DRIVER_PORT'] || XGen::Mongo::Driver::Mongo::DEFAULT_PORT +port = ENV['MONGO_RUBY_DRIVER_PORT'] || Mongo::Mongo::DEFAULT_PORT puts "Connecting to #{host}:#{port}" -db = Mongo.new(host, port).db('ruby-mongo-examples') +db = Mongo::Mongo.new(host, port).db('ruby-mongo-examples') coll = db.collection('test') # Remove all records, if any diff --git a/examples/simple.rb b/examples/simple.rb index 3415fe3..979ab8b 100644 --- a/examples/simple.rb +++ b/examples/simple.rb @@ -1,13 +1,13 @@ $LOAD_PATH[0,0] = File.join(File.dirname(__FILE__), '..', 'lib') require 'mongo' -include XGen::Mongo::Driver +include Mongo host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost' -port = ENV['MONGO_RUBY_DRIVER_PORT'] || XGen::Mongo::Driver::Mongo::DEFAULT_PORT +port = ENV['MONGO_RUBY_DRIVER_PORT'] || Mongo::Mongo::DEFAULT_PORT puts "Connecting to #{host}:#{port}" -db = Mongo.new(host, port).db('ruby-mongo-examples') +db = Mongo::Mongo.new(host, port).db('ruby-mongo-examples') coll = db.collection('test') # Erase all records from collection, if any diff --git a/examples/strict.rb b/examples/strict.rb index b2f99a4..ee29b5f 100644 --- a/examples/strict.rb +++ b/examples/strict.rb @@ -1,13 +1,13 @@ $LOAD_PATH[0,0] = File.join(File.dirname(__FILE__), '..', 'lib') require 'mongo' -include XGen::Mongo::Driver +include Mongo host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost' -port = ENV['MONGO_RUBY_DRIVER_PORT'] || XGen::Mongo::Driver::Mongo::DEFAULT_PORT +port = ENV['MONGO_RUBY_DRIVER_PORT'] || Mongo::Mongo::DEFAULT_PORT puts "Connecting to #{host}:#{port}" -db = Mongo.new(host, port).db('ruby-mongo-examples') +db = Mongo::Mongo.new(host, port).db('ruby-mongo-examples') db.drop_collection('does-not-exist') db.create_collection('test') diff --git a/examples/types.rb b/examples/types.rb index 3e7d73f..30768ea 100644 --- a/examples/types.rb +++ b/examples/types.rb @@ -2,13 +2,13 @@ $LOAD_PATH[0,0] = File.join(File.dirname(__FILE__), '..', 'lib') require 'mongo' require 'pp' -include XGen::Mongo::Driver +include Mongo host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost' -port = ENV['MONGO_RUBY_DRIVER_PORT'] || XGen::Mongo::Driver::Mongo::DEFAULT_PORT +port = ENV['MONGO_RUBY_DRIVER_PORT'] || Mongo::Mongo::DEFAULT_PORT puts "Connecting to #{host}:#{port}" -db = Mongo.new(host, port).db('ruby-mongo-examples') +db = Mongo::Mongo.new(host, port).db('ruby-mongo-examples') coll = db.collection('test') # Remove all records, if any @@ -25,13 +25,8 @@ coll.insert('array' => [1, 2, 3], 'float' => 33.33333, 'regex' => /foobar/i, 'boolean' => true, - '$where' => Code.new('this.x == 3'), + 'where' => Code.new('this.x == 3'), 'dbref' => DBRef.new(coll.name, ObjectID.new), - -# NOTE: the undefined type is not saved to the database properly. This is a -# Mongo bug. However, the undefined type may go away completely. -# 'undef' => Undefined.new, - 'null' => nil, 'symbol' => :zildjian) diff --git a/ext/cbson/cbson.c b/ext/cbson/cbson.c index 406e5b1..3b848e6 100644 --- a/ext/cbson/cbson.c +++ b/ext/cbson/cbson.c @@ -275,7 +275,7 @@ static int write_element_allow_id(VALUE key, VALUE value, VALUE extra, int allow case T_STRING: { if (strcmp(rb_class2name(RBASIC(value)->klass), - "XGen::Mongo::Driver::Code") == 0) { + "Mongo::Code") == 0) { int start_position, length_location, length, total_length; write_name_and_type(buffer, key, 0x0F); @@ -314,7 +314,7 @@ static int write_element_allow_id(VALUE key, VALUE value, VALUE extra, int allow { // TODO there has to be a better way to do these checks... const char* cls = rb_class2name(RBASIC(value)->klass); - if (strcmp(cls, "XGen::Mongo::Driver::Binary") == 0 || + if (strcmp(cls, "Mongo::Binary") == 0 || strcmp(cls, "ByteBuffer") == 0) { const char subtype = strcmp(cls, "ByteBuffer") ? (const char)FIX2INT(rb_funcall(value, rb_intern("subtype"), 0)) : 2; @@ -333,7 +333,7 @@ static int write_element_allow_id(VALUE key, VALUE value, VALUE extra, int allow buffer_write_bytes(buffer, RSTRING_PTR(string_data), length); break; } - if (strcmp(cls, "XGen::Mongo::Driver::ObjectID") == 0) { + if (strcmp(cls, "Mongo::ObjectID") == 0) { VALUE as_array = rb_funcall(value, rb_intern("to_a"), 0); int i; write_name_and_type(buffer, key, 0x07); @@ -343,7 +343,7 @@ static int write_element_allow_id(VALUE key, VALUE value, VALUE extra, int allow } break; } - if (strcmp(cls, "XGen::Mongo::Driver::DBRef") == 0) { + if (strcmp(cls, "Mongo::DBRef") == 0) { int start_position, length_location, obj_length; VALUE ns, oid; write_name_and_type(buffer, key, 0x03); @@ -364,7 +364,7 @@ static int write_element_allow_id(VALUE key, VALUE value, VALUE extra, int allow memcpy(buffer->buffer + length_location, &obj_length, 4); break; } - if (strcmp(cls, "XGen::Mongo::Driver::Undefined") == 0) { + if (strcmp(cls, "Mongo::Undefined") == 0) { write_name_and_type(buffer, key, 0x0A); // just use nil type break; } @@ -736,25 +736,22 @@ static VALUE method_deserialize(VALUE self, VALUE bson) { } void Init_cbson() { - VALUE driver, CBson; + VALUE mongo, CBson; Time = rb_const_get(rb_cObject, rb_intern("Time")); - driver = rb_const_get(rb_const_get(rb_const_get(rb_cObject, - rb_intern("XGen")), - rb_intern("Mongo")), - rb_intern("Driver")); + mongo = rb_const_get(rb_cObject, rb_intern("Mongo")); rb_require("mongo/types/binary"); - Binary = rb_const_get(driver, rb_intern("Binary")); + Binary = rb_const_get(mongo, rb_intern("Binary")); rb_require("mongo/types/objectid"); - ObjectID = rb_const_get(driver, rb_intern("ObjectID")); + ObjectID = rb_const_get(mongo, rb_intern("ObjectID")); rb_require("mongo/types/dbref"); - DBRef = rb_const_get(driver, rb_intern("DBRef")); + DBRef = rb_const_get(mongo, rb_intern("DBRef")); rb_require("mongo/types/code"); - Code = rb_const_get(driver, rb_intern("Code")); + Code = rb_const_get(mongo, rb_intern("Code")); rb_require("mongo/types/regexp_of_holding"); - RegexpOfHolding = rb_const_get(driver, rb_intern("RegexpOfHolding")); + RegexpOfHolding = rb_const_get(mongo, rb_intern("RegexpOfHolding")); rb_require("mongo/errors"); - InvalidName = rb_const_get(driver, rb_intern("InvalidName")); + InvalidName = rb_const_get(mongo, rb_intern("InvalidName")); rb_require("mongo/util/ordered_hash"); OrderedHash = rb_const_get(rb_cObject, rb_intern("OrderedHash")); diff --git a/lib/mongo.rb b/lib/mongo.rb index 3aabfed..d86bb50 100644 --- a/lib/mongo.rb +++ b/lib/mongo.rb @@ -12,9 +12,7 @@ require 'mongo/cursor' require 'mongo/collection' require 'mongo/admin' -module XGen - module Mongo - ASCENDING = 1 - DESCENDING = -1 - end +module Mongo + ASCENDING = 1 + DESCENDING = -1 end diff --git a/lib/mongo/admin.rb b/lib/mongo/admin.rb index b1e50e6..25e57b5 100644 --- a/lib/mongo/admin.rb +++ b/lib/mongo/admin.rb @@ -16,72 +16,68 @@ require 'mongo/util/ordered_hash' -module XGen - module Mongo - module Driver +module Mongo - # Provide administrative database methods: those having to do with - # profiling and validation. - class Admin + # Provide administrative database methods: those having to do with + # profiling and validation. + class Admin - def initialize(db) - @db = db - end - - # Return the current database profiling level. - def profiling_level - oh = OrderedHash.new - oh[:profile] = -1 - doc = @db.db_command(oh) - raise "Error with profile command: #{doc.inspect}" unless @db.ok?(doc) && doc['was'].kind_of?(Numeric) - case doc['was'].to_i - when 0 - :off - when 1 - :slow_only - when 2 - :all - else - raise "Error: illegal profiling level value #{doc['was']}" - end - end - - # Set database profiling level to :off, :slow_only, or :all. - def profiling_level=(level) - oh = OrderedHash.new - oh[:profile] = case level - when :off - 0 - when :slow_only - 1 - when :all - 2 - else - raise "Error: illegal profiling level value #{level}" - end - doc = @db.db_command(oh) - raise "Error with profile command: #{doc.inspect}" unless @db.ok?(doc) - end - - # Return an array contining current profiling information from the - # database. - def profiling_info - @db.query(Collection.new(@db, DB::SYSTEM_PROFILE_COLLECTION), Query.new({})).to_a - end - - # Validate a named collection by raising an exception if there is a - # problem or returning an interesting hash (see especially the - # 'result' string value) if all is well. - def validate_collection(name) - doc = @db.db_command(:validate => name) - raise "Error with validate command: #{doc.inspect}" unless @db.ok?(doc) - result = doc['result'] - raise "Error with validation data: #{doc.inspect}" unless result.kind_of?(String) - raise "Error: invalid collection #{name}: #{doc.inspect}" if result =~ /\b(exception|corrupt)\b/i - doc - end + def initialize(db) + @db = db + end + # Return the current database profiling level. + def profiling_level + oh = OrderedHash.new + oh[:profile] = -1 + doc = @db.db_command(oh) + raise "Error with profile command: #{doc.inspect}" unless @db.ok?(doc) && doc['was'].kind_of?(Numeric) + case doc['was'].to_i + when 0 + :off + when 1 + :slow_only + when 2 + :all + else + raise "Error: illegal profiling level value #{doc['was']}" end end + + # Set database profiling level to :off, :slow_only, or :all. + def profiling_level=(level) + oh = OrderedHash.new + oh[:profile] = case level + when :off + 0 + when :slow_only + 1 + when :all + 2 + else + raise "Error: illegal profiling level value #{level}" + end + doc = @db.db_command(oh) + raise "Error with profile command: #{doc.inspect}" unless @db.ok?(doc) + end + + # Return an array contining current profiling information from the + # database. + def profiling_info + @db.query(Collection.new(@db, DB::SYSTEM_PROFILE_COLLECTION), Query.new({})).to_a + end + + # Validate a named collection by raising an exception if there is a + # problem or returning an interesting hash (see especially the + # 'result' string value) if all is well. + def validate_collection(name) + doc = @db.db_command(:validate => name) + raise "Error with validate command: #{doc.inspect}" unless @db.ok?(doc) + result = doc['result'] + raise "Error with validation data: #{doc.inspect}" unless result.kind_of?(String) + raise "Error: invalid collection #{name}: #{doc.inspect}" if result =~ /\b(exception|corrupt)\b/i + doc + end + end end diff --git a/lib/mongo/collection.rb b/lib/mongo/collection.rb index f588096..2cfecfb 100644 --- a/lib/mongo/collection.rb +++ b/lib/mongo/collection.rb @@ -16,320 +16,318 @@ require 'mongo/query' -module XGen - module Mongo - module Driver +module Mongo - # A named collection of records in a database. - class Collection + # A named collection of records in a database. + class Collection - attr_reader :db, :name, :hint + attr_reader :db, :name, :hint - def initialize(db, name) - case name - when Symbol, String - else - raise TypeError, "new_name must be a string or symbol" - end + def initialize(db, name) + case name + when Symbol, String + else + raise TypeError, "new_name must be a string or symbol" + end - name = name.to_s + name = name.to_s - if name.empty? or name.include? ".." - raise InvalidName, "collection names cannot be empty" - end - if name.include? "$" and not name.match(/^\$cmd/) - raise InvalidName, "collection names must not contain '$'" - end - if name.match(/^\./) or name.match(/\.$/) - raise InvalidName, "collection names must not start or end with '.'" - end + if name.empty? or name.include? ".." + raise InvalidName, "collection names cannot be empty" + end + if name.include? "$" and not name.match(/^\$cmd/) + raise InvalidName, "collection names must not contain '$'" + end + if name.match(/^\./) or name.match(/\.$/) + raise InvalidName, "collection names must not start or end with '.'" + end - @db, @name = db, name - @hint = nil + @db, @name = db, name + @hint = nil + end + + # Get a sub-collection of this collection by name. + # + # Raises InvalidName if an invalid collection name is used. + # + # :name :: the name of the collection to get + def [](name) + name = "#{self.name}.#{name}" + return Collection.new(self, name) if !db.strict? || db.collection_names.include?(name) + raise "Collection #{name} doesn't exist. Currently in strict mode." + end + + # Set hint fields to use and return +self+. hint may be a single field + # name, array of field names, or a hash (preferably an OrderedHash). + # May be +nil+. + def hint=(hint) + @hint = normalize_hint_fields(hint) + self + end + + # Query the database. + # + # The +selector+ argument is a prototype document that all results must + # match. For example: + # + # collection.find({"hello" => "world"}) + # + # only matches documents that have a key "hello" with value "world". + # Matches can have other keys *in addition* to "hello". + # + # If given an optional block +find+ will yield a Cursor to that block, + # close the cursor, and then return nil. This guarantees that partially + # evaluated cursors will be closed. If given no block +find+ returns a + # cursor. + # + # :selector :: A document (hash) specifying elements which must be + # present for a document to be included in the result set. + # + # Options: + # :fields :: Array of field names that should be returned in the result + # set ("_id" will always be included). By limiting results + # to a certain subset of fields you can cut down on network + # traffic and decoding time. + # :offset :: Start at this record when returning records + # :limit :: Maximum number of records to return + # :sort :: Either hash of field names as keys and 1/-1 as values; 1 == + # ascending, -1 == descending, or array of field names (all + # assumed to be sorted in ascending order). + # :hint :: See #hint. This option overrides the collection-wide value. + # :snapshot :: If true, snapshot mode will be used for this query. + # Snapshot mode assures no duplicates are returned, or + # objects missed, which were preset at both the start and + # end of the query's execution. For details see + # http://www.mongodb.org/display/DOCS/How+to+do+Snapshotting+in+the+Mongo+Database + def find(selector={}, options={}) + fields = options.delete(:fields) + fields = ["_id"] if fields && fields.empty? + offset = options.delete(:offset) || 0 + limit = options.delete(:limit) || 0 + sort = options.delete(:sort) + hint = options.delete(:hint) + snapshot = options.delete(:snapshot) + if hint + hint = normalize_hint_fields(hint) + else + hint = @hint # assumed to be normalized already + end + raise RuntimeError, "Unknown options [#{options.inspect}]" unless options.empty? + + cursor = @db.query(self, Query.new(selector, fields, offset, limit, sort, hint, snapshot)) + if block_given? + yield cursor + cursor.close() + nil + else + cursor + end + end + + # Get a single object from the database. + # + # Raises TypeError if the argument is of an improper type. Returns a + # single document (hash), or nil if no result is found. + # + # :spec_or_object_id :: a hash specifying elements which must be + # present for a document to be included in the result set OR an + # instance of ObjectID to be used as the value for an _id query. + # if nil an empty spec, {}, will be used. + # :options :: options, as passed to Collection#find + def find_one(spec_or_object_id=nil, options={}) + spec = case spec_or_object_id + when nil + {} + when ObjectID + {:_id => spec_or_object_id} + when Hash + spec_or_object_id + else + raise TypeError, "spec_or_object_id must be an instance of ObjectID or Hash, or nil" + end + find(spec, options.merge(:limit => -1)).next_object + end + + # DEPRECATED - use find_one instead + # + # Find the first record that matches +selector+. See #find. + def find_first(selector={}, options={}) + warn "Collection#find_first is deprecated and will be removed. Please use Collection#find_one instead." + find_one(selector, options) + end + + # Save a document in this collection. + # + # If +to_save+ already has an '_id' then an update (upsert) operation + # is performed and any existing document with that _id is overwritten. + # Otherwise an insert operation is performed. Returns the _id of the + # saved document. + # + # :to_save :: the document (a hash) to be saved + # + # Options: + # :safe :: if true, check that the save succeeded. OperationFailure + # will be raised on an error. Checking for safety requires an extra + # round-trip to the database + def save(to_save, options={}) + if id = to_save[:_id] || to_save['_id'] + update({:_id => id}, to_save, :upsert => true, :safe => options.delete(:safe)) + id + else + insert(to_save, :safe => options.delete(:safe)) + end + end + + # Insert a document(s) into this collection. + # + # "<<" is aliased to this method. Returns the _id of the inserted + # document or a list of _ids of the inserted documents. The object(s) + # may have been modified by the database's PK factory, if it has one. + # + # :doc_or_docs :: a document (as a hash) or Array of documents to be + # inserted + # + # Options: + # :safe :: if true, check that the insert succeeded. OperationFailure + # will be raised on an error. Checking for safety requires an extra + # round-trip to the database + def insert(doc_or_docs, options={}) + doc_or_docs = [doc_or_docs] if !doc_or_docs.is_a?(Array) + res = @db.insert_into_db(@name, doc_or_docs) + if options.delete(:safe) + error = @db.error + if error + raise OperationFailure, error end + end + res.size > 1 ? res : res.first + end + alias_method :<<, :insert - # Get a sub-collection of this collection by name. - # - # Raises InvalidName if an invalid collection name is used. - # - # :name :: the name of the collection to get - def [](name) - name = "#{self.name}.#{name}" - return Collection.new(self, name) if !db.strict? || db.collection_names.include?(name) - raise "Collection #{name} doesn't exist. Currently in strict mode." + # Remove the records that match +selector+. + def remove(selector={}) + @db.remove_from_db(@name, selector) + end + + # Remove all records. + def clear + remove({}) + end + + # DEPRECATED - use update(... :upsert => true) instead + # + # Update records that match +selector+ by applying +obj+ as an update. + # If no match, inserts (???). + def repsert(selector, obj) + warn "Collection#repsert is deprecated and will be removed. Please use Collection#update instead." + update(selector, obj, :upsert => true) + end + + # DEPRECATED - use update(... :upsert => false) instead + # + # Update records that match +selector+ by applying +obj+ as an update. + def replace(selector, obj) + warn "Collection#replace is deprecated and will be removed. Please use Collection#update instead." + update(selector, obj) + end + + # DEPRECATED - use update(... :upsert => false) instead + # + # Update records that match +selector+ by applying +obj+ as an update. + # Both +selector+ and +modifier_obj+ are required. + def modify(selector, modifier_obj) + warn "Collection#modify is deprecated and will be removed. Please use Collection#update instead." + update(selector, modifier_obj) + end + + # Update a document(s) in this collection. + # + # :spec :: a hash specifying elements which must be present for + # a document to be updated + # :document :: a hash specifying the fields to be changed in the + # selected document(s), or (in the case of an upsert) the document to + # be inserted + # + # Options: + # :upsert :: if true, perform an upsert operation + # :safe :: if true, check that the update succeeded. OperationFailure + # will be raised on an error. Checking for safety requires an extra + # round-trip to the database + def update(spec, document, options={}) + upsert = options.delete(:upsert) + safe = options.delete(:safe) + + if upsert + @db.repsert_in_db(@name, spec, document) + else + @db.replace_in_db(@name, spec, document) + end + if safe + error = @db.error + if error + raise OperationFailure, error end + end + end - # Set hint fields to use and return +self+. hint may be a single field - # name, array of field names, or a hash (preferably an OrderedHash). - # May be +nil+. - def hint=(hint) - @hint = normalize_hint_fields(hint) - self + # Create a new index. +field_or_spec+ + # should be either a single field name or a Array of [field name, + # direction] pairs. Directions should be specified as + # Mongo::ASCENDING or Mongo::DESCENDING. + # +unique+ is an optional boolean indicating whether this index + # should enforce a uniqueness constraint. + def create_index(field_or_spec, unique=false) + @db.create_index(@name, field_or_spec, unique) + end + + # Drop index +name+. + def drop_index(name) + @db.drop_index(@name, name) + end + + # Drop all indexes. + def drop_indexes + # just need to call drop indexes with no args; will drop them all + @db.drop_index(@name, '*') + end + + # Drop the entire collection. USE WITH CAUTION. + def drop + @db.drop_collection(@name) + end + + # Perform a query similar to an SQL group by operation. + # + # Returns an array of grouped items. + # + # :keys :: Array of fields to group by + # :condition :: specification of rows to be considered (as a 'find' + # query specification) + # :initial :: initial value of the aggregation counter object + # :reduce :: aggregation function as a JavaScript string + # :command :: if true, run the group as a command instead of in an + # eval - it is likely that this option will eventually be + # deprecated and all groups will be run as commands + def group(keys, condition, initial, reduce, command=false) + if command + hash = {} + keys.each do |k| + hash[k] = 1 end - - # Query the database. - # - # The +selector+ argument is a prototype document that all results must - # match. For example: - # - # collection.find({"hello" => "world"}) - # - # only matches documents that have a key "hello" with value "world". - # Matches can have other keys *in addition* to "hello". - # - # If given an optional block +find+ will yield a Cursor to that block, - # close the cursor, and then return nil. This guarantees that partially - # evaluated cursors will be closed. If given no block +find+ returns a - # cursor. - # - # :selector :: A document (hash) specifying elements which must be - # present for a document to be included in the result set. - # - # Options: - # :fields :: Array of field names that should be returned in the result - # set ("_id" will always be included). By limiting results - # to a certain subset of fields you can cut down on network - # traffic and decoding time. - # :offset :: Start at this record when returning records - # :limit :: Maximum number of records to return - # :sort :: Either hash of field names as keys and 1/-1 as values; 1 == - # ascending, -1 == descending, or array of field names (all - # assumed to be sorted in ascending order). - # :hint :: See #hint. This option overrides the collection-wide value. - # :snapshot :: If true, snapshot mode will be used for this query. - # Snapshot mode assures no duplicates are returned, or - # objects missed, which were preset at both the start and - # end of the query's execution. For details see - # http://www.mongodb.org/display/DOCS/How+to+do+Snapshotting+in+the+Mongo+Database - def find(selector={}, options={}) - fields = options.delete(:fields) - fields = ["_id"] if fields && fields.empty? - offset = options.delete(:offset) || 0 - limit = options.delete(:limit) || 0 - sort = options.delete(:sort) - hint = options.delete(:hint) - snapshot = options.delete(:snapshot) - if hint - hint = normalize_hint_fields(hint) - else - hint = @hint # assumed to be normalized already - end - raise RuntimeError, "Unknown options [#{options.inspect}]" unless options.empty? - - cursor = @db.query(self, Query.new(selector, fields, offset, limit, sort, hint, snapshot)) - if block_given? - yield cursor - cursor.close() - nil - else - cursor - end + result = @db.db_command({"group" => + { + "ns" => @name, + "$reduce" => Code.new(reduce), + "key" => hash, + "cond" => condition, + "initial" => initial}}) + if result["ok"] == 1 + return result["retval"] + else + raise OperationFailure, "group command failed: #{result['errmsg']}" end - - # Get a single object from the database. - # - # Raises TypeError if the argument is of an improper type. Returns a - # single document (hash), or nil if no result is found. - # - # :spec_or_object_id :: a hash specifying elements which must be - # present for a document to be included in the result set OR an - # instance of ObjectID to be used as the value for an _id query. - # if nil an empty spec, {}, will be used. - # :options :: options, as passed to Collection#find - def find_one(spec_or_object_id=nil, options={}) - spec = case spec_or_object_id - when nil - {} - when ObjectID - {:_id => spec_or_object_id} - when Hash - spec_or_object_id - else - raise TypeError, "spec_or_object_id must be an instance of ObjectID or Hash, or nil" - end - find(spec, options.merge(:limit => -1)).next_object - end - - # DEPRECATED - use find_one instead - # - # Find the first record that matches +selector+. See #find. - def find_first(selector={}, options={}) - warn "Collection#find_first is deprecated and will be removed. Please use Collection#find_one instead." - find_one(selector, options) - end - - # Save a document in this collection. - # - # If +to_save+ already has an '_id' then an update (upsert) operation - # is performed and any existing document with that _id is overwritten. - # Otherwise an insert operation is performed. Returns the _id of the - # saved document. - # - # :to_save :: the document (a hash) to be saved - # - # Options: - # :safe :: if true, check that the save succeeded. OperationFailure - # will be raised on an error. Checking for safety requires an extra - # round-trip to the database - def save(to_save, options={}) - if id = to_save[:_id] || to_save['_id'] - update({:_id => id}, to_save, :upsert => true, :safe => options.delete(:safe)) - id - else - insert(to_save, :safe => options.delete(:safe)) - end - end - - # Insert a document(s) into this collection. - # - # "<<" is aliased to this method. Returns the _id of the inserted - # document or a list of _ids of the inserted documents. The object(s) - # may have been modified by the database's PK factory, if it has one. - # - # :doc_or_docs :: a document (as a hash) or Array of documents to be - # inserted - # - # Options: - # :safe :: if true, check that the insert succeeded. OperationFailure - # will be raised on an error. Checking for safety requires an extra - # round-trip to the database - def insert(doc_or_docs, options={}) - doc_or_docs = [doc_or_docs] if !doc_or_docs.is_a?(Array) - res = @db.insert_into_db(@name, doc_or_docs) - if options.delete(:safe) - error = @db.error - if error - raise OperationFailure, error - end - end - res.size > 1 ? res : res.first - end - alias_method :<<, :insert - - # Remove the records that match +selector+. - def remove(selector={}) - @db.remove_from_db(@name, selector) - end - - # Remove all records. - def clear - remove({}) - end - - # DEPRECATED - use update(... :upsert => true) instead - # - # Update records that match +selector+ by applying +obj+ as an update. - # If no match, inserts (???). - def repsert(selector, obj) - warn "Collection#repsert is deprecated and will be removed. Please use Collection#update instead." - update(selector, obj, :upsert => true) - end - - # DEPRECATED - use update(... :upsert => false) instead - # - # Update records that match +selector+ by applying +obj+ as an update. - def replace(selector, obj) - warn "Collection#replace is deprecated and will be removed. Please use Collection#update instead." - update(selector, obj) - end - - # DEPRECATED - use update(... :upsert => false) instead - # - # Update records that match +selector+ by applying +obj+ as an update. - # Both +selector+ and +modifier_obj+ are required. - def modify(selector, modifier_obj) - warn "Collection#modify is deprecated and will be removed. Please use Collection#update instead." - update(selector, modifier_obj) - end - - # Update a document(s) in this collection. - # - # :spec :: a hash specifying elements which must be present for - # a document to be updated - # :document :: a hash specifying the fields to be changed in the - # selected document(s), or (in the case of an upsert) the document to - # be inserted - # - # Options: - # :upsert :: if true, perform an upsert operation - # :safe :: if true, check that the update succeeded. OperationFailure - # will be raised on an error. Checking for safety requires an extra - # round-trip to the database - def update(spec, document, options={}) - upsert = options.delete(:upsert) - safe = options.delete(:safe) - - if upsert - @db.repsert_in_db(@name, spec, document) - else - @db.replace_in_db(@name, spec, document) - end - if safe - error = @db.error - if error - raise OperationFailure, error - end - end - end - - # Create a new index. +field_or_spec+ - # should be either a single field name or a Array of [field name, - # direction] pairs. Directions should be specified as - # XGen::Mongo::ASCENDING or XGen::Mongo::DESCENDING. - # +unique+ is an optional boolean indicating whether this index - # should enforce a uniqueness constraint. - def create_index(field_or_spec, unique=false) - @db.create_index(@name, field_or_spec, unique) - end - - # Drop index +name+. - def drop_index(name) - @db.drop_index(@name, name) - end - - # Drop all indexes. - def drop_indexes - # just need to call drop indexes with no args; will drop them all - @db.drop_index(@name, '*') - end - - # Drop the entire collection. USE WITH CAUTION. - def drop - @db.drop_collection(@name) - end - - # Perform a query similar to an SQL group by operation. - # - # Returns an array of grouped items. - # - # :keys :: Array of fields to group by - # :condition :: specification of rows to be considered (as a 'find' - # query specification) - # :initial :: initial value of the aggregation counter object - # :reduce :: aggregation function as a JavaScript string - # :command :: if true, run the group as a command instead of in an - # eval - it is likely that this option will eventually be - # deprecated and all groups will be run as commands - def group(keys, condition, initial, reduce, command=false) - if command - hash = {} - keys.each do |k| - hash[k] = 1 - end - result = @db.db_command({"group" => - { - "ns" => @name, - "$reduce" => Code.new(reduce), - "key" => hash, - "cond" => condition, - "initial" => initial}}) - if result["ok"] == 1 - return result["retval"] - else - raise OperationFailure, "group command failed: #{result['errmsg']}" - end - end - group_function = < @name, - "keys" => keys, - "condition" => condition, - "initial" => initial - }))["result"] - end + return @db.eval(Code.new(group_function, + { + "ns" => @name, + "keys" => keys, + "condition" => condition, + "initial" => initial + }))["result"] + end - # Rename this collection. - # - # If operating in auth mode, client must be authorized as an admin to - # perform this operation. Raises +InvalidName+ if +new_name+ is an invalid - # collection name. - # - # :new_name :: new name for this collection - def rename(new_name) - case new_name - when Symbol, String - else - raise TypeError, "new_name must be a string or symbol" - end + # Rename this collection. + # + # If operating in auth mode, client must be authorized as an admin to + # perform this operation. Raises +InvalidName+ if +new_name+ is an invalid + # collection name. + # + # :new_name :: new name for this collection + def rename(new_name) + case new_name + when Symbol, String + else + raise TypeError, "new_name must be a string or symbol" + end - new_name = new_name.to_s + new_name = new_name.to_s - if new_name.empty? or new_name.include? ".." - raise InvalidName, "collection names cannot be empty" - end - if new_name.include? "$" - raise InvalidName, "collection names must not contain '$'" - end - if new_name.match(/^\./) or new_name.match(/\.$/) - raise InvalidName, "collection names must not start or end with '.'" - end + if new_name.empty? or new_name.include? ".." + raise InvalidName, "collection names cannot be empty" + end + if new_name.include? "$" + raise InvalidName, "collection names must not contain '$'" + end + if new_name.match(/^\./) or new_name.match(/\.$/) + raise InvalidName, "collection names must not start or end with '.'" + end - @db.rename_collection(@name, new_name) - end + @db.rename_collection(@name, new_name) + end - # Get information on the indexes for the collection +collection_name+. - # Returns a hash where the keys are index names (as returned by - # Collection#create_index and the values are lists of [key, direction] - # pairs specifying the index (as passed to Collection#create_index). - def index_information - @db.index_information(@name) - end + # Get information on the indexes for the collection +collection_name+. + # Returns a hash where the keys are index names (as returned by + # Collection#create_index and the values are lists of [key, direction] + # pairs specifying the index (as passed to Collection#create_index). + def index_information + @db.index_information(@name) + end - # Return a hash containing options that apply to this collection. - # 'create' will be the collection name. For the other possible keys - # and values, see DB#create_collection. - def options - @db.collections_info(@name).next_object()['options'] - end + # Return a hash containing options that apply to this collection. + # 'create' will be the collection name. For the other possible keys + # and values, see DB#create_collection. + def options + @db.collections_info(@name).next_object()['options'] + end - # Get the number of documents in this collection. - # - # Specifying a +selector+ is DEPRECATED and will be removed. Please use - # find(selector).count() instead. - def count(selector=nil) - if selector - warn "specifying a selector for Collection#count is deprecated and will be removed. Please use Collection.find(selector).count instead." - end - find(selector || {}).count() - end + # Get the number of documents in this collection. + # + # Specifying a +selector+ is DEPRECATED and will be removed. Please use + # find(selector).count() instead. + def count(selector=nil) + if selector + warn "specifying a selector for Collection#count is deprecated and will be removed. Please use Collection.find(selector).count instead." + end + find(selector || {}).count() + end - protected + protected - def normalize_hint_fields(hint) - case hint - when String - {hint => 1} - when Hash - hint - when nil - nil - else - h = OrderedHash.new - hint.to_a.each { |k| h[k] = 1 } - h - end - end + def normalize_hint_fields(hint) + case hint + when String + {hint => 1} + when Hash + hint + when nil + nil + else + h = OrderedHash.new + hint.to_a.each { |k| h[k] = 1 } + h end end end end - diff --git a/lib/mongo/cursor.rb b/lib/mongo/cursor.rb index df5ee73..bdbb299 100644 --- a/lib/mongo/cursor.rb +++ b/lib/mongo/cursor.rb @@ -18,232 +18,228 @@ require 'mongo/message' require 'mongo/util/byte_buffer' require 'mongo/util/bson' -module XGen - module Mongo - module Driver +module Mongo - # A cursor over query results. Returned objects are hashes. - class Cursor + # A cursor over query results. Returned objects are hashes. + class Cursor - include Enumerable + include Enumerable - RESPONSE_HEADER_SIZE = 20 + RESPONSE_HEADER_SIZE = 20 - attr_reader :db, :collection, :query + attr_reader :db, :collection, :query - def initialize(db, collection, query, admin=false) - @db, @collection, @query, @admin = db, collection, query, admin - @num_to_return = @query.number_to_return || 0 - @cache = [] - @closed = false - @can_call_to_a = true - @query_run = false - @rows = nil - end + def initialize(db, collection, query, admin=false) + @db, @collection, @query, @admin = db, collection, query, admin + @num_to_return = @query.number_to_return || 0 + @cache = [] + @closed = false + @can_call_to_a = true + @query_run = false + @rows = nil + end - def closed?; @closed; end + def closed?; @closed; end - # Internal method, not for general use. Return +true+ if there are - # more records to retrieve. We do not check @num_to_return; #each is - # responsible for doing that. - def more? - num_remaining > 0 - end + # Internal method, not for general use. Return +true+ if there are + # more records to retrieve. We do not check @num_to_return; #each is + # responsible for doing that. + def more? + num_remaining > 0 + end - # Return the next object or nil if there are no more. Raises an error - # if necessary. - def next_object - refill_via_get_more if num_remaining == 0 - o = @cache.shift + # Return the next object or nil if there are no more. Raises an error + # if necessary. + def next_object + refill_via_get_more if num_remaining == 0 + o = @cache.shift - if o && o['$err'] - err = o['$err'] + if o && o['$err'] + err = o['$err'] - # If the server has stopped being the master (e.g., it's one of a - # pair but it has died or something like that) then we close that - # connection. If the db has auto connect option and a pair of - # servers, next request will re-open on master server. - @db.close if err == "not master" + # If the server has stopped being the master (e.g., it's one of a + # pair but it has died or something like that) then we close that + # connection. If the db has auto connect option and a pair of + # servers, next request will re-open on master server. + @db.close if err == "not master" - raise err - end - - o - end - - # Get the size of the results set for this query. - # - # Returns the number of objects in the results set for this query. Does - # not take limit and skip into account. Raises OperationFailure on a - # database error. - def count - command = OrderedHash["count", @collection.name, - "query", @query.selector] - response = @db.db_command(command) - return response['n'].to_i if response['ok'] == 1 - return 0 if response['errmsg'] == "ns missing" - raise OperationFailure, "Count failed: #{response['errmsg']}" - end - - # Iterate over each object, yielding it to the given block. At most - # @num_to_return records are returned (or all of them, if - # @num_to_return is 0). - # - # If #to_a has already been called then this method uses the array - # that we store internally. In that case, #each can be called multiple - # times because it re-uses that array. - # - # You can call #each after calling #to_a (multiple times even, because - # it will use the internally-stored array), but you can't call #to_a - # after calling #each unless you also called it before calling #each. - # If you try to do that, an error will be raised. - def each - if @rows # Already turned into an array - @rows.each { |row| yield row } - else - num_returned = 0 - while more? && (@num_to_return <= 0 || num_returned < @num_to_return) - yield next_object() - num_returned += 1 - end - @can_call_to_a = false - end - end - - # Return all of the rows (up to the +num_to_return+ value specified in - # #new) as an array. Calling this multiple times will work fine; it - # always returns the same array. - # - # Don't use this if you're expecting large amounts of data, of course. - # All of the returned rows are kept in an array stored in this object - # so it can be reused. - # - # You can call #each after calling #to_a (multiple times even, because - # it will use the internally-stored array), but you can't call #to_a - # after calling #each unless you also called it before calling #each. - # If you try to do that, an error will be raised. - def to_a - return @rows if @rows - raise "can't call Cursor#to_a after calling Cursor#each" unless @can_call_to_a - @rows = [] - num_returned = 0 - while more? && (@num_to_return <= 0 || num_returned < @num_to_return) - @rows << next_object() - num_returned += 1 - end - @rows - end - - # Returns an explain plan record. - def explain - old_val = @query.explain - @query.explain = true - - c = Cursor.new(@db, @collection, @query) - explanation = c.next_object - c.close - - @query.explain = old_val - explanation - end - - # Close the cursor. - # - # Note: if a cursor is read until exhausted (read until OP_QUERY or - # OP_GETMORE returns zero for the cursor id), there is no need to - # close it by calling this method. - def close - @db.send_to_db(KillCursorsMessage.new(@cursor_id)) if @cursor_id - @cache = [] - @cursor_id = 0 - @closed = true - end - - protected - - def read_all - read_message_header - read_response_header - read_objects_off_wire - end - - def read_objects_off_wire - while doc = next_object_on_wire - @cache << doc - end - end - - def read_message_header - MessageHeader.new.read_header(@db) - end - - def read_response_header - header_buf = ByteBuffer.new - header_buf.put_array(@db.receive_full(RESPONSE_HEADER_SIZE).unpack("C*")) - raise "Short read for DB response header; expected #{RESPONSE_HEADER_SIZE} bytes, saw #{header_buf.length}" unless header_buf.length == RESPONSE_HEADER_SIZE - header_buf.rewind - @result_flags = header_buf.get_int - @cursor_id = header_buf.get_long - @starting_from = header_buf.get_int - @n_returned = header_buf.get_int - @n_remaining = @n_returned - end - - def num_remaining - refill_via_get_more if @cache.length == 0 - @cache.length - end - - private - - def next_object_on_wire - send_query_if_needed - # if @n_remaining is 0 but we have a non-zero cursor, there are more - # to fetch, so do a GetMore operation, but don't do it here - do it - # when someone pulls an object out of the cache and it's empty - return nil if @n_remaining == 0 - object_from_stream - end - - def refill_via_get_more - if send_query_if_needed or @cursor_id == 0 - return - end - @db._synchronize { - @db.send_to_db(GetMoreMessage.new(@admin ? 'admin' : @db.name, @collection.name, @cursor_id)) - read_all - } - end - - def object_from_stream - buf = ByteBuffer.new - buf.put_array(@db.receive_full(4).unpack("C*")) - buf.rewind - size = buf.get_int - buf.put_array(@db.receive_full(size - 4).unpack("C*"), 4) - @n_remaining -= 1 - buf.rewind - BSON.new.deserialize(buf) - end - - def send_query_if_needed - # Run query first time we request an object from the wire - if @query_run - false - else - @db._synchronize { - @db.send_query_message(QueryMessage.new(@admin ? 'admin' : @db.name, @collection.name, @query)) - @query_run = true - read_all - } - true - end - end - - def to_s - "DBResponse(flags=#@result_flags, cursor_id=#@cursor_id, start=#@starting_from, n_returned=#@n_returned)" - end + raise err end + + o + end + + # Get the size of the results set for this query. + # + # Returns the number of objects in the results set for this query. Does + # not take limit and skip into account. Raises OperationFailure on a + # database error. + def count + command = OrderedHash["count", @collection.name, + "query", @query.selector] + response = @db.db_command(command) + return response['n'].to_i if response['ok'] == 1 + return 0 if response['errmsg'] == "ns missing" + raise OperationFailure, "Count failed: #{response['errmsg']}" + end + + # Iterate over each object, yielding it to the given block. At most + # @num_to_return records are returned (or all of them, if + # @num_to_return is 0). + # + # If #to_a has already been called then this method uses the array + # that we store internally. In that case, #each can be called multiple + # times because it re-uses that array. + # + # You can call #each after calling #to_a (multiple times even, because + # it will use the internally-stored array), but you can't call #to_a + # after calling #each unless you also called it before calling #each. + # If you try to do that, an error will be raised. + def each + if @rows # Already turned into an array + @rows.each { |row| yield row } + else + num_returned = 0 + while more? && (@num_to_return <= 0 || num_returned < @num_to_return) + yield next_object() + num_returned += 1 + end + @can_call_to_a = false + end + end + + # Return all of the rows (up to the +num_to_return+ value specified in + # #new) as an array. Calling this multiple times will work fine; it + # always returns the same array. + # + # Don't use this if you're expecting large amounts of data, of course. + # All of the returned rows are kept in an array stored in this object + # so it can be reused. + # + # You can call #each after calling #to_a (multiple times even, because + # it will use the internally-stored array), but you can't call #to_a + # after calling #each unless you also called it before calling #each. + # If you try to do that, an error will be raised. + def to_a + return @rows if @rows + raise "can't call Cursor#to_a after calling Cursor#each" unless @can_call_to_a + @rows = [] + num_returned = 0 + while more? && (@num_to_return <= 0 || num_returned < @num_to_return) + @rows << next_object() + num_returned += 1 + end + @rows + end + + # Returns an explain plan record. + def explain + old_val = @query.explain + @query.explain = true + + c = Cursor.new(@db, @collection, @query) + explanation = c.next_object + c.close + + @query.explain = old_val + explanation + end + + # Close the cursor. + # + # Note: if a cursor is read until exhausted (read until OP_QUERY or + # OP_GETMORE returns zero for the cursor id), there is no need to + # close it by calling this method. + def close + @db.send_to_db(KillCursorsMessage.new(@cursor_id)) if @cursor_id + @cache = [] + @cursor_id = 0 + @closed = true + end + + protected + + def read_all + read_message_header + read_response_header + read_objects_off_wire + end + + def read_objects_off_wire + while doc = next_object_on_wire + @cache << doc + end + end + + def read_message_header + MessageHeader.new.read_header(@db) + end + + def read_response_header + header_buf = ByteBuffer.new + header_buf.put_array(@db.receive_full(RESPONSE_HEADER_SIZE).unpack("C*")) + raise "Short read for DB response header; expected #{RESPONSE_HEADER_SIZE} bytes, saw #{header_buf.length}" unless header_buf.length == RESPONSE_HEADER_SIZE + header_buf.rewind + @result_flags = header_buf.get_int + @cursor_id = header_buf.get_long + @starting_from = header_buf.get_int + @n_returned = header_buf.get_int + @n_remaining = @n_returned + end + + def num_remaining + refill_via_get_more if @cache.length == 0 + @cache.length + end + + private + + def next_object_on_wire + send_query_if_needed + # if @n_remaining is 0 but we have a non-zero cursor, there are more + # to fetch, so do a GetMore operation, but don't do it here - do it + # when someone pulls an object out of the cache and it's empty + return nil if @n_remaining == 0 + object_from_stream + end + + def refill_via_get_more + if send_query_if_needed or @cursor_id == 0 + return + end + @db._synchronize { + @db.send_to_db(GetMoreMessage.new(@admin ? 'admin' : @db.name, @collection.name, @cursor_id)) + read_all + } + end + + def object_from_stream + buf = ByteBuffer.new + buf.put_array(@db.receive_full(4).unpack("C*")) + buf.rewind + size = buf.get_int + buf.put_array(@db.receive_full(size - 4).unpack("C*"), 4) + @n_remaining -= 1 + buf.rewind + BSON.new.deserialize(buf) + end + + def send_query_if_needed + # Run query first time we request an object from the wire + if @query_run + false + else + @db._synchronize { + @db.send_query_message(QueryMessage.new(@admin ? 'admin' : @db.name, @collection.name, @query)) + @query_run = true + read_all + } + true + end + end + + def to_s + "DBResponse(flags=#@result_flags, cursor_id=#@cursor_id, start=#@starting_from, n_returned=#@n_returned)" end end end diff --git a/lib/mongo/db.rb b/lib/mongo/db.rb index 450844a..abda9aa 100644 --- a/lib/mongo/db.rb +++ b/lib/mongo/db.rb @@ -23,542 +23,538 @@ require 'mongo/query' require 'mongo/util/ordered_hash.rb' require 'mongo/admin' -module XGen - module Mongo - module Driver +module Mongo - # A Mongo database. - class DB + # A Mongo database. + class DB - SYSTEM_NAMESPACE_COLLECTION = "system.namespaces" - SYSTEM_INDEX_COLLECTION = "system.indexes" - SYSTEM_PROFILE_COLLECTION = "system.profile" - SYSTEM_USER_COLLECTION = "system.users" - SYSTEM_COMMAND_COLLECTION = "$cmd" + SYSTEM_NAMESPACE_COLLECTION = "system.namespaces" + SYSTEM_INDEX_COLLECTION = "system.indexes" + SYSTEM_PROFILE_COLLECTION = "system.profile" + SYSTEM_USER_COLLECTION = "system.users" + SYSTEM_COMMAND_COLLECTION = "$cmd" - # Strict mode enforces collection existence checks. When +true+, - # asking for a collection that does not exist or trying to create a - # collection that already exists raises an error. - # - # Strict mode is off (+false+) by default. Its value can be changed at - # any time. - attr_writer :strict + # Strict mode enforces collection existence checks. When +true+, + # asking for a collection that does not exist or trying to create a + # collection that already exists raises an error. + # + # Strict mode is off (+false+) by default. Its value can be changed at + # any time. + attr_writer :strict - # Returns the value of the +strict+ flag. - def strict?; @strict; end + # Returns the value of the +strict+ flag. + def strict?; @strict; end - # The name of the database. - attr_reader :name + # The name of the database. + attr_reader :name - # Host to which we are currently connected. - attr_reader :host - # Port to which we are currently connected. - attr_reader :port + # Host to which we are currently connected. + attr_reader :host + # Port to which we are currently connected. + attr_reader :port - # An array of [host, port] pairs. - attr_reader :nodes + # An array of [host, port] pairs. + attr_reader :nodes - # The database's socket. For internal (and Cursor) use only. - attr_reader :socket + # The database's socket. For internal (and Cursor) use only. + attr_reader :socket - def slave_ok?; @slave_ok; end - def auto_reconnect?; @auto_reconnect; end + def slave_ok?; @slave_ok; end + def auto_reconnect?; @auto_reconnect; end - # A primary key factory object (or +nil+). See the README.doc file or - # DB#new for details. - attr_reader :pk_factory + # A primary key factory object (or +nil+). See the README.doc file or + # DB#new for details. + attr_reader :pk_factory - def pk_factory=(pk_factory) - raise "error: can not change PK factory" if @pk_factory - @pk_factory = pk_factory - end + def pk_factory=(pk_factory) + raise "error: can not change PK factory" if @pk_factory + @pk_factory = pk_factory + end - # Instances of DB are normally obtained by calling Mongo#db. - # - # db_name :: The database name - # - # nodes :: An array of [host, port] pairs. See Mongo#new, which offers - # a more flexible way of defining nodes. - # - # options :: A hash of options. - # - # Options: - # - # :strict :: If true, collections must exist to be accessed and must - # not exist to be created. See #collection and - # #create_collection. - # - # :pk :: A primary key factory object that must respond to :create_pk, - # which should take a hash and return a hash which merges the - # original hash with any primary key fields the factory wishes - # to inject. (NOTE: if the object already has a primary key, - # the factory should not inject a new key; this means that the - # object is being used in a repsert but it already exists.) The - # idea here is that when ever a record is inserted, the :pk - # object's +create_pk+ method will be called and the new hash - # returned will be inserted. - # - # :slave_ok :: Only used if +nodes+ contains only one host/port. If - # false, when connecting to that host/port we check to - # see if the server is the master. If it is not, an error - # is thrown. - # - # :auto_reconnect :: If the connection gets closed (for example, we - # have a server pair and saw the "not master" - # error, which closes the connection), then - # automatically try to reconnect to the master or - # to the single server we have been given. Defaults - # to +false+. - # - # When a DB object first connects to a pair, it will find the master - # instance and connect to that one. On socket error or if we recieve a - # "not master" error, we again find the master of the pair. - def initialize(db_name, nodes, options={}) - case db_name - when Symbol, String - else - raise TypeError, "db_name must be a string or symbol" - end + # Instances of DB are normally obtained by calling Mongo#db. + # + # db_name :: The database name + # + # nodes :: An array of [host, port] pairs. See Mongo#new, which offers + # a more flexible way of defining nodes. + # + # options :: A hash of options. + # + # Options: + # + # :strict :: If true, collections must exist to be accessed and must + # not exist to be created. See #collection and + # #create_collection. + # + # :pk :: A primary key factory object that must respond to :create_pk, + # which should take a hash and return a hash which merges the + # original hash with any primary key fields the factory wishes + # to inject. (NOTE: if the object already has a primary key, + # the factory should not inject a new key; this means that the + # object is being used in a repsert but it already exists.) The + # idea here is that when ever a record is inserted, the :pk + # object's +create_pk+ method will be called and the new hash + # returned will be inserted. + # + # :slave_ok :: Only used if +nodes+ contains only one host/port. If + # false, when connecting to that host/port we check to + # see if the server is the master. If it is not, an error + # is thrown. + # + # :auto_reconnect :: If the connection gets closed (for example, we + # have a server pair and saw the "not master" + # error, which closes the connection), then + # automatically try to reconnect to the master or + # to the single server we have been given. Defaults + # to +false+. + # + # When a DB object first connects to a pair, it will find the master + # instance and connect to that one. On socket error or if we recieve a + # "not master" error, we again find the master of the pair. + def initialize(db_name, nodes, options={}) + case db_name + when Symbol, String + else + raise TypeError, "db_name must be a string or symbol" + end - [" ", ".", "$", "/", "\\"].each do |invalid_char| - if db_name.include? invalid_char - raise InvalidName, "database names cannot contain the character '#{invalid_char}'" - end - end - if db_name.empty? - raise InvalidName, "database name cannot be the empty string" - end - - @name, @nodes = db_name, nodes - @strict = options[:strict] - @pk_factory = options[:pk] - @slave_ok = options[:slave_ok] && @nodes.length == 1 # only OK if one node - @auto_reconnect = options[:auto_reconnect] - @semaphore = Object.new - @semaphore.extend Mutex_m - @socket = nil - connect_to_master - end - - def connect_to_master - close if @socket - @host = @port = nil - @nodes.detect { |hp| - @host, @port = *hp - begin - @socket = TCPSocket.new(@host, @port) - @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) - - # Check for master. Can't call master? because it uses mutex, - # which may already be in use during this call. - semaphore_is_locked = @semaphore.locked? - @semaphore.unlock if semaphore_is_locked - is_master = master? - @semaphore.lock if semaphore_is_locked - - break if @slave_ok || is_master - rescue SocketError, SystemCallError, IOError => ex - close if @socket - end - @socket - } - raise "error: failed to connect to any given host:port" unless @socket - end - - # Returns true if +username+ has +password+ in - # +SYSTEM_USER_COLLECTION+. +name+ is username, +password+ is - # plaintext password. - def authenticate(username, password) - doc = db_command(:getnonce => 1) - raise "error retrieving nonce: #{doc}" unless ok?(doc) - nonce = doc['nonce'] - - auth = OrderedHash.new - auth['authenticate'] = 1 - auth['user'] = username - auth['nonce'] = nonce - auth['key'] = Digest::MD5.hexdigest("#{nonce}#{username}#{hash_password(username, password)}") - ok?(db_command(auth)) - end - - # Deauthorizes use for this database for this connection. - def logout - doc = db_command(:logout => 1) - raise "error logging out: #{doc.inspect}" unless ok?(doc) - end - - # Returns an array of collection names in this database. - def collection_names - names = collections_info.collect { |doc| doc['name'] || '' } - names = names.delete_if {|name| name.index(@name).nil? || name.index('$')} - names.map {|name| name.sub(@name + '.', '')} - end - - # Returns a cursor over query result hashes. Each hash contains a - # 'name' string and optionally an 'options' hash. If +coll_name+ is - # specified, an array of length 1 is returned. - def collections_info(coll_name=nil) - selector = {} - selector[:name] = full_coll_name(coll_name) if coll_name - query(Collection.new(self, SYSTEM_NAMESPACE_COLLECTION), Query.new(selector)) - end - - # Create a collection. If +strict+ is false, will return existing or - # new collection. If +strict+ is true, will raise an error if - # collection +name+ already exists. - # - # Options is an optional hash: - # - # :capped :: Boolean. If not specified, capped is +false+. - # - # :size :: If +capped+ is +true+, specifies the maximum number of - # bytes. If +false+, specifies the initial extent of the - # collection. - # - # :max :: Max number of records in a capped collection. Optional. - def create_collection(name, options={}) - # First check existence - if collection_names.include?(name) - if strict? - raise "Collection #{name} already exists. Currently in strict mode." - else - return Collection.new(self, name) - end - end - - # Create new collection - oh = OrderedHash.new - oh[:create] = name - doc = db_command(oh.merge(options || {})) - ok = doc['ok'] - return Collection.new(self, name) if ok.kind_of?(Numeric) && (ok.to_i == 1 || ok.to_i == 0) - raise "Error creating collection: #{doc.inspect}" - end - - def admin - Admin.new(self) - end - - # Return a collection. If +strict+ is false, will return existing or - # new collection. If +strict+ is true, will raise an error if - # collection +name+ does not already exists. - def collection(name) - return Collection.new(self, name) if !strict? || collection_names.include?(name) - raise "Collection #{name} doesn't exist. Currently in strict mode." - end - alias_method :[], :collection - - # Drop collection +name+. Returns +true+ on success or if the - # collection does not exist, +false+ otherwise. - def drop_collection(name) - return true unless collection_names.include?(name) - - ok?(db_command(:drop => name)) - end - - # Returns the error message from the most recently executed database - # operation for this connection, or +nil+ if there was no error. - # - # Note: as of this writing, errors are only detected on the db server - # for certain kinds of operations (writes). The plan is to change this - # so that all operations will set the error if needed. - def error - doc = db_command(:getlasterror => 1) - raise "error retrieving last error: #{doc}" unless ok?(doc) - doc['err'] - end - - # Returns +true+ if an error was caused by the most recently executed - # database operation. - # - # Note: as of this writing, errors are only detected on the db server - # for certain kinds of operations (writes). The plan is to change this - # so that all operations will set the error if needed. - def error? - error != nil - end - - # Get the most recent error to have occured on this database - # - # Only returns errors that have occured since the last call to - # DB#reset_error_history - returns +nil+ if there is no such error. - def previous_error - error = db_command(:getpreverror => 1) - if error["err"] - error - else - nil - end - end - - # Reset the error history of this database - # - # Calls to DB#previous_error will only return errors that have occurred - # since the most recent call to this method. - def reset_error_history - db_command(:reseterror => 1) - end - - # Returns true if this database is a master (or is not paired with any - # other database), false if it is a slave. - def master? - doc = db_command(:ismaster => 1) - is_master = doc['ismaster'] - ok?(doc) && is_master.kind_of?(Numeric) && is_master.to_i == 1 - end - - # Returns a string of the form "host:port" that points to the master - # database. Works even if this is the master database. - def master - doc = db_command(:ismaster => 1) - is_master = doc['ismaster'] - raise "Error retrieving master database: #{doc.inspect}" unless ok?(doc) && is_master.kind_of?(Numeric) - case is_master.to_i - when 1 - "#@host:#@port" - else - doc['remote'] - end - end - - # Close the connection to the database. - def close - if @socket - s = @socket - @socket = nil - s.close - end - end - - def connected? - @socket != nil - end - - def receive_full(length) - message = "" - while message.length < length do - chunk = @socket.recv(length - message.length) - raise "connection closed" unless chunk.length > 0 - message += chunk - end - message - end - - # Send a MsgMessage to the database. - def send_message(msg) - send_to_db(MsgMessage.new(msg)) - end - - # Returns a Cursor over the query results. - # - # Note that the query gets sent lazily; the cursor calls - # #send_query_message when needed. If the caller never requests an - # object from the cursor, the query never gets sent. - def query(collection, query, admin=false) - Cursor.new(self, collection, query, admin) - end - - # Used by a Cursor to lazily send the query to the database. - def send_query_message(query_message) - send_to_db(query_message) - end - - # Remove the records that match +selector+ from +collection_name+. - # Normally called by Collection#remove or Collection#clear. - def remove_from_db(collection_name, selector) - _synchronize { - send_to_db(RemoveMessage.new(@name, collection_name, selector)) - } - end - - # Update records in +collection_name+ that match +selector+ by - # applying +obj+ as an update. Normally called by Collection#replace. - def replace_in_db(collection_name, selector, obj) - _synchronize { - send_to_db(UpdateMessage.new(@name, collection_name, selector, obj, false)) - } - end - - # DEPRECATED - use Collection#update instead - def modify_in_db(collection_name, selector, obj) - warn "DB#modify_in_db is deprecated and will be removed. Please use Collection#update instead." - replace_in_db(collection_name, selector, obj) - end - - # Update records in +collection_name+ that match +selector+ by - # applying +obj+ as an update. If no match, inserts (???). Normally - # called by Collection#repsert. - def repsert_in_db(collection_name, selector, obj) - _synchronize { - obj = @pk_factory.create_pk(obj) if @pk_factory - send_to_db(UpdateMessage.new(@name, collection_name, selector, obj, true)) - obj - } - end - - # DEPRECATED - use Collection.find(selector).count() instead - def count(collection_name, selector={}) - warn "DB#count is deprecated and will be removed. Please use Collection.find(selector).count instead." - collection(collection_name).find(selector).count() - end - - # Dereference a DBRef, getting the document it points to. - def dereference(dbref) - collection(dbref.namespace).find_one("_id" => dbref.object_id) - end - - # Evaluate a JavaScript expression on MongoDB. - # +code+ should be a string or Code instance containing a JavaScript - # expression. Additional arguments will be passed to that expression - # when it is run on the server. - def eval(code, *args) - if not code.is_a? Code - code = Code.new(code) - end - - oh = OrderedHash.new - oh[:$eval] = code - oh[:args] = args - doc = db_command(oh) - return doc['retval'] if ok?(doc) - raise OperationFailure, "eval failed: #{doc['errmsg']}" - end - - # Rename collection +from+ to +to+. Meant to be called by - # Collection#rename. - def rename_collection(from, to) - oh = OrderedHash.new - oh[:renameCollection] = "#{@name}.#{from}" - oh[:to] = "#{@name}.#{to}" - doc = db_command(oh, true) - raise "Error renaming collection: #{doc.inspect}" unless ok?(doc) - end - - # Drop index +name+ from +collection_name+. Normally called from - # Collection#drop_index or Collection#drop_indexes. - def drop_index(collection_name, name) - oh = OrderedHash.new - oh[:deleteIndexes] = collection_name - oh[:index] = name - doc = db_command(oh) - raise "Error with drop_index command: #{doc.inspect}" unless ok?(doc) - end - - # Get information on the indexes for the collection +collection_name+. - # Normally called by Collection#index_information. Returns a hash where - # the keys are index names (as returned by Collection#create_index and - # the values are lists of [key, direction] pairs specifying the index - # (as passed to Collection#create_index). - def index_information(collection_name) - sel = {:ns => full_coll_name(collection_name)} - info = {} - query(Collection.new(self, SYSTEM_INDEX_COLLECTION), Query.new(sel)).each { |index| - info[index['name']] = index['key'].to_a - } - info - end - - # Create a new index on +collection_name+. +field_or_spec+ - # should be either a single field name or a Array of [field name, - # direction] pairs. Directions should be specified as - # XGen::Mongo::ASCENDING or XGen::Mongo::DESCENDING. Normally called - # by Collection#create_index. If +unique+ is true the index will - # enforce a uniqueness constraint. - def create_index(collection_name, field_or_spec, unique=false) - field_h = OrderedHash.new - if field_or_spec.is_a?(String) || field_or_spec.is_a?(Symbol) - field_h[field_or_spec.to_s] = 1 - else - field_or_spec.each { |f| field_h[f[0].to_s] = f[1] } - end - name = gen_index_name(field_h) - sel = { - :name => name, - :ns => full_coll_name(collection_name), - :key => field_h, - :unique => unique - } - _synchronize { - send_to_db(InsertMessage.new(@name, SYSTEM_INDEX_COLLECTION, false, sel)) - } - name - end - - # Insert +objects+ into +collection_name+. Normally called by - # Collection#insert. Returns a new array containing the _ids - # of the inserted documents. - def insert_into_db(collection_name, objects) - _synchronize { - if @pk_factory - objects.collect! { |o| - @pk_factory.create_pk(o) - } - else - objects = objects.collect do |o| - o[:_id] || o['_id'] ? o : o.merge!(:_id => ObjectID.new) - end - end - send_to_db(InsertMessage.new(@name, collection_name, true, *objects)) - objects.collect { |o| o[:_id] || o['_id'] } - } - end - - def send_to_db(message) - connect_to_master if !connected? && @auto_reconnect - begin - @socket.print(message.buf.to_s) - @socket.flush - rescue => ex - close - raise ex - end - end - - def full_coll_name(collection_name) - "#{@name}.#{collection_name}" - end - - # Return +true+ if +doc+ contains an 'ok' field with the value 1. - def ok?(doc) - ok = doc['ok'] - ok.kind_of?(Numeric) && ok.to_i == 1 - end - - # DB commands need to be ordered, so selector must be an OrderedHash - # (or a Hash with only one element). What DB commands really need is - # that the "command" key be first. - # - # Do not call this. Intended for driver use only. - def db_command(selector, use_admin_db=false) - if !selector.kind_of?(OrderedHash) - if !selector.kind_of?(Hash) || selector.keys.length > 1 - raise "db_command must be given an OrderedHash when there is more than one key" - end - end - - q = Query.new(selector) - q.number_to_return = 1 - query(Collection.new(self, SYSTEM_COMMAND_COLLECTION), q, use_admin_db).next_object - end - - def _synchronize &block - @semaphore.synchronize &block - end - - private - - def hash_password(username, plaintext) - Digest::MD5.hexdigest("#{username}:mongo:#{plaintext}") - end - - def gen_index_name(spec) - temp = [] - spec.each_pair { |field, direction| - temp = temp.push("#{field}_#{direction}") - } - return temp.join("_") + [" ", ".", "$", "/", "\\"].each do |invalid_char| + if db_name.include? invalid_char + raise InvalidName, "database names cannot contain the character '#{invalid_char}'" end end + if db_name.empty? + raise InvalidName, "database name cannot be the empty string" + end + + @name, @nodes = db_name, nodes + @strict = options[:strict] + @pk_factory = options[:pk] + @slave_ok = options[:slave_ok] && @nodes.length == 1 # only OK if one node + @auto_reconnect = options[:auto_reconnect] + @semaphore = Object.new + @semaphore.extend Mutex_m + @socket = nil + connect_to_master + end + + def connect_to_master + close if @socket + @host = @port = nil + @nodes.detect { |hp| + @host, @port = *hp + begin + @socket = TCPSocket.new(@host, @port) + @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) + + # Check for master. Can't call master? because it uses mutex, + # which may already be in use during this call. + semaphore_is_locked = @semaphore.locked? + @semaphore.unlock if semaphore_is_locked + is_master = master? + @semaphore.lock if semaphore_is_locked + + break if @slave_ok || is_master + rescue SocketError, SystemCallError, IOError => ex + close if @socket + end + @socket + } + raise "error: failed to connect to any given host:port" unless @socket + end + + # Returns true if +username+ has +password+ in + # +SYSTEM_USER_COLLECTION+. +name+ is username, +password+ is + # plaintext password. + def authenticate(username, password) + doc = db_command(:getnonce => 1) + raise "error retrieving nonce: #{doc}" unless ok?(doc) + nonce = doc['nonce'] + + auth = OrderedHash.new + auth['authenticate'] = 1 + auth['user'] = username + auth['nonce'] = nonce + auth['key'] = Digest::MD5.hexdigest("#{nonce}#{username}#{hash_password(username, password)}") + ok?(db_command(auth)) + end + + # Deauthorizes use for this database for this connection. + def logout + doc = db_command(:logout => 1) + raise "error logging out: #{doc.inspect}" unless ok?(doc) + end + + # Returns an array of collection names in this database. + def collection_names + names = collections_info.collect { |doc| doc['name'] || '' } + names = names.delete_if {|name| name.index(@name).nil? || name.index('$')} + names.map {|name| name.sub(@name + '.', '')} + end + + # Returns a cursor over query result hashes. Each hash contains a + # 'name' string and optionally an 'options' hash. If +coll_name+ is + # specified, an array of length 1 is returned. + def collections_info(coll_name=nil) + selector = {} + selector[:name] = full_coll_name(coll_name) if coll_name + query(Collection.new(self, SYSTEM_NAMESPACE_COLLECTION), Query.new(selector)) + end + + # Create a collection. If +strict+ is false, will return existing or + # new collection. If +strict+ is true, will raise an error if + # collection +name+ already exists. + # + # Options is an optional hash: + # + # :capped :: Boolean. If not specified, capped is +false+. + # + # :size :: If +capped+ is +true+, specifies the maximum number of + # bytes. If +false+, specifies the initial extent of the + # collection. + # + # :max :: Max number of records in a capped collection. Optional. + def create_collection(name, options={}) + # First check existence + if collection_names.include?(name) + if strict? + raise "Collection #{name} already exists. Currently in strict mode." + else + return Collection.new(self, name) + end + end + + # Create new collection + oh = OrderedHash.new + oh[:create] = name + doc = db_command(oh.merge(options || {})) + ok = doc['ok'] + return Collection.new(self, name) if ok.kind_of?(Numeric) && (ok.to_i == 1 || ok.to_i == 0) + raise "Error creating collection: #{doc.inspect}" + end + + def admin + Admin.new(self) + end + + # Return a collection. If +strict+ is false, will return existing or + # new collection. If +strict+ is true, will raise an error if + # collection +name+ does not already exists. + def collection(name) + return Collection.new(self, name) if !strict? || collection_names.include?(name) + raise "Collection #{name} doesn't exist. Currently in strict mode." + end + alias_method :[], :collection + + # Drop collection +name+. Returns +true+ on success or if the + # collection does not exist, +false+ otherwise. + def drop_collection(name) + return true unless collection_names.include?(name) + + ok?(db_command(:drop => name)) + end + + # Returns the error message from the most recently executed database + # operation for this connection, or +nil+ if there was no error. + # + # Note: as of this writing, errors are only detected on the db server + # for certain kinds of operations (writes). The plan is to change this + # so that all operations will set the error if needed. + def error + doc = db_command(:getlasterror => 1) + raise "error retrieving last error: #{doc}" unless ok?(doc) + doc['err'] + end + + # Returns +true+ if an error was caused by the most recently executed + # database operation. + # + # Note: as of this writing, errors are only detected on the db server + # for certain kinds of operations (writes). The plan is to change this + # so that all operations will set the error if needed. + def error? + error != nil + end + + # Get the most recent error to have occured on this database + # + # Only returns errors that have occured since the last call to + # DB#reset_error_history - returns +nil+ if there is no such error. + def previous_error + error = db_command(:getpreverror => 1) + if error["err"] + error + else + nil + end + end + + # Reset the error history of this database + # + # Calls to DB#previous_error will only return errors that have occurred + # since the most recent call to this method. + def reset_error_history + db_command(:reseterror => 1) + end + + # Returns true if this database is a master (or is not paired with any + # other database), false if it is a slave. + def master? + doc = db_command(:ismaster => 1) + is_master = doc['ismaster'] + ok?(doc) && is_master.kind_of?(Numeric) && is_master.to_i == 1 + end + + # Returns a string of the form "host:port" that points to the master + # database. Works even if this is the master database. + def master + doc = db_command(:ismaster => 1) + is_master = doc['ismaster'] + raise "Error retrieving master database: #{doc.inspect}" unless ok?(doc) && is_master.kind_of?(Numeric) + case is_master.to_i + when 1 + "#@host:#@port" + else + doc['remote'] + end + end + + # Close the connection to the database. + def close + if @socket + s = @socket + @socket = nil + s.close + end + end + + def connected? + @socket != nil + end + + def receive_full(length) + message = "" + while message.length < length do + chunk = @socket.recv(length - message.length) + raise "connection closed" unless chunk.length > 0 + message += chunk + end + message + end + + # Send a MsgMessage to the database. + def send_message(msg) + send_to_db(MsgMessage.new(msg)) + end + + # Returns a Cursor over the query results. + # + # Note that the query gets sent lazily; the cursor calls + # #send_query_message when needed. If the caller never requests an + # object from the cursor, the query never gets sent. + def query(collection, query, admin=false) + Cursor.new(self, collection, query, admin) + end + + # Used by a Cursor to lazily send the query to the database. + def send_query_message(query_message) + send_to_db(query_message) + end + + # Remove the records that match +selector+ from +collection_name+. + # Normally called by Collection#remove or Collection#clear. + def remove_from_db(collection_name, selector) + _synchronize { + send_to_db(RemoveMessage.new(@name, collection_name, selector)) + } + end + + # Update records in +collection_name+ that match +selector+ by + # applying +obj+ as an update. Normally called by Collection#replace. + def replace_in_db(collection_name, selector, obj) + _synchronize { + send_to_db(UpdateMessage.new(@name, collection_name, selector, obj, false)) + } + end + + # DEPRECATED - use Collection#update instead + def modify_in_db(collection_name, selector, obj) + warn "DB#modify_in_db is deprecated and will be removed. Please use Collection#update instead." + replace_in_db(collection_name, selector, obj) + end + + # Update records in +collection_name+ that match +selector+ by + # applying +obj+ as an update. If no match, inserts (???). Normally + # called by Collection#repsert. + def repsert_in_db(collection_name, selector, obj) + _synchronize { + obj = @pk_factory.create_pk(obj) if @pk_factory + send_to_db(UpdateMessage.new(@name, collection_name, selector, obj, true)) + obj + } + end + + # DEPRECATED - use Collection.find(selector).count() instead + def count(collection_name, selector={}) + warn "DB#count is deprecated and will be removed. Please use Collection.find(selector).count instead." + collection(collection_name).find(selector).count() + end + + # Dereference a DBRef, getting the document it points to. + def dereference(dbref) + collection(dbref.namespace).find_one("_id" => dbref.object_id) + end + + # Evaluate a JavaScript expression on MongoDB. + # +code+ should be a string or Code instance containing a JavaScript + # expression. Additional arguments will be passed to that expression + # when it is run on the server. + def eval(code, *args) + if not code.is_a? Code + code = Code.new(code) + end + + oh = OrderedHash.new + oh[:$eval] = code + oh[:args] = args + doc = db_command(oh) + return doc['retval'] if ok?(doc) + raise OperationFailure, "eval failed: #{doc['errmsg']}" + end + + # Rename collection +from+ to +to+. Meant to be called by + # Collection#rename. + def rename_collection(from, to) + oh = OrderedHash.new + oh[:renameCollection] = "#{@name}.#{from}" + oh[:to] = "#{@name}.#{to}" + doc = db_command(oh, true) + raise "Error renaming collection: #{doc.inspect}" unless ok?(doc) + end + + # Drop index +name+ from +collection_name+. Normally called from + # Collection#drop_index or Collection#drop_indexes. + def drop_index(collection_name, name) + oh = OrderedHash.new + oh[:deleteIndexes] = collection_name + oh[:index] = name + doc = db_command(oh) + raise "Error with drop_index command: #{doc.inspect}" unless ok?(doc) + end + + # Get information on the indexes for the collection +collection_name+. + # Normally called by Collection#index_information. Returns a hash where + # the keys are index names (as returned by Collection#create_index and + # the values are lists of [key, direction] pairs specifying the index + # (as passed to Collection#create_index). + def index_information(collection_name) + sel = {:ns => full_coll_name(collection_name)} + info = {} + query(Collection.new(self, SYSTEM_INDEX_COLLECTION), Query.new(sel)).each { |index| + info[index['name']] = index['key'].to_a + } + info + end + + # Create a new index on +collection_name+. +field_or_spec+ + # should be either a single field name or a Array of [field name, + # direction] pairs. Directions should be specified as + # Mongo::ASCENDING or Mongo::DESCENDING. Normally called + # by Collection#create_index. If +unique+ is true the index will + # enforce a uniqueness constraint. + def create_index(collection_name, field_or_spec, unique=false) + field_h = OrderedHash.new + if field_or_spec.is_a?(String) || field_or_spec.is_a?(Symbol) + field_h[field_or_spec.to_s] = 1 + else + field_or_spec.each { |f| field_h[f[0].to_s] = f[1] } + end + name = gen_index_name(field_h) + sel = { + :name => name, + :ns => full_coll_name(collection_name), + :key => field_h, + :unique => unique + } + _synchronize { + send_to_db(InsertMessage.new(@name, SYSTEM_INDEX_COLLECTION, false, sel)) + } + name + end + + # Insert +objects+ into +collection_name+. Normally called by + # Collection#insert. Returns a new array containing the _ids + # of the inserted documents. + def insert_into_db(collection_name, objects) + _synchronize { + if @pk_factory + objects.collect! { |o| + @pk_factory.create_pk(o) + } + else + objects = objects.collect do |o| + o[:_id] || o['_id'] ? o : o.merge!(:_id => ObjectID.new) + end + end + send_to_db(InsertMessage.new(@name, collection_name, true, *objects)) + objects.collect { |o| o[:_id] || o['_id'] } + } + end + + def send_to_db(message) + connect_to_master if !connected? && @auto_reconnect + begin + @socket.print(message.buf.to_s) + @socket.flush + rescue => ex + close + raise ex + end + end + + def full_coll_name(collection_name) + "#{@name}.#{collection_name}" + end + + # Return +true+ if +doc+ contains an 'ok' field with the value 1. + def ok?(doc) + ok = doc['ok'] + ok.kind_of?(Numeric) && ok.to_i == 1 + end + + # DB commands need to be ordered, so selector must be an OrderedHash + # (or a Hash with only one element). What DB commands really need is + # that the "command" key be first. + # + # Do not call this. Intended for driver use only. + def db_command(selector, use_admin_db=false) + if !selector.kind_of?(OrderedHash) + if !selector.kind_of?(Hash) || selector.keys.length > 1 + raise "db_command must be given an OrderedHash when there is more than one key" + end + end + + q = Query.new(selector) + q.number_to_return = 1 + query(Collection.new(self, SYSTEM_COMMAND_COLLECTION), q, use_admin_db).next_object + end + + def _synchronize &block + @semaphore.synchronize &block + end + + private + + def hash_password(username, plaintext) + Digest::MD5.hexdigest("#{username}:mongo:#{plaintext}") + end + + def gen_index_name(spec) + temp = [] + spec.each_pair { |field, direction| + temp = temp.push("#{field}_#{direction}") + } + return temp.join("_") end end end diff --git a/lib/mongo/errors.rb b/lib/mongo/errors.rb index d2b881a..5e73312 100644 --- a/lib/mongo/errors.rb +++ b/lib/mongo/errors.rb @@ -14,14 +14,10 @@ # Exceptions raised by the MongoDB driver. -module XGen - module Mongo - module Driver - # Raised when a database operation fails. - class OperationFailure < RuntimeError; end +module Mongo + # Raised when a database operation fails. + class OperationFailure < RuntimeError; end - # Raised when an invalid name is used. - class InvalidName < RuntimeError; end - end - end + # Raised when an invalid name is used. + class InvalidName < RuntimeError; end end diff --git a/lib/mongo/gridfs/chunk.rb b/lib/mongo/gridfs/chunk.rb index d492e6e..ddeee7f 100644 --- a/lib/mongo/gridfs/chunk.rb +++ b/lib/mongo/gridfs/chunk.rb @@ -19,78 +19,74 @@ require 'mongo/util/byte_buffer' require 'mongo/util/ordered_hash' -module XGen - module Mongo - module GridFS +module GridFS - # A chunk stores a portion of GridStore data. - class Chunk + # A chunk stores a portion of GridStore data. + class Chunk - DEFAULT_CHUNK_SIZE = 1024 * 256 + DEFAULT_CHUNK_SIZE = 1024 * 256 - attr_reader :object_id, :chunk_number - attr_accessor :data + attr_reader :object_id, :chunk_number + attr_accessor :data - def initialize(file, mongo_object={}) - @file = file - @object_id = mongo_object['_id'] || XGen::Mongo::Driver::ObjectID.new - @chunk_number = mongo_object['n'] || 0 + def initialize(file, mongo_object={}) + @file = file + @object_id = mongo_object['_id'] || Mongo::ObjectID.new + @chunk_number = mongo_object['n'] || 0 - @data = ByteBuffer.new - case mongo_object['data'] - when String - mongo_object['data'].each_byte { |b| @data.put(b) } - when ByteBuffer - @data.put_array(mongo_object['data'].to_a) - when Array - @data.put_array(mongo_object['data']) - when nil - else - raise "illegal chunk format; data is #{mongo_object['data'] ? (' ' + mongo_object['data'].class.name) : 'nil'}" - end - @data.rewind - end + @data = ByteBuffer.new + case mongo_object['data'] + when String + mongo_object['data'].each_byte { |b| @data.put(b) } + when ByteBuffer + @data.put_array(mongo_object['data'].to_a) + when Array + @data.put_array(mongo_object['data']) + when nil + else + raise "illegal chunk format; data is #{mongo_object['data'] ? (' ' + mongo_object['data'].class.name) : 'nil'}" + end + @data.rewind + end - def pos; @data.position; end - def pos=(pos); @data.position = pos; end - def eof?; !@data.more?; end + def pos; @data.position; end + def pos=(pos); @data.position = pos; end + def eof?; !@data.more?; end - def size; @data.size; end - alias_method :length, :size - - # Erase all data after current position. - def truncate - if @data.position < @data.length - curr_data = @data - @data = ByteBuffer.new - @data.put_array(curr_data.to_a[0...curr_data.position]) - end - end - - def getc - @data.more? ? @data.get : nil - end - - def putc(byte) - @data.put(byte) - end - - def save - coll = @file.chunk_collection - coll.remove({'_id' => @object_id}) - coll.insert(to_mongo_object) - end - - def to_mongo_object - h = OrderedHash.new - h['_id'] = @object_id - h['files_id'] = @file.files_id - h['n'] = @chunk_number - h['data'] = data - h - end + def size; @data.size; end + alias_method :length, :size + # Erase all data after current position. + def truncate + if @data.position < @data.length + curr_data = @data + @data = ByteBuffer.new + @data.put_array(curr_data.to_a[0...curr_data.position]) end end + + def getc + @data.more? ? @data.get : nil + end + + def putc(byte) + @data.put(byte) + end + + def save + coll = @file.chunk_collection + coll.remove({'_id' => @object_id}) + coll.insert(to_mongo_object) + end + + def to_mongo_object + h = OrderedHash.new + h['_id'] = @object_id + h['files_id'] = @file.files_id + h['n'] = @chunk_number + h['data'] = data + h + end + end end diff --git a/lib/mongo/gridfs/grid_store.rb b/lib/mongo/gridfs/grid_store.rb index 6ce3a98..4ae53fa 100644 --- a/lib/mongo/gridfs/grid_store.rb +++ b/lib/mongo/gridfs/grid_store.rb @@ -18,451 +18,447 @@ require 'mongo/types/objectid' require 'mongo/util/ordered_hash' require 'mongo/gridfs/chunk' -module XGen - module Mongo - module GridFS +module GridFS - # GridStore is an IO-like object that provides input and output for - # streams of data to Mongo. See Mongo's documentation about GridFS for - # storage implementation details. + # GridStore is an IO-like object that provides input and output for + # streams of data to Mongo. See Mongo's documentation about GridFS for + # storage implementation details. + # + # Example code: + # + # require 'mongo/gridfs' + # GridStore.open(database, 'filename', 'w') { |f| + # f.puts "Hello, world!" + # } + # GridStore.open(database, 'filename, 'r') { |f| + # puts f.read # => Hello, world!\n + # } + # GridStore.open(database, 'filename', 'w+') { |f| + # f.puts "But wait, there's more!" + # } + # GridStore.open(database, 'filename, 'r') { |f| + # puts f.read # => Hello, world!\nBut wait, there's more!\n + # } + class GridStore + + DEFAULT_ROOT_COLLECTION = 'fs' + DEFAULT_CONTENT_TYPE = 'text/plain' + + include Enumerable + + attr_accessor :filename + + # Array of strings; may be +nil+ + attr_accessor :aliases + + # Default is DEFAULT_CONTENT_TYPE + attr_accessor :content_type + + attr_accessor :metadata + + attr_reader :files_id + + # Time that the file was first saved. + attr_reader :upload_date + + attr_reader :chunk_size + + attr_accessor :lineno + + attr_reader :md5 + + class << self + + def exist?(db, name, root_collection=DEFAULT_ROOT_COLLECTION) + db.collection("#{root_collection}.files").find({'filename' => name}).next_object != nil + end + + def open(db, name, mode, options={}) + gs = self.new(db, name, mode, options) + result = nil + begin + result = yield gs if block_given? + ensure + gs.close + end + result + end + + def read(db, name, length=nil, offset=nil) + GridStore.open(db, name, 'r') { |gs| + gs.seek(offset) if offset + gs.read(length) + } + end + + # List the contains of all GridFS files stored in the given db and + # root collection. # - # Example code: + # :db :: the database to use # - # require 'mongo/gridfs' - # GridStore.open(database, 'filename', 'w') { |f| - # f.puts "Hello, world!" - # } - # GridStore.open(database, 'filename, 'r') { |f| - # puts f.read # => Hello, world!\n - # } - # GridStore.open(database, 'filename', 'w+') { |f| - # f.puts "But wait, there's more!" - # } - # GridStore.open(database, 'filename, 'r') { |f| - # puts f.read # => Hello, world!\nBut wait, there's more!\n - # } - class GridStore + # :root_collection :: the root collection to use + def list(db, root_collection=DEFAULT_ROOT_COLLECTION) + db.collection("#{root_collection}.files").find().map { |f| + f['filename'] + } + end - DEFAULT_ROOT_COLLECTION = 'fs' - DEFAULT_CONTENT_TYPE = 'text/plain' + def readlines(db, name, separator=$/) + GridStore.open(db, name, 'r') { |gs| + gs.readlines(separator) + } + end - include Enumerable + def unlink(db, *names) + names.each { |name| + gs = GridStore.new(db, name) + gs.send(:delete_chunks) + gs.collection.remove('_id' => gs.files_id) + } + end + alias_method :delete, :unlink - attr_accessor :filename + end - # Array of strings; may be +nil+ - attr_accessor :aliases + #--- + # ================================================================ + #+++ - # Default is DEFAULT_CONTENT_TYPE - attr_accessor :content_type + # Mode may only be 'r', 'w', or 'w+'. + # + # Options. Descriptions start with a list of the modes for which that + # option is legitimate. + # + # :root :: (r, w, w+) Name of root collection to use, instead of + # DEFAULT_ROOT_COLLECTION. + # + # :metadata:: (w, w+) A hash containing any data you want persisted as + # this file's metadata. See also metadata= + # + # :chunk_size :: (w) Sets chunk size for files opened for writing + # See also chunk_size= which may only be called before + # any data is written. + # + # :content_type :: (w) Default value is DEFAULT_CONTENT_TYPE. See + # also #content_type= + def initialize(db, name, mode='r', options={}) + @db, @filename, @mode = db, name, mode + @root = options[:root] || DEFAULT_ROOT_COLLECTION - attr_accessor :metadata + doc = collection.find({'filename' => @filename}).next_object + if doc + @files_id = doc['_id'] + @content_type = doc['contentType'] + @chunk_size = doc['chunkSize'] + @upload_date = doc['uploadDate'] + @aliases = doc['aliases'] + @length = doc['length'] + @metadata = doc['metadata'] + @md5 = doc['md5'] + else + @files_id = Mongo::ObjectID.new + @content_type = DEFAULT_CONTENT_TYPE + @chunk_size = Chunk::DEFAULT_CHUNK_SIZE + @length = 0 + end - attr_reader :files_id + case mode + when 'r' + @curr_chunk = nth_chunk(0) + @position = 0 + when 'w' + chunk_collection.create_index([['files_id', Mongo::ASCENDING], ['n', Mongo::ASCENDING]]) + delete_chunks + @curr_chunk = Chunk.new(self, 'n' => 0) + @content_type = options[:content_type] if options[:content_type] + @chunk_size = options[:chunk_size] if options[:chunk_size] + @metadata = options[:metadata] if options[:metadata] + @position = 0 + when 'w+' + chunk_collection.create_index([['files_id', Mongo::ASCENDING], ['n', Mongo::ASCENDING]]) + @curr_chunk = nth_chunk(last_chunk_number) || Chunk.new(self, 'n' => 0) # might be empty + @curr_chunk.pos = @curr_chunk.data.length if @curr_chunk + @metadata = options[:metadata] if options[:metadata] + @position = @length + else + raise "error: illegal mode #{mode}" + end - # Time that the file was first saved. - attr_reader :upload_date + @lineno = 0 + @pushback_byte = nil + end - attr_reader :chunk_size + def collection + @db.collection("#{@root}.files") + end - attr_accessor :lineno + # Returns collection used for storing chunks. Depends on value of + # @root. + def chunk_collection + @db.collection("#{@root}.chunks") + end - attr_reader :md5 + # Change chunk size. Can only change if the file is opened for write + # and no data has yet been written. + def chunk_size=(size) + unless @mode[0] == ?w && @position == 0 && @upload_date == nil + raise "error: can only change chunk size if open for write and no data written." + end + @chunk_size = size + end - class << self - - def exist?(db, name, root_collection=DEFAULT_ROOT_COLLECTION) - db.collection("#{root_collection}.files").find({'filename' => name}).next_object != nil - end - - def open(db, name, mode, options={}) - gs = self.new(db, name, mode, options) - result = nil - begin - result = yield gs if block_given? - ensure - gs.close - end - result - end - - def read(db, name, length=nil, offset=nil) - GridStore.open(db, name, 'r') { |gs| - gs.seek(offset) if offset - gs.read(length) - } - end - - # List the contains of all GridFS files stored in the given db and - # root collection. - # - # :db :: the database to use - # - # :root_collection :: the root collection to use - def list(db, root_collection=DEFAULT_ROOT_COLLECTION) - db.collection("#{root_collection}.files").find().map { |f| - f['filename'] - } - end - - def readlines(db, name, separator=$/) - GridStore.open(db, name, 'r') { |gs| - gs.readlines(separator) - } - end - - def unlink(db, *names) - names.each { |name| - gs = GridStore.new(db, name) - gs.send(:delete_chunks) - gs.collection.remove('_id' => gs.files_id) - } - end - alias_method :delete, :unlink + #--- + # ================ reading ================ + #+++ + def getc + if @pushback_byte + byte = @pushback_byte + @pushback_byte = nil + @position += 1 + byte + elsif eof? + nil + else + if @curr_chunk.eof? + @curr_chunk = nth_chunk(@curr_chunk.chunk_number + 1) end - - #--- - # ================================================================ - #+++ - - # Mode may only be 'r', 'w', or 'w+'. - # - # Options. Descriptions start with a list of the modes for which that - # option is legitimate. - # - # :root :: (r, w, w+) Name of root collection to use, instead of - # DEFAULT_ROOT_COLLECTION. - # - # :metadata:: (w, w+) A hash containing any data you want persisted as - # this file's metadata. See also metadata= - # - # :chunk_size :: (w) Sets chunk size for files opened for writing - # See also chunk_size= which may only be called before - # any data is written. - # - # :content_type :: (w) Default value is DEFAULT_CONTENT_TYPE. See - # also #content_type= - def initialize(db, name, mode='r', options={}) - @db, @filename, @mode = db, name, mode - @root = options[:root] || DEFAULT_ROOT_COLLECTION - - doc = collection.find({'filename' => @filename}).next_object - if doc - @files_id = doc['_id'] - @content_type = doc['contentType'] - @chunk_size = doc['chunkSize'] - @upload_date = doc['uploadDate'] - @aliases = doc['aliases'] - @length = doc['length'] - @metadata = doc['metadata'] - @md5 = doc['md5'] - else - @files_id = XGen::Mongo::Driver::ObjectID.new - @content_type = DEFAULT_CONTENT_TYPE - @chunk_size = Chunk::DEFAULT_CHUNK_SIZE - @length = 0 - end - - case mode - when 'r' - @curr_chunk = nth_chunk(0) - @position = 0 - when 'w' - chunk_collection.create_index([['files_id', XGen::Mongo::ASCENDING], ['n', XGen::Mongo::ASCENDING]]) - delete_chunks - @curr_chunk = Chunk.new(self, 'n' => 0) - @content_type = options[:content_type] if options[:content_type] - @chunk_size = options[:chunk_size] if options[:chunk_size] - @metadata = options[:metadata] if options[:metadata] - @position = 0 - when 'w+' - chunk_collection.create_index([['files_id', XGen::Mongo::ASCENDING], ['n', XGen::Mongo::ASCENDING]]) - @curr_chunk = nth_chunk(last_chunk_number) || Chunk.new(self, 'n' => 0) # might be empty - @curr_chunk.pos = @curr_chunk.data.length if @curr_chunk - @metadata = options[:metadata] if options[:metadata] - @position = @length - else - raise "error: illegal mode #{mode}" - end - - @lineno = 0 - @pushback_byte = nil - end - - def collection - @db.collection("#{@root}.files") - end - - # Returns collection used for storing chunks. Depends on value of - # @root. - def chunk_collection - @db.collection("#{@root}.chunks") - end - - # Change chunk size. Can only change if the file is opened for write - # and no data has yet been written. - def chunk_size=(size) - unless @mode[0] == ?w && @position == 0 && @upload_date == nil - raise "error: can only change chunk size if open for write and no data written." - end - @chunk_size = size - end - - #--- - # ================ reading ================ - #+++ - - def getc - if @pushback_byte - byte = @pushback_byte - @pushback_byte = nil - @position += 1 - byte - elsif eof? - nil - else - if @curr_chunk.eof? - @curr_chunk = nth_chunk(@curr_chunk.chunk_number + 1) - end - @position += 1 - @curr_chunk.getc - end - end - - def gets(separator=$/) - str = '' - byte = self.getc - return nil if byte == nil # EOF - while byte != nil - s = byte.chr - str << s - break if s == separator - byte = self.getc - end - @lineno += 1 - str - end - - def read(len=nil, buf=nil) - buf ||= '' - byte = self.getc - while byte != nil && (len == nil || len > 0) - buf << byte.chr - len -= 1 if len - byte = self.getc if (len == nil || len > 0) - end - buf - end - - def readchar - byte = self.getc - raise EOFError.new if byte == nil - byte - end - - def readline(separator=$/) - line = gets - raise EOFError.new if line == nil - line - end - - def readlines(separator=$/) - read.split(separator).collect { |line| "#{line}#{separator}" } - end - - def each - line = gets - while line - yield line - line = gets - end - end - alias_method :each_line, :each - - def each_byte - byte = self.getc - while byte - yield byte - byte = self.getc - end - end - - def ungetc(byte) - @pushback_byte = byte - @position -= 1 - end - - #--- - # ================ writing ================ - #+++ - - def putc(byte) - if @curr_chunk.pos == @chunk_size - prev_chunk_number = @curr_chunk.chunk_number - @curr_chunk.save - @curr_chunk = Chunk.new(self, 'n' => prev_chunk_number + 1) - end - @position += 1 - @curr_chunk.putc(byte) - end - - def print(*objs) - objs = [$_] if objs == nil || objs.empty? - objs.each { |obj| - str = obj.to_s - str.each_byte { |byte| self.putc(byte) } - } - nil - end - - def puts(*objs) - if objs == nil || objs.empty? - self.putc(10) - else - print(*objs.collect{ |obj| - str = obj.to_s - str << "\n" unless str =~ /\n$/ - str - }) - end - nil - end - - def <<(obj) - write(obj.to_s) - end - - # Writes +string+ as bytes and returns the number of bytes written. - def write(string) - raise "#@filename not opened for write" unless @mode[0] == ?w - count = 0 - string.each_byte { |byte| - self.putc byte - count += 1 - } - count - end - - # A no-op. - def flush - end - - #--- - # ================ status ================ - #+++ - - def eof - raise IOError.new("stream not open for reading") unless @mode[0] == ?r - @position >= @length - end - alias_method :eof?, :eof - - #--- - # ================ positioning ================ - #+++ - - def rewind - if @curr_chunk.chunk_number != 0 - if @mode[0] == ?w - delete_chunks - @curr_chunk = Chunk.new(self, 'n' => 0) - else - @curr_chunk == nth_chunk(0) - end - end - @curr_chunk.pos = 0 - @lineno = 0 - @position = 0 - end - - def seek(pos, whence=IO::SEEK_SET) - target_pos = case whence - when IO::SEEK_CUR - @position + pos - when IO::SEEK_END - @length + pos - when IO::SEEK_SET - pos - end - - new_chunk_number = (target_pos / @chunk_size).to_i - if new_chunk_number != @curr_chunk.chunk_number - @curr_chunk.save if @mode[0] == ?w - @curr_chunk = nth_chunk(new_chunk_number) - end - @position = target_pos - @curr_chunk.pos = @position % @chunk_size - 0 - end - - def tell - @position - end - - #--- - # ================ closing ================ - #+++ - - def close - if @mode[0] == ?w - if @curr_chunk - @curr_chunk.truncate - @curr_chunk.save if @curr_chunk.pos > 0 - end - files = collection - if @upload_date - files.remove('_id' => @files_id) - else - @upload_date = Time.now - end - files.insert(to_mongo_object) - end - @db = nil - end - - def closed? - @db == nil - end - - #--- - # ================ protected ================ - #+++ - - protected - - def to_mongo_object - h = OrderedHash.new - h['_id'] = @files_id - h['filename'] = @filename - h['contentType'] = @content_type - h['length'] = @curr_chunk ? @curr_chunk.chunk_number * @chunk_size + @curr_chunk.pos : 0 - h['chunkSize'] = @chunk_size - h['uploadDate'] = @upload_date - h['aliases'] = @aliases - h['metadata'] = @metadata - md5_command = OrderedHash.new - md5_command['filemd5'] = @files_id - md5_command['root'] = @root - h['md5'] = @db.db_command(md5_command)['md5'] - h - end - - def delete_chunks - chunk_collection.remove({'files_id' => @files_id}) if @files_id - @curr_chunk = nil - end - - def nth_chunk(n) - mongo_chunk = chunk_collection.find({'files_id' => @files_id, 'n' => n}).next_object - Chunk.new(self, mongo_chunk || {}) - end - - def last_chunk_number - (@length / @chunk_size).to_i - end - + @position += 1 + @curr_chunk.getc end end + + def gets(separator=$/) + str = '' + byte = self.getc + return nil if byte == nil # EOF + while byte != nil + s = byte.chr + str << s + break if s == separator + byte = self.getc + end + @lineno += 1 + str + end + + def read(len=nil, buf=nil) + buf ||= '' + byte = self.getc + while byte != nil && (len == nil || len > 0) + buf << byte.chr + len -= 1 if len + byte = self.getc if (len == nil || len > 0) + end + buf + end + + def readchar + byte = self.getc + raise EOFError.new if byte == nil + byte + end + + def readline(separator=$/) + line = gets + raise EOFError.new if line == nil + line + end + + def readlines(separator=$/) + read.split(separator).collect { |line| "#{line}#{separator}" } + end + + def each + line = gets + while line + yield line + line = gets + end + end + alias_method :each_line, :each + + def each_byte + byte = self.getc + while byte + yield byte + byte = self.getc + end + end + + def ungetc(byte) + @pushback_byte = byte + @position -= 1 + end + + #--- + # ================ writing ================ + #+++ + + def putc(byte) + if @curr_chunk.pos == @chunk_size + prev_chunk_number = @curr_chunk.chunk_number + @curr_chunk.save + @curr_chunk = Chunk.new(self, 'n' => prev_chunk_number + 1) + end + @position += 1 + @curr_chunk.putc(byte) + end + + def print(*objs) + objs = [$_] if objs == nil || objs.empty? + objs.each { |obj| + str = obj.to_s + str.each_byte { |byte| self.putc(byte) } + } + nil + end + + def puts(*objs) + if objs == nil || objs.empty? + self.putc(10) + else + print(*objs.collect{ |obj| + str = obj.to_s + str << "\n" unless str =~ /\n$/ + str + }) + end + nil + end + + def <<(obj) + write(obj.to_s) + end + + # Writes +string+ as bytes and returns the number of bytes written. + def write(string) + raise "#@filename not opened for write" unless @mode[0] == ?w + count = 0 + string.each_byte { |byte| + self.putc byte + count += 1 + } + count + end + + # A no-op. + def flush + end + + #--- + # ================ status ================ + #+++ + + def eof + raise IOError.new("stream not open for reading") unless @mode[0] == ?r + @position >= @length + end + alias_method :eof?, :eof + + #--- + # ================ positioning ================ + #+++ + + def rewind + if @curr_chunk.chunk_number != 0 + if @mode[0] == ?w + delete_chunks + @curr_chunk = Chunk.new(self, 'n' => 0) + else + @curr_chunk == nth_chunk(0) + end + end + @curr_chunk.pos = 0 + @lineno = 0 + @position = 0 + end + + def seek(pos, whence=IO::SEEK_SET) + target_pos = case whence + when IO::SEEK_CUR + @position + pos + when IO::SEEK_END + @length + pos + when IO::SEEK_SET + pos + end + + new_chunk_number = (target_pos / @chunk_size).to_i + if new_chunk_number != @curr_chunk.chunk_number + @curr_chunk.save if @mode[0] == ?w + @curr_chunk = nth_chunk(new_chunk_number) + end + @position = target_pos + @curr_chunk.pos = @position % @chunk_size + 0 + end + + def tell + @position + end + + #--- + # ================ closing ================ + #+++ + + def close + if @mode[0] == ?w + if @curr_chunk + @curr_chunk.truncate + @curr_chunk.save if @curr_chunk.pos > 0 + end + files = collection + if @upload_date + files.remove('_id' => @files_id) + else + @upload_date = Time.now + end + files.insert(to_mongo_object) + end + @db = nil + end + + def closed? + @db == nil + end + + #--- + # ================ protected ================ + #+++ + + protected + + def to_mongo_object + h = OrderedHash.new + h['_id'] = @files_id + h['filename'] = @filename + h['contentType'] = @content_type + h['length'] = @curr_chunk ? @curr_chunk.chunk_number * @chunk_size + @curr_chunk.pos : 0 + h['chunkSize'] = @chunk_size + h['uploadDate'] = @upload_date + h['aliases'] = @aliases + h['metadata'] = @metadata + md5_command = OrderedHash.new + md5_command['filemd5'] = @files_id + md5_command['root'] = @root + h['md5'] = @db.db_command(md5_command)['md5'] + h + end + + def delete_chunks + chunk_collection.remove({'files_id' => @files_id}) if @files_id + @curr_chunk = nil + end + + def nth_chunk(n) + mongo_chunk = chunk_collection.find({'files_id' => @files_id, 'n' => n}).next_object + Chunk.new(self, mongo_chunk || {}) + end + + def last_chunk_number + (@length / @chunk_size).to_i + end + end end diff --git a/lib/mongo/message/get_more_message.rb b/lib/mongo/message/get_more_message.rb index 935305b..f45198b 100644 --- a/lib/mongo/message/get_more_message.rb +++ b/lib/mongo/message/get_more_message.rb @@ -17,21 +17,16 @@ require 'mongo/message/message' require 'mongo/message/opcodes' -module XGen - module Mongo - module Driver +module Mongo - class GetMoreMessage < Message + class GetMoreMessage < Message - def initialize(db_name, collection_name, cursor) - super(OP_GET_MORE) - write_int(0) - write_string("#{db_name}.#{collection_name}") - write_int(0) # num to return; leave it up to the db for now - write_long(cursor) - end - end + def initialize(db_name, collection_name, cursor) + super(OP_GET_MORE) + write_int(0) + write_string("#{db_name}.#{collection_name}") + write_int(0) # num to return; leave it up to the db for now + write_long(cursor) end end end - diff --git a/lib/mongo/message/insert_message.rb b/lib/mongo/message/insert_message.rb index 486d48a..4aecbab 100644 --- a/lib/mongo/message/insert_message.rb +++ b/lib/mongo/message/insert_message.rb @@ -17,19 +17,15 @@ require 'mongo/message/message' require 'mongo/message/opcodes' -module XGen - module Mongo - module Driver +module Mongo - class InsertMessage < Message + class InsertMessage < Message - def initialize(db_name, collection_name, check_keys=true, *objs) - super(OP_INSERT) - write_int(0) - write_string("#{db_name}.#{collection_name}") - objs.each { |o| write_doc(o, check_keys) } - end - end + def initialize(db_name, collection_name, check_keys=true, *objs) + super(OP_INSERT) + write_int(0) + write_string("#{db_name}.#{collection_name}") + objs.each { |o| write_doc(o, check_keys) } end end end diff --git a/lib/mongo/message/kill_cursors_message.rb b/lib/mongo/message/kill_cursors_message.rb index 6c3d5ca..c1f5597 100644 --- a/lib/mongo/message/kill_cursors_message.rb +++ b/lib/mongo/message/kill_cursors_message.rb @@ -17,20 +17,15 @@ require 'mongo/message/message' require 'mongo/message/opcodes' -module XGen - module Mongo - module Driver +module Mongo - class KillCursorsMessage < Message + class KillCursorsMessage < Message - def initialize(*cursors) - super(OP_KILL_CURSORS) - write_int(0) - write_int(cursors.length) - cursors.each { |c| write_long c } - end - end + def initialize(*cursors) + super(OP_KILL_CURSORS) + write_int(0) + write_int(cursors.length) + cursors.each { |c| write_long c } end end end - diff --git a/lib/mongo/message/message.rb b/lib/mongo/message/message.rb index cb124b6..8a8cc9e 100644 --- a/lib/mongo/message/message.rb +++ b/lib/mongo/message/message.rb @@ -17,68 +17,64 @@ require 'mongo/util/bson' require 'mongo/util/byte_buffer' -module XGen - module Mongo - module Driver +module Mongo - class Message + class Message - HEADER_SIZE = 16 # size, id, response_to, opcode + HEADER_SIZE = 16 # size, id, response_to, opcode - @@class_req_id = 0 + @@class_req_id = 0 - attr_reader :buf # for testing + attr_reader :buf # for testing - def initialize(op) - @op = op - @message_length = HEADER_SIZE - @data_length = 0 - @request_id = (@@class_req_id += 1) - @response_id = 0 - @buf = ByteBuffer.new + def initialize(op) + @op = op + @message_length = HEADER_SIZE + @data_length = 0 + @request_id = (@@class_req_id += 1) + @response_id = 0 + @buf = ByteBuffer.new - @buf.put_int(16) # holder for length - @buf.put_int(@request_id) - @buf.put_int(0) # response_to - @buf.put_int(op) - end - - def write_int(i) - @buf.put_int(i) - update_message_length - end - - def write_long(i) - @buf.put_long(i) - update_message_length - end - - def write_string(s) - BSON.serialize_cstr(@buf, s) - update_message_length - end - - def write_doc(hash, check_keys=false) - @buf.put_array(BSON.new.serialize(hash, check_keys).to_a) - update_message_length - end - - def to_a - @buf.to_a - end - - def dump - @buf.dump - end - - # Do not call. Private, but kept public for testing. - def update_message_length - pos = @buf.position - @buf.put_int(@buf.size, 0) - @buf.position = pos - end - - end + @buf.put_int(16) # holder for length + @buf.put_int(@request_id) + @buf.put_int(0) # response_to + @buf.put_int(op) end + + def write_int(i) + @buf.put_int(i) + update_message_length + end + + def write_long(i) + @buf.put_long(i) + update_message_length + end + + def write_string(s) + BSON.serialize_cstr(@buf, s) + update_message_length + end + + def write_doc(hash, check_keys=false) + @buf.put_array(BSON.new.serialize(hash, check_keys).to_a) + update_message_length + end + + def to_a + @buf.to_a + end + + def dump + @buf.dump + end + + # Do not call. Private, but kept public for testing. + def update_message_length + pos = @buf.position + @buf.put_int(@buf.size, 0) + @buf.position = pos + end + end end diff --git a/lib/mongo/message/message_header.rb b/lib/mongo/message/message_header.rb index cf1ffc0..09c2691 100644 --- a/lib/mongo/message/message_header.rb +++ b/lib/mongo/message/message_header.rb @@ -16,35 +16,30 @@ require 'mongo/util/byte_buffer' -module XGen - module Mongo - module Driver +module Mongo - class MessageHeader + class MessageHeader - HEADER_SIZE = 16 + HEADER_SIZE = 16 - def initialize() - @buf = ByteBuffer.new - end + def initialize() + @buf = ByteBuffer.new + end - def read_header(db) - @buf.rewind - @buf.put_array(db.receive_full(HEADER_SIZE).unpack("C*")) - raise "Short read for DB response header: expected #{HEADER_SIZE} bytes, saw #{@buf.size}" unless @buf.size == HEADER_SIZE - @buf.rewind - @size = @buf.get_int - @request_id = @buf.get_int - @response_to = @buf.get_int - @op = @buf.get_int - self - end + def read_header(db) + @buf.rewind + @buf.put_array(db.receive_full(HEADER_SIZE).unpack("C*")) + raise "Short read for DB response header: expected #{HEADER_SIZE} bytes, saw #{@buf.size}" unless @buf.size == HEADER_SIZE + @buf.rewind + @size = @buf.get_int + @request_id = @buf.get_int + @response_to = @buf.get_int + @op = @buf.get_int + self + end - def dump - @buf.dump - end - end + def dump + @buf.dump end end end - diff --git a/lib/mongo/message/msg_message.rb b/lib/mongo/message/msg_message.rb index 851edec..48370af 100644 --- a/lib/mongo/message/msg_message.rb +++ b/lib/mongo/message/msg_message.rb @@ -17,17 +17,13 @@ require 'mongo/message/message' require 'mongo/message/opcodes' -module XGen - module Mongo - module Driver +module Mongo - class MsgMessage < Message + class MsgMessage < Message - def initialize(msg) - super(OP_MSG) - write_string(msg) - end - end + def initialize(msg) + super(OP_MSG) + write_string(msg) end end end diff --git a/lib/mongo/message/opcodes.rb b/lib/mongo/message/opcodes.rb index b931a64..88b9c54 100644 --- a/lib/mongo/message/opcodes.rb +++ b/lib/mongo/message/opcodes.rb @@ -14,19 +14,14 @@ # limitations under the License. # ++ -module XGen - module Mongo - module Driver - OP_REPLY = 1 # reply. responseTo is set. - OP_MSG = 1000 # generic msg command followed by a string - OP_UPDATE = 2001 # update object - OP_INSERT = 2002 - # GET_BY_OID = 2003 - OP_QUERY = 2004 - OP_GET_MORE = 2005 - OP_DELETE = 2006 - OP_KILL_CURSORS = 2007 - end - end +module Mongo + OP_REPLY = 1 # reply. responseTo is set. + OP_MSG = 1000 # generic msg command followed by a string + OP_UPDATE = 2001 # update object + OP_INSERT = 2002 + # GET_BY_OID = 2003 + OP_QUERY = 2004 + OP_GET_MORE = 2005 + OP_DELETE = 2006 + OP_KILL_CURSORS = 2007 end - diff --git a/lib/mongo/message/query_message.rb b/lib/mongo/message/query_message.rb index d5316b9..9e1196e 100644 --- a/lib/mongo/message/query_message.rb +++ b/lib/mongo/message/query_message.rb @@ -18,60 +18,56 @@ require 'mongo/message/message' require 'mongo/message/opcodes' require 'mongo/util/ordered_hash' -module XGen - module Mongo - module Driver +module Mongo - class QueryMessage < Message + class QueryMessage < Message - attr_reader :query + attr_reader :query - def initialize(db_name, collection_name, query) - super(OP_QUERY) - @query = query - write_int(0) - write_string("#{db_name}.#{collection_name}") - write_int(query.number_to_skip) - write_int(-query.number_to_return) # Negative means hard limit - sel = query.selector - if query.contains_special_fields - sel = OrderedHash.new - sel['query'] = query.selector - if query.order_by && query.order_by.length > 0 - sel['orderby'] = case query.order_by + def initialize(db_name, collection_name, query) + super(OP_QUERY) + @query = query + write_int(0) + write_string("#{db_name}.#{collection_name}") + write_int(query.number_to_skip) + write_int(-query.number_to_return) # Negative means hard limit + sel = query.selector + if query.contains_special_fields + sel = OrderedHash.new + sel['query'] = query.selector + if query.order_by && query.order_by.length > 0 + sel['orderby'] = case query.order_by + when String + {query.order_by => 1} + when Array + h = OrderedHash.new + query.order_by.each { |ob| + case ob when String - {query.order_by => 1} - when Array - h = OrderedHash.new - query.order_by.each { |ob| - case ob - when String - h[ob] = 1 - when Hash # should have one entry; will handle all - ob.each { |k,v| h[k] = v } - else - raise "illegal query order_by value #{query.order_by.inspect}" - end - } - h - when Hash # Should be an ordered hash, but this message doesn't care - query.order_by + h[ob] = 1 + when Hash # should have one entry; will handle all + ob.each { |k,v| h[k] = v } else - raise "illegal order_by: is a #{query.order_by.class.name}, must be String, Array, Hash, or OrderedHash" + raise "illegal query order_by value #{query.order_by.inspect}" end - end - sel['$hint'] = query.hint if query.hint && query.hint.length > 0 - sel['$explain'] = true if query.explain - sel['$snapshot'] = true if query.snapshot - end - write_doc(sel) - write_doc(query.fields) if query.fields - end - - def first_key(key) - @first_key = key + } + h + when Hash # Should be an ordered hash, but this message doesn't care + query.order_by + else + raise "illegal order_by: is a #{query.order_by.class.name}, must be String, Array, Hash, or OrderedHash" + end end + sel['$hint'] = query.hint if query.hint && query.hint.length > 0 + sel['$explain'] = true if query.explain + sel['$snapshot'] = true if query.snapshot end + write_doc(sel) + write_doc(query.fields) if query.fields + end + + def first_key(key) + @first_key = key end end end diff --git a/lib/mongo/message/remove_message.rb b/lib/mongo/message/remove_message.rb index f2da33c..2023e03 100644 --- a/lib/mongo/message/remove_message.rb +++ b/lib/mongo/message/remove_message.rb @@ -17,20 +17,16 @@ require 'mongo/message/message' require 'mongo/message/opcodes' -module XGen - module Mongo - module Driver +module Mongo - class RemoveMessage < Message + class RemoveMessage < Message - def initialize(db_name, collection_name, sel) - super(OP_DELETE) - write_int(0) - write_string("#{db_name}.#{collection_name}") - write_int(0) # flags? - write_doc(sel) - end - end + def initialize(db_name, collection_name, sel) + super(OP_DELETE) + write_int(0) + write_string("#{db_name}.#{collection_name}") + write_int(0) # flags? + write_doc(sel) end end end diff --git a/lib/mongo/message/update_message.rb b/lib/mongo/message/update_message.rb index 129b61f..99e7ba5 100644 --- a/lib/mongo/message/update_message.rb +++ b/lib/mongo/message/update_message.rb @@ -17,21 +17,17 @@ require 'mongo/message/message' require 'mongo/message/opcodes' -module XGen - module Mongo - module Driver +module Mongo - class UpdateMessage < Message + class UpdateMessage < Message - def initialize(db_name, collection_name, sel, obj, repsert) - super(OP_UPDATE) - write_int(0) - write_string("#{db_name}.#{collection_name}") - write_int(repsert ? 1 : 0) # 1 if a repsert operation (upsert) - write_doc(sel) - write_doc(obj) - end - end + def initialize(db_name, collection_name, sel, obj, repsert) + super(OP_UPDATE) + write_int(0) + write_string("#{db_name}.#{collection_name}") + write_int(repsert ? 1 : 0) # 1 if a repsert operation (upsert) + write_doc(sel) + write_doc(obj) end end end diff --git a/lib/mongo/mongo.rb b/lib/mongo/mongo.rb index abda843..a5cf03a 100644 --- a/lib/mongo/mongo.rb +++ b/lib/mongo/mongo.rb @@ -16,149 +16,144 @@ require 'mongo/db' -module XGen - module Mongo - module Driver +module Mongo - # Represents a Mongo database server. - class Mongo + # Represents a Mongo database server. + class Mongo - DEFAULT_PORT = 27017 + DEFAULT_PORT = 27017 - # Create a Mongo database server instance. You specify either one or a - # pair of servers. If one, you also say if connecting to a slave is - # OK. In either case, the host default is "localhost" and port default - # is DEFAULT_PORT. - # - # If you specify a pair, pair_or_host is a hash with two keys :left - # and :right. Each key maps to either - # * a server name, in which case port is DEFAULT_PORT - # * a port number, in which case server is "localhost" - # * an array containing a server name and a port number in that order - # - # +options+ are passed on to each DB instance: - # - # :slave_ok :: Only used if one host is specified. If false, when - # connecting to that host/port a DB object will check to - # see if the server is the master. If it is not, an error - # is thrown. - # - # :auto_reconnect :: If a DB connection gets closed (for example, we - # have a server pair and saw the "not master" - # error, which closes the connection), then - # automatically try to reconnect to the master or - # to the single server we have been given. Defaults - # to +false+. - # - # Since that's so confusing, here are a few examples: - # - # Mongo.new # localhost, DEFAULT_PORT, !slave - # Mongo.new("localhost") # localhost, DEFAULT_PORT, !slave - # Mongo.new("localhost", 3000) # localhost, 3000, slave not ok - # # localhost, 3000, slave ok - # Mongo.new("localhost", 3000, :slave_ok => true) - # # localhost, DEFAULT_PORT, auto reconnect - # Mongo.new(nil, nil, :auto_reconnect => true) - # - # # A pair of servers. DB will always talk to the master. On socket - # # error or "not master" error, we will auto-reconnect to the - # # current master. - # Mongo.new({:left => ["db1.example.com", 3000], - # :right => "db2.example.com"}, # DEFAULT_PORT - # nil, :auto_reconnect => true) - # - # # Here, :right is localhost/DEFAULT_PORT. No auto-reconnect. - # Mongo.new({:left => ["db1.example.com", 3000]}) - # - # When a DB object first connects to a pair, it will find the master - # instance and connect to that one. - def initialize(pair_or_host=nil, port=nil, options={}) - @pair = case pair_or_host - when String - [[pair_or_host, port ? port.to_i : DEFAULT_PORT]] - when Hash - connections = [] - connections << pair_val_to_connection(pair_or_host[:left]) - connections << pair_val_to_connection(pair_or_host[:right]) - connections - when nil - [['localhost', DEFAULT_PORT]] - end - @options = options - end + # Create a Mongo database server instance. You specify either one or a + # pair of servers. If one, you also say if connecting to a slave is + # OK. In either case, the host default is "localhost" and port default + # is DEFAULT_PORT. + # + # If you specify a pair, pair_or_host is a hash with two keys :left + # and :right. Each key maps to either + # * a server name, in which case port is DEFAULT_PORT + # * a port number, in which case server is "localhost" + # * an array containing a server name and a port number in that order + # + # +options+ are passed on to each DB instance: + # + # :slave_ok :: Only used if one host is specified. If false, when + # connecting to that host/port a DB object will check to + # see if the server is the master. If it is not, an error + # is thrown. + # + # :auto_reconnect :: If a DB connection gets closed (for example, we + # have a server pair and saw the "not master" + # error, which closes the connection), then + # automatically try to reconnect to the master or + # to the single server we have been given. Defaults + # to +false+. + # + # Since that's so confusing, here are a few examples: + # + # Mongo.new # localhost, DEFAULT_PORT, !slave + # Mongo.new("localhost") # localhost, DEFAULT_PORT, !slave + # Mongo.new("localhost", 3000) # localhost, 3000, slave not ok + # # localhost, 3000, slave ok + # Mongo.new("localhost", 3000, :slave_ok => true) + # # localhost, DEFAULT_PORT, auto reconnect + # Mongo.new(nil, nil, :auto_reconnect => true) + # + # # A pair of servers. DB will always talk to the master. On socket + # # error or "not master" error, we will auto-reconnect to the + # # current master. + # Mongo.new({:left => ["db1.example.com", 3000], + # :right => "db2.example.com"}, # DEFAULT_PORT + # nil, :auto_reconnect => true) + # + # # Here, :right is localhost/DEFAULT_PORT. No auto-reconnect. + # Mongo.new({:left => ["db1.example.com", 3000]}) + # + # When a DB object first connects to a pair, it will find the master + # instance and connect to that one. + def initialize(pair_or_host=nil, port=nil, options={}) + @pair = case pair_or_host + when String + [[pair_or_host, port ? port.to_i : DEFAULT_PORT]] + when Hash + connections = [] + connections << pair_val_to_connection(pair_or_host[:left]) + connections << pair_val_to_connection(pair_or_host[:right]) + connections + when nil + [['localhost', DEFAULT_PORT]] + end + @options = options + end - # Return the XGen::Mongo::Driver::DB named +db_name+. The slave_ok and - # auto_reconnect options passed in via #new may be overridden here. - # See DB#new for other options you can pass in. - def db(db_name, options={}) - XGen::Mongo::Driver::DB.new(db_name, @pair, @options.merge(options)) - end + # Return the Mongo::DB named +db_name+. The slave_ok and + # auto_reconnect options passed in via #new may be overridden here. + # See DB#new for other options you can pass in. + def db(db_name, options={}) + DB.new(db_name, @pair, @options.merge(options)) + end - # Returns a hash containing database names as keys and disk space for - # each as values. - def database_info - doc = single_db_command('admin', :listDatabases => 1) - h = {} - doc['databases'].each { |db| - h[db['name']] = db['sizeOnDisk'].to_i - } - h - end + # Returns a hash containing database names as keys and disk space for + # each as values. + def database_info + doc = single_db_command('admin', :listDatabases => 1) + h = {} + doc['databases'].each { |db| + h[db['name']] = db['sizeOnDisk'].to_i + } + h + end - # Returns an array of database names. - def database_names - database_info.keys - end + # Returns an array of database names. + def database_names + database_info.keys + end - # Not implemented. - def clone_database(from) - raise "not implemented" - end + # Not implemented. + def clone_database(from) + raise "not implemented" + end - # Not implemented. - def copy_database(from_host, from_db, to_db) - raise "not implemented" - end + # Not implemented. + def copy_database(from_host, from_db, to_db) + raise "not implemented" + end - # Drops the database +name+. - def drop_database(name) - single_db_command(name, :dropDatabase => 1) - end + # Drops the database +name+. + def drop_database(name) + single_db_command(name, :dropDatabase => 1) + end - protected - - # Turns an array containing a host name string and a - # port number integer into a [host, port] pair array. - def pair_val_to_connection(a) - case a - when nil - ['localhost', DEFAULT_PORT] - when String - [a, DEFAULT_PORT] - when Integer - ['localhost', a] - when Array - a - end - end - - # Send cmd (a hash, possibly ordered) to the admin database and return - # the answer. Raises an error unless the return is "ok" (DB#ok? - # returns +true+). - def single_db_command(db_name, cmd) - db = nil - begin - db = db(db_name) - doc = db.db_command(cmd) - raise "error retrieving database info: #{doc.inspect}" unless db.ok?(doc) - doc - ensure - db.close if db - end - end + protected + # Turns an array containing a host name string and a + # port number integer into a [host, port] pair array. + def pair_val_to_connection(a) + case a + when nil + ['localhost', DEFAULT_PORT] + when String + [a, DEFAULT_PORT] + when Integer + ['localhost', a] + when Array + a end end + + # Send cmd (a hash, possibly ordered) to the admin database and return + # the answer. Raises an error unless the return is "ok" (DB#ok? + # returns +true+). + def single_db_command(db_name, cmd) + db = nil + begin + db = db(db_name) + doc = db.db_command(cmd) + raise "error retrieving database info: #{doc.inspect}" unless db.ok?(doc) + doc + ensure + db.close if db + end + end + end end - diff --git a/lib/mongo/query.rb b/lib/mongo/query.rb index 9e9a017..4d85826 100644 --- a/lib/mongo/query.rb +++ b/lib/mongo/query.rb @@ -18,102 +18,98 @@ require 'mongo/collection' require 'mongo/message' require 'mongo/types/code' -module XGen - module Mongo - module Driver +module Mongo - # A query against a collection. A query's selector is a hash. See the - # Mongo documentation for query details. - class Query + # A query against a collection. A query's selector is a hash. See the + # Mongo documentation for query details. + class Query - attr_accessor :number_to_skip, :number_to_return, :order_by, :snapshot - # If true, $explain will be set in QueryMessage that uses this query. - attr_accessor :explain - # Either +nil+ or a hash (preferably an OrderedHash). - attr_accessor :hint - attr_reader :selector # writer defined below + attr_accessor :number_to_skip, :number_to_return, :order_by, :snapshot + # If true, $explain will be set in QueryMessage that uses this query. + attr_accessor :explain + # Either +nil+ or a hash (preferably an OrderedHash). + attr_accessor :hint + attr_reader :selector # writer defined below - # sel :: A hash describing the query. See the Mongo docs for details. - # - # return_fields :: If not +nil+, a single field name or an array of - # field names. Only those fields will be returned. - # (Called :fields in calls to Collection#find.) - # - # number_to_skip :: Number of records to skip before returning - # records. (Called :offset in calls to - # Collection#find.) Default is 0. - # - # number_to_return :: Max number of records to return. (Called :limit - # in calls to Collection#find.) Default is 0 (all - # records). - # - # order_by :: If not +nil+, specifies record sort order. May be a - # String, Hash, OrderedHash, or Array. If a string, the - # results will be ordered by that field in ascending - # order. If an array, it should be an array of field names - # which will all be sorted in ascending order. If a hash, - # it may be either a regular Hash or an OrderedHash. The - # keys should be field names, and the values should be 1 - # (ascending) or -1 (descending). Note that if it is a - # regular Hash then sorting by more than one field - # probably will not be what you intend because key order - # is not preserved. (order_by is called :sort in calls to - # Collection#find.) - # - # hint :: If not +nil+, specifies query hint fields. Must be either - # +nil+ or a hash (preferably an OrderedHash). See - # Collection#hint. - def initialize(sel={}, return_fields=nil, number_to_skip=0, number_to_return=0, order_by=nil, hint=nil, snapshot=nil) - @number_to_skip, @number_to_return, @order_by, @hint, @snapshot = - number_to_skip, number_to_return, order_by, hint, snapshot - @explain = nil - self.selector = sel - self.fields = return_fields - end - - # Set query selector hash. If sel is Code/string, it will be used as a - # $where clause. (See Mongo docs for details.) - def selector=(sel) - @selector = case sel - when nil - {} - when Code - {"$where" => sel} - when String - {"$where" => Code.new(sel)} - when Hash - sel - end - end - - # Set fields to return. If +val+ is +nil+ or empty, all fields will be - # returned. - def fields=(val) - @fields = val - @fields = nil if @fields && @fields.empty? - end - - def fields - case @fields - when String - {@fields => 1} - when Array - if @fields.length == 0 - nil - else - h = {} - @fields.each { |field| h[field] = 1 } - h - end - else # nil, anything else - nil - end - end - - def contains_special_fields - (@order_by != nil && @order_by.length > 0) || @explain || @hint || @snapshot + # sel :: A hash describing the query. See the Mongo docs for details. + # + # return_fields :: If not +nil+, a single field name or an array of + # field names. Only those fields will be returned. + # (Called :fields in calls to Collection#find.) + # + # number_to_skip :: Number of records to skip before returning + # records. (Called :offset in calls to + # Collection#find.) Default is 0. + # + # number_to_return :: Max number of records to return. (Called :limit + # in calls to Collection#find.) Default is 0 (all + # records). + # + # order_by :: If not +nil+, specifies record sort order. May be a + # String, Hash, OrderedHash, or Array. If a string, the + # results will be ordered by that field in ascending + # order. If an array, it should be an array of field names + # which will all be sorted in ascending order. If a hash, + # it may be either a regular Hash or an OrderedHash. The + # keys should be field names, and the values should be 1 + # (ascending) or -1 (descending). Note that if it is a + # regular Hash then sorting by more than one field + # probably will not be what you intend because key order + # is not preserved. (order_by is called :sort in calls to + # Collection#find.) + # + # hint :: If not +nil+, specifies query hint fields. Must be either + # +nil+ or a hash (preferably an OrderedHash). See + # Collection#hint. + def initialize(sel={}, return_fields=nil, number_to_skip=0, number_to_return=0, order_by=nil, hint=nil, snapshot=nil) + @number_to_skip, @number_to_return, @order_by, @hint, @snapshot = + number_to_skip, number_to_return, order_by, hint, snapshot + @explain = nil + self.selector = sel + self.fields = return_fields + end + + # Set query selector hash. If sel is Code/string, it will be used as a + # $where clause. (See Mongo docs for details.) + def selector=(sel) + @selector = case sel + when nil + {} + when Code + {"$where" => sel} + when String + {"$where" => Code.new(sel)} + when Hash + sel + end + end + + # Set fields to return. If +val+ is +nil+ or empty, all fields will be + # returned. + def fields=(val) + @fields = val + @fields = nil if @fields && @fields.empty? + end + + def fields + case @fields + when String + {@fields => 1} + when Array + if @fields.length == 0 + nil + else + h = {} + @fields.each { |field| h[field] = 1 } + h end + else # nil, anything else + nil end end + + def contains_special_fields + (@order_by != nil && @order_by.length > 0) || @explain || @hint || @snapshot + end end end diff --git a/lib/mongo/types/binary.rb b/lib/mongo/types/binary.rb index 40d2e46..37422d6 100644 --- a/lib/mongo/types/binary.rb +++ b/lib/mongo/types/binary.rb @@ -16,27 +16,23 @@ require 'mongo/util/byte_buffer' -module XGen - module Mongo - module Driver +module Mongo - # An array of binary bytes with a Mongo subtype value. - class Binary < ByteBuffer + # An array of binary bytes with a Mongo subtype value. + class Binary < ByteBuffer - SUBTYPE_BYTES = 0x02 - SUBTYPE_UUID = 0x03 - SUBTYPE_MD5 = 0x05 - SUBTYPE_USER_DEFINED = 0x80 + SUBTYPE_BYTES = 0x02 + SUBTYPE_UUID = 0x03 + SUBTYPE_MD5 = 0x05 + SUBTYPE_USER_DEFINED = 0x80 - # One of the SUBTYPE_* constants. Default is SUBTYPE_BYTES. - attr_accessor :subtype + # One of the SUBTYPE_* constants. Default is SUBTYPE_BYTES. + attr_accessor :subtype - def initialize(initial_data=[], subtype=SUBTYPE_BYTES) - super(initial_data) - @subtype = subtype - end - - end + def initialize(initial_data=[], subtype=SUBTYPE_BYTES) + super(initial_data) + @subtype = subtype end + end end diff --git a/lib/mongo/types/code.rb b/lib/mongo/types/code.rb index f23a416..09f61f0 100644 --- a/lib/mongo/types/code.rb +++ b/lib/mongo/types/code.rb @@ -14,21 +14,17 @@ # limitations under the License. # ++ -module XGen - module Mongo - module Driver +module Mongo - # JavaScript code to be evaluated by MongoDB - class Code < String - # Hash mapping identifiers to their values - attr_accessor :scope + # JavaScript code to be evaluated by MongoDB + class Code < String + # Hash mapping identifiers to their values + attr_accessor :scope - def initialize(code, scope={}) - super(code) - @scope = scope - end - - end + def initialize(code, scope={}) + super(code) + @scope = scope end + end end diff --git a/lib/mongo/types/dbref.rb b/lib/mongo/types/dbref.rb index c09ef26..2c1f4d7 100644 --- a/lib/mongo/types/dbref.rb +++ b/lib/mongo/types/dbref.rb @@ -14,24 +14,20 @@ # limitations under the License. # ++ -module XGen - module Mongo - module Driver +module Mongo - class DBRef + class DBRef - attr_reader :namespace, :object_id + attr_reader :namespace, :object_id - def initialize(namespace, object_id) - @namespace, @object_id = - namespace, object_id - end - - def to_s - "ns: #{namespace}, id: #{object_id}" - end - - end + def initialize(namespace, object_id) + @namespace, @object_id = + namespace, object_id end + + def to_s + "ns: #{namespace}, id: #{object_id}" + end + end end diff --git a/lib/mongo/types/objectid.rb b/lib/mongo/types/objectid.rb index d81b0ad..d4ebb61 100644 --- a/lib/mongo/types/objectid.rb +++ b/lib/mongo/types/objectid.rb @@ -17,121 +17,117 @@ require 'mutex_m' require 'mongo/util/byte_buffer' -module XGen - module Mongo - module Driver +module Mongo - # Implementation of the Babble OID. Object ids are not required by - # Mongo, but they make certain operations more efficient. - # - # The driver does not automatically assign ids to records that are - # inserted. (An upcoming feature will allow you to give an id "factory" - # to a database and/or a collection.) - # - # 12 bytes - # --- - # 0 time - # 1 - # 2 - # 3 - # 4 machine - # 5 - # 6 - # 7 pid - # 8 - # 9 inc - # 10 - # 11 - class ObjectID + # Implementation of the Babble OID. Object ids are not required by + # Mongo, but they make certain operations more efficient. + # + # The driver does not automatically assign ids to records that are + # inserted. (An upcoming feature will allow you to give an id "factory" + # to a database and/or a collection.) + # + # 12 bytes + # --- + # 0 time + # 1 + # 2 + # 3 + # 4 machine + # 5 + # 6 + # 7 pid + # 8 + # 9 inc + # 10 + # 11 + class ObjectID - MACHINE = ( val = rand(0x1000000); [val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff] ) - PID = ( val = rand(0x10000); [val & 0xff, (val >> 8) & 0xff]; ) + MACHINE = ( val = rand(0x1000000); [val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff] ) + PID = ( val = rand(0x10000); [val & 0xff, (val >> 8) & 0xff]; ) - # The string representation of an OID is different than its internal - # and BSON byte representations. The BYTE_ORDER here maps - # internal/BSON byte position (the index in BYTE_ORDER) to the - # position of the two hex characters representing that byte in the - # string representation. For example, the 0th BSON byte corresponds to - # the (0-based) 7th pair of hex chars in the string. - BYTE_ORDER = [7, 6, 5, 4, 3, 2, 1, 0, 11, 10, 9, 8] + # The string representation of an OID is different than its internal + # and BSON byte representations. The BYTE_ORDER here maps + # internal/BSON byte position (the index in BYTE_ORDER) to the + # position of the two hex characters representing that byte in the + # string representation. For example, the 0th BSON byte corresponds to + # the (0-based) 7th pair of hex chars in the string. + BYTE_ORDER = [7, 6, 5, 4, 3, 2, 1, 0, 11, 10, 9, 8] - LOCK = Object.new - LOCK.extend Mutex_m + LOCK = Object.new + LOCK.extend Mutex_m - @@index_time = Time.new.to_i - @@index = 0 + @@index_time = Time.new.to_i + @@index = 0 - # Given a string representation of an ObjectID, return a new ObjectID - # with that value. - def self.from_string(str) - raise "illegal ObjectID format" unless legal?(str) - data = [] - BYTE_ORDER.each_with_index { |string_position, data_index| - data[data_index] = str[string_position * 2, 2].to_i(16) - } - self.new(data) - end - - def self.legal?(str) - len = BYTE_ORDER.length * 2 - str =~ /([0-9a-f]+)/i - match = $1 - str && str.length == len && match == str - end - - # +data+ is an array of bytes. If nil, a new id will be generated. - # The time +t+ is only used for testing; leave it nil. - def initialize(data=nil, t=nil) - @data = data || generate_id(t) - end - - def eql?(other) - @data == other.instance_variable_get("@data") - end - alias_method :==, :eql? - - def to_a - @data.dup - end - - def to_s - str = ' ' * 24 - BYTE_ORDER.each_with_index { |string_position, data_index| - str[string_position * 2, 2] = '%02x' % @data[data_index] - } - str - end - - # (Would normally be private, but isn't so we can test it.) - def generate_id(t=nil) - t ||= Time.new.to_i - buf = ByteBuffer.new - buf.put_int(t & 0xffffffff) - buf.put_array(MACHINE) - buf.put_array(PID) - i = index_for_time(t) - buf.put(i & 0xff) - buf.put((i >> 8) & 0xff) - buf.put((i >> 16) & 0xff) - - buf.rewind - buf.to_a.dup - end - - # (Would normally be private, but isn't so we can test it.) - def index_for_time(t) - LOCK.mu_synchronize { - if t != @@index_time - @@index = 0 - @@index_time = t - end - retval = @@index - @@index += 1 - retval - } - end - - end + # Given a string representation of an ObjectID, return a new ObjectID + # with that value. + def self.from_string(str) + raise "illegal ObjectID format" unless legal?(str) + data = [] + BYTE_ORDER.each_with_index { |string_position, data_index| + data[data_index] = str[string_position * 2, 2].to_i(16) + } + self.new(data) end + + def self.legal?(str) + len = BYTE_ORDER.length * 2 + str =~ /([0-9a-f]+)/i + match = $1 + str && str.length == len && match == str + end + + # +data+ is an array of bytes. If nil, a new id will be generated. + # The time +t+ is only used for testing; leave it nil. + def initialize(data=nil, t=nil) + @data = data || generate_id(t) + end + + def eql?(other) + @data == other.instance_variable_get("@data") + end + alias_method :==, :eql? + + def to_a + @data.dup + end + + def to_s + str = ' ' * 24 + BYTE_ORDER.each_with_index { |string_position, data_index| + str[string_position * 2, 2] = '%02x' % @data[data_index] + } + str + end + + # (Would normally be private, but isn't so we can test it.) + def generate_id(t=nil) + t ||= Time.new.to_i + buf = ByteBuffer.new + buf.put_int(t & 0xffffffff) + buf.put_array(MACHINE) + buf.put_array(PID) + i = index_for_time(t) + buf.put(i & 0xff) + buf.put((i >> 8) & 0xff) + buf.put((i >> 16) & 0xff) + + buf.rewind + buf.to_a.dup + end + + # (Would normally be private, but isn't so we can test it.) + def index_for_time(t) + LOCK.mu_synchronize { + if t != @@index_time + @@index = 0 + @@index_time = t + end + retval = @@index + @@index += 1 + retval + } + end + end end diff --git a/lib/mongo/types/regexp_of_holding.rb b/lib/mongo/types/regexp_of_holding.rb index 92dbfd8..2ebf19d 100644 --- a/lib/mongo/types/regexp_of_holding.rb +++ b/lib/mongo/types/regexp_of_holding.rb @@ -14,31 +14,27 @@ # limitations under the License. # ++ -module XGen - module Mongo - module Driver +module Mongo - # A Regexp that can hold on to extra options and ignore them. Mongo - # regexes may contain option characters beyond 'i', 'm', and 'x'. (Note - # that Mongo only uses those three, but that regexes coming from other - # languages may store different option characters.) - # - # Note that you do not have to use this class at all if you wish to - # store regular expressions in Mongo. The Mongo and Ruby regex option - # flags are the same. Storing regexes is discouraged, in any case. - class RegexpOfHolding < Regexp + # A Regexp that can hold on to extra options and ignore them. Mongo + # regexes may contain option characters beyond 'i', 'm', and 'x'. (Note + # that Mongo only uses those three, but that regexes coming from other + # languages may store different option characters.) + # + # Note that you do not have to use this class at all if you wish to + # store regular expressions in Mongo. The Mongo and Ruby regex option + # flags are the same. Storing regexes is discouraged, in any case. + class RegexpOfHolding < Regexp - attr_accessor :extra_options_str - - # +str+ and +options+ are the same as Regexp. +extra_options_str+ - # contains all the other flags that were in Mongo but we do not use or - # understand. - def initialize(str, options, extra_options_str) - super(str, options) - @extra_options_str = extra_options_str - end - end + attr_accessor :extra_options_str + # +str+ and +options+ are the same as Regexp. +extra_options_str+ + # contains all the other flags that were in Mongo but we do not use or + # understand. + def initialize(str, options, extra_options_str) + super(str, options) + @extra_options_str = extra_options_str end end + end diff --git a/lib/mongo/types/undefined.rb b/lib/mongo/types/undefined.rb index 72face1..d883579 100644 --- a/lib/mongo/types/undefined.rb +++ b/lib/mongo/types/undefined.rb @@ -14,19 +14,15 @@ # limitations under the License. # ++ -module XGen - module Mongo - module Driver +module Mongo - # DEPRECATED - the ruby driver converts the BSON undefined type to nil, - # and saves this type as nil - class Undefined < Object + # DEPRECATED - the ruby driver converts the BSON undefined type to nil, + # and saves this type as nil + class Undefined < Object - def initialize - super - warn "the Undefined type is deprecated and will be removed - BSON undefineds get implicitely converted to nil now" - end - end + def initialize + super + warn "the Undefined type is deprecated and will be removed - BSON undefineds get implicitely converted to nil now" end end end diff --git a/lib/mongo/util/bson.rb b/lib/mongo/util/bson.rb index bfa0b25..023136c 100644 --- a/lib/mongo/util/bson.rb +++ b/lib/mongo/util/bson.rb @@ -26,7 +26,7 @@ require 'mongo/types/undefined' # A BSON seralizer/deserializer. class BSON - include XGen::Mongo::Driver + include Mongo MINKEY = -1 EOO = 0 diff --git a/lib/mongo/util/xml_to_ruby.rb b/lib/mongo/util/xml_to_ruby.rb index dfc3ec5..59e105f 100644 --- a/lib/mongo/util/xml_to_ruby.rb +++ b/lib/mongo/util/xml_to_ruby.rb @@ -21,7 +21,7 @@ require 'mongo' # an OrderedHash. class XMLToRuby - include XGen::Mongo::Driver + include Mongo def xml_to_ruby(io) doc = REXML::Document.new(io) diff --git a/test/mongo-qa/_common.rb b/test/mongo-qa/_common.rb index 2760de5..fc0f9bb 100644 --- a/test/mongo-qa/_common.rb +++ b/test/mongo-qa/_common.rb @@ -5,4 +5,4 @@ DEFAULT_HOST = '127.0.0.1' DEFAULT_PORT = 27017 DEFAULT_DB = 'driver_test_framework' -include XGen::Mongo::Driver +include Mongo diff --git a/test/mongo-qa/gridfs_in b/test/mongo-qa/gridfs_in index bb39fd3..732238d 100644 --- a/test/mongo-qa/gridfs_in +++ b/test/mongo-qa/gridfs_in @@ -3,9 +3,9 @@ require File.join(File.dirname(__FILE__), '_common.rb') require 'mongo/gridfs' -include XGen::Mongo::GridFS +include GridFS -db = Mongo.new(DEFAULT_HOST, DEFAULT_PORT).db(DEFAULT_DB) +db = Mongo::Mongo.new(DEFAULT_HOST, DEFAULT_PORT).db(DEFAULT_DB) input_file = ARGV[0] diff --git a/test/mongo-qa/gridfs_out b/test/mongo-qa/gridfs_out index 7313b74..49492e6 100644 --- a/test/mongo-qa/gridfs_out +++ b/test/mongo-qa/gridfs_out @@ -3,9 +3,9 @@ require File.join(File.dirname(__FILE__), '_common.rb') require 'mongo/gridfs' -include XGen::Mongo::GridFS +include GridFS -db = Mongo.new(DEFAULT_HOST, DEFAULT_PORT).db(DEFAULT_DB) +db = Mongo::Mongo.new(DEFAULT_HOST, DEFAULT_PORT).db(DEFAULT_DB) input_file = ARGV[0] output_file = ARGV[1] diff --git a/test/mongo-qa/indices b/test/mongo-qa/indices index 0156a71..45649bf 100755 --- a/test/mongo-qa/indices +++ b/test/mongo-qa/indices @@ -2,9 +2,9 @@ require File.join(File.dirname(__FILE__), '_common.rb') -include XGen::Mongo +include Mongo -db = Mongo.new(DEFAULT_HOST, DEFAULT_PORT).db(DEFAULT_DB) +db = Mongo::Mongo.new(DEFAULT_HOST, DEFAULT_PORT).db(DEFAULT_DB) x = db.collection('x') y = db.collection('y') diff --git a/test/test_admin.rb b/test/test_admin.rb index 15a0492..61c7761 100644 --- a/test/test_admin.rb +++ b/test/test_admin.rb @@ -5,7 +5,7 @@ require 'test/unit' # NOTE: assumes Mongo is running class AdminTest < Test::Unit::TestCase - include XGen::Mongo::Driver + include Mongo @@db = Mongo.new(ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost', ENV['MONGO_RUBY_DRIVER_PORT'] || Mongo::DEFAULT_PORT).db('ruby-mongo-test') diff --git a/test/test_bson.rb b/test/test_bson.rb index 78b2673..30af1b1 100644 --- a/test/test_bson.rb +++ b/test/test_bson.rb @@ -5,7 +5,7 @@ require 'test/unit' class BSONTest < Test::Unit::TestCase - include XGen::Mongo::Driver + include Mongo def setup # We don't pass a DB to the constructor, even though we are about to test @@ -85,7 +85,7 @@ class BSONTest < Test::Unit::TestCase assert_equal doc, doc2 r = doc2['doc'] - assert_kind_of XGen::Mongo::Driver::RegexpOfHolding, r + assert_kind_of RegexpOfHolding, r assert_equal '', r.extra_options_str r.extra_options_str << 'zywcab' @@ -99,7 +99,7 @@ class BSONTest < Test::Unit::TestCase assert_equal doc, doc2 r = doc2['doc'] - assert_kind_of XGen::Mongo::Driver::RegexpOfHolding, r + assert_kind_of RegexpOfHolding, r assert_equal 'abcwyz', r.extra_options_str # must be sorted end diff --git a/test/test_chunk.rb b/test/test_chunk.rb index b7dcdd8..1bc48ce 100644 --- a/test/test_chunk.rb +++ b/test/test_chunk.rb @@ -5,8 +5,8 @@ require 'mongo/gridfs' class ChunkTest < Test::Unit::TestCase - include XGen::Mongo::Driver - include XGen::Mongo::GridFS + include Mongo + include GridFS @@db = Mongo.new(ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost', ENV['MONGO_RUBY_DRIVER_PORT'] || Mongo::DEFAULT_PORT).db('ruby-mongo-utils-test') diff --git a/test/test_collection.rb b/test/test_collection.rb index 1347daa..455e994 100644 --- a/test/test_collection.rb +++ b/test/test_collection.rb @@ -20,8 +20,7 @@ require 'test/unit' # NOTE: assumes Mongo is running class TestCollection < Test::Unit::TestCase - include XGen::Mongo - include XGen::Mongo::Driver + include Mongo @@db = Mongo.new(ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost', ENV['MONGO_RUBY_DRIVER_PORT'] || Mongo::DEFAULT_PORT).db('ruby-mongo-test') diff --git a/test/test_cursor.rb b/test/test_cursor.rb index a93ceaa..aedad12 100644 --- a/test/test_cursor.rb +++ b/test/test_cursor.rb @@ -5,7 +5,7 @@ require 'test/unit' # NOTE: assumes Mongo is running class CursorTest < Test::Unit::TestCase - include XGen::Mongo::Driver + include Mongo @@db = Mongo.new(ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost', ENV['MONGO_RUBY_DRIVER_PORT'] || Mongo::DEFAULT_PORT).db('ruby-mongo-test') diff --git a/test/test_db.rb b/test/test_db.rb index 3163ec9..a5833c4 100644 --- a/test/test_db.rb +++ b/test/test_db.rb @@ -5,7 +5,7 @@ require 'test/unit' class TestPKFactory def create_pk(row) - row['_id'] ||= XGen::Mongo::Driver::ObjectID.new + row['_id'] ||= Mongo::ObjectID.new row end end @@ -13,7 +13,7 @@ end # NOTE: assumes Mongo is running class DBTest < Test::Unit::TestCase - include XGen::Mongo::Driver + include Mongo @@host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost' @@port = ENV['MONGO_RUBY_DRIVER_PORT'] || Mongo::DEFAULT_PORT @@ -85,7 +85,7 @@ class DBTest < Test::Unit::TestCase assert_not_nil oid assert_equal insert_id, oid - oid = XGen::Mongo::Driver::ObjectID.new + oid = ObjectID.new data = {'_id' => oid, 'name' => 'Barney', 'age' => 41} coll.insert(data) row = coll.find_one({'name' => data['name']}) diff --git a/test/test_db_api.rb b/test/test_db_api.rb index 3835d64..7acf1ff 100644 --- a/test/test_db_api.rb +++ b/test/test_db_api.rb @@ -4,8 +4,7 @@ require 'test/unit' # NOTE: assumes Mongo is running class DBAPITest < Test::Unit::TestCase - include XGen::Mongo - include XGen::Mongo::Driver + include Mongo @@db = Mongo.new(ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost', ENV['MONGO_RUBY_DRIVER_PORT'] || Mongo::DEFAULT_PORT).db('ruby-mongo-test') @@ -817,18 +816,4 @@ class DBAPITest < Test::Unit::TestCase @@db.collection("test").find({}, :snapshot => true, :sort => 'a').to_a end end - -# TODO this test fails with error message "Undefed Before end of object" -# That is a database error. The undefined type may go away. - -# def test_insert_undefined -# doc = {'undef' => Undefined.new} -# @@coll.clear -# @@coll.insert(doc) -# p @@db.error # DEBUG -# assert_equal 1, @@coll.count -# row = @@coll.find().next_object -# assert_not_nil row -# end - end diff --git a/test/test_db_connection.rb b/test/test_db_connection.rb index dab44bc..34f7853 100644 --- a/test/test_db_connection.rb +++ b/test/test_db_connection.rb @@ -5,7 +5,7 @@ require 'test/unit' # NOTE: assumes Mongo is running class DBConnectionTest < Test::Unit::TestCase - include XGen::Mongo::Driver + include Mongo def test_no_exceptions host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost' diff --git a/test/test_grid_store.rb b/test/test_grid_store.rb index 0b82d0b..459acc8 100644 --- a/test/test_grid_store.rb +++ b/test/test_grid_store.rb @@ -5,8 +5,8 @@ require 'mongo/gridfs' class GridStoreTest < Test::Unit::TestCase - include XGen::Mongo::Driver - include XGen::Mongo::GridFS + include Mongo + include GridFS @@db = Mongo.new(ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost', ENV['MONGO_RUBY_DRIVER_PORT'] || Mongo::DEFAULT_PORT).db('ruby-mongo-test') diff --git a/test/test_message.rb b/test/test_message.rb index a648a9a..4765658 100644 --- a/test/test_message.rb +++ b/test/test_message.rb @@ -4,7 +4,7 @@ require 'test/unit' class MessageTest < Test::Unit::TestCase - include XGen::Mongo::Driver + include Mongo def setup @msg = Message.new(42) diff --git a/test/test_mongo.rb b/test/test_mongo.rb index 1265661..764da13 100644 --- a/test/test_mongo.rb +++ b/test/test_mongo.rb @@ -5,7 +5,7 @@ require 'test/unit' # NOTE: assumes Mongo is running class MongoTest < Test::Unit::TestCase - include XGen::Mongo::Driver + include Mongo def setup @host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost' diff --git a/test/test_objectid.rb b/test/test_objectid.rb index 4adaa7e..79470b7 100644 --- a/test/test_objectid.rb +++ b/test/test_objectid.rb @@ -4,7 +4,7 @@ require 'test/unit' class ObjectIDTest < Test::Unit::TestCase - include XGen::Mongo::Driver + include Mongo def setup @t = 42 diff --git a/test/test_round_trip.rb b/test/test_round_trip.rb index a6faad9..4991643 100644 --- a/test/test_round_trip.rb +++ b/test/test_round_trip.rb @@ -13,7 +13,7 @@ require 'test/unit' # of this project), then we find the BSON test files there and use those, too. class RoundTripTest < Test::Unit::TestCase - include XGen::Mongo::Driver + include Mongo @@ruby = nil diff --git a/test/test_threading.rb b/test/test_threading.rb index 7679a41..e1befb6 100644 --- a/test/test_threading.rb +++ b/test/test_threading.rb @@ -4,7 +4,7 @@ require 'test/unit' class TestThreading < Test::Unit::TestCase - include XGen::Mongo::Driver + include Mongo @@host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost' @@port = ENV['MONGO_RUBY_DRIVER_PORT'] || Mongo::DEFAULT_PORT