object_id_gem 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ # What's the current version of this gem?
2
+ module ObjectidColumns
3
+ VERSION = "1.0.6"
4
+ end
@@ -0,0 +1,71 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'objectid_columns/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "object_id_gem"
8
+ spec.version = ObjectidColumns::VERSION
9
+ spec.authors = ["Andrew Geweke"]
10
+ spec.email = ["ageweke@swiftype.com"]
11
+ spec.summary = %q{Transparently store MongoDB ObjectId values in ActiveRecord.}
12
+ spec.homepage = "https://www.github.com/swiftype/objectid_columns"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+
21
+ ar_version = ENV['OBJECTID_COLUMNS_AR_TEST_VERSION']
22
+ ar_version = ar_version.strip if ar_version
23
+
24
+ version_spec = case ar_version
25
+ when nil then [ ">= 5.0"]
26
+ when 'master' then nil
27
+ else [ "=#{ar_version}" ]
28
+ end
29
+
30
+ if version_spec
31
+ spec.add_dependency("activerecord", *version_spec)
32
+ spec.add_dependency("activesupport", *version_spec)
33
+ end
34
+
35
+ spec.add_development_dependency "bundler", "~> 1.5"
36
+ spec.add_development_dependency "rake"
37
+ spec.add_development_dependency "rspec", "~> 2.14"
38
+ spec.add_development_dependency "moped", "~> 1.5" unless RUBY_VERSION =~ /^1\.8\./
39
+ spec.add_development_dependency "bson", "~> 1.9"
40
+
41
+ require File.expand_path(File.join(File.dirname(__FILE__), 'spec', 'objectid_columns', 'helpers', 'database_helper'))
42
+ database_gem_name = ObjectidColumns::Helpers::DatabaseHelper.maybe_database_gem_name
43
+
44
+ # Ugh. Later versions of the 'mysql2' gem are incompatible with AR 3.0.x; so, here, we explicitly trap that case
45
+ # and use an earlier version of that Gem.
46
+ if database_gem_name && database_gem_name == 'mysql2' && ar_version && ar_version =~ /^3\.0\./
47
+ spec.add_development_dependency('mysql2', '~> 0.2.0')
48
+ else
49
+ spec.add_development_dependency(database_gem_name)
50
+ end
51
+
52
+ # Double ugh. Basically, composite_primary_keys -- as useful as it is! -- is also incredibly incompatible with so
53
+ # much stuff:
54
+ #
55
+ # * Under Ruby 1.9+ with Postgres, it causes binary strings sent to or from the database to get truncated
56
+ # at the first null byte (!), which completely breaks binary-column support;
57
+ # * Under JRuby with ActiveRecord 3.0, it's completely broken;
58
+ # * Under JRuby with ActiveRecord 3.1 and PostgreSQL, it's also broken.
59
+ #
60
+ # In these cases, we simply don't load or test against composite_primary_keys; our code is good, but the interactions
61
+ # between CPK and the rest of the system make it impossible to run those tests. There is corresponding code in our
62
+ # +basic_system_spec+ to exclude those combinations.
63
+ cpk_allowed = true
64
+ cpk_allowed = false if database_gem_name =~ /(pg|postgres)/i && RUBY_VERSION =~ /^(1\.9)|(2\.)/ && ar_version && ar_version =~ /^4\.(0|1)\./
65
+ cpk_allowed = false if defined?(RUBY_ENGINE) && (RUBY_ENGINE == 'jruby') && ar_version && ar_version =~ /^3\.0\./
66
+ cpk_allowed = false if defined?(RUBY_ENGINE) && (RUBY_ENGINE == 'jruby') && ar_version && ar_version =~ /^3\.1\./ && database_gem_name =~ /(pg|postgres)/i
67
+
68
+ if cpk_allowed
69
+ spec.add_development_dependency "composite_primary_keys"
70
+ end
71
+ end
@@ -0,0 +1,178 @@
1
+ module ObjectidColumns
2
+ module Helpers
3
+ class DatabaseHelper
4
+ class InvalidDatabaseConfigurationError < StandardError; end
5
+
6
+ class << self
7
+ def maybe_database_gem_name
8
+ begin
9
+ dh = new
10
+ dh.database_gem_name
11
+ rescue InvalidDatabaseConfigurationError => idce
12
+ nil
13
+ end
14
+ end
15
+ end
16
+
17
+ def initialize
18
+ config # make sure we raise on instantiation if configuration is invalid
19
+ end
20
+
21
+ def database_type
22
+ case database_gem_name
23
+ when /mysql/i then :mysql
24
+ when /sqlite/i then :sqlite
25
+ when /pg/i, /postgres/i then :postgres
26
+ else raise "Unknown database type for Gem name: #{database_gem_name.inspect}"
27
+ end
28
+ end
29
+
30
+ def setup_activerecord!
31
+ require 'active_record'
32
+ require config[:require]
33
+ ::ActiveRecord::Base.establish_connection(config[:config])
34
+
35
+ require 'logger'
36
+ require 'stringio'
37
+ @logs = StringIO.new
38
+ ::ActiveRecord::Base.logger = Logger.new(@logs)
39
+
40
+ if config[:config][:adapter] == 'sqlite3'
41
+ sqlite_version = ::ActiveRecord::Base.connection.send(:sqlite_version).instance_variable_get("@version").inspect rescue "unknown"
42
+ end
43
+ end
44
+
45
+ def table_name(name)
46
+ "objectidcols_spec_#{name}"
47
+ end
48
+
49
+ def database_gem_name
50
+ config[:database_gem_name]
51
+ end
52
+
53
+ def adapter_name
54
+ config[:config][:adapter]
55
+ end
56
+
57
+ private
58
+ def config
59
+ config_from_config_file || travis_ci_config_from_environment || invalid_config_file!
60
+ end
61
+
62
+ def config_from_config_file
63
+ return nil unless File.exist?(config_file_path)
64
+ require config_file_path
65
+
66
+ return nil unless defined?(OBJECTID_COLUMNS_SPEC_DATABASE_CONFIG)
67
+ return nil unless OBJECTID_COLUMNS_SPEC_DATABASE_CONFIG.kind_of?(Hash)
68
+
69
+ return nil unless OBJECTID_COLUMNS_SPEC_DATABASE_CONFIG[:require]
70
+ return nil unless OBJECTID_COLUMNS_SPEC_DATABASE_CONFIG[:database_gem_name]
71
+
72
+ return nil unless OBJECTID_COLUMNS_SPEC_DATABASE_CONFIG
73
+ OBJECTID_COLUMNS_SPEC_DATABASE_CONFIG
74
+ end
75
+
76
+ def travis_ci_config_from_environment
77
+ dbtype = (ENV['OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE'] || '').strip.downcase
78
+ is_jruby = defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
79
+
80
+ if is_jruby
81
+ case dbtype
82
+ when 'mysql'
83
+ {
84
+ :require => 'activerecord-jdbcmysql-adapter',
85
+ :database_gem_name => 'activerecord-jdbcmysql-adapter',
86
+ :config => {
87
+ :adapter => 'jdbcmysql',
88
+ :database => 'myapp_test',
89
+ :username => 'travis',
90
+ :encoding => 'utf8'
91
+ }
92
+ }
93
+ when 'postgres', 'postgresql'
94
+ {
95
+ :require => 'activerecord-jdbcpostgresql-adapter',
96
+ :database_gem_name => 'activerecord-jdbcpostgresql-adapter',
97
+ :config => {
98
+ :adapter => 'jdbcpostgresql',
99
+ :database => 'myapp_test',
100
+ :username => 'postgres'
101
+ }
102
+ }
103
+ when 'sqlite'
104
+ {
105
+ :require => 'activerecord-jdbcsqlite3-adapter',
106
+ :database_gem_name => 'activerecord-jdbcsqlite3-adapter',
107
+ :config => {
108
+ :adapter => 'jdbcsqlite3',
109
+ :database => ':memory:'
110
+ }
111
+ }
112
+ when '', nil then nil
113
+ else
114
+ raise "Unknown Travis CI database type: #{dbtype.inspect}"
115
+ end
116
+ else
117
+ case dbtype
118
+ when 'postgres', 'postgresql'
119
+ {
120
+ :require => 'pg',
121
+ :database_gem_name => 'pg',
122
+ :config => {
123
+ :adapter => 'postgresql',
124
+ :database => 'myapp_test',
125
+ :username => 'postgres',
126
+ :min_messages => 'WARNING'
127
+ }
128
+ }
129
+ when 'mysql'
130
+ {
131
+ :require => 'mysql2',
132
+ :database_gem_name => 'mysql2',
133
+ :config => {
134
+ :adapter => 'mysql2',
135
+ :database => 'myapp_test',
136
+ :username => 'travis',
137
+ :encoding => 'utf8'
138
+ }
139
+ }
140
+ when 'sqlite'
141
+ {
142
+ :require => 'sqlite3',
143
+ :database_gem_name => 'sqlite3',
144
+ :config => {
145
+ :adapter => 'sqlite3',
146
+ :database => ':memory:',
147
+ :timeout => 500
148
+ }
149
+ }
150
+ when '', nil then nil
151
+ else
152
+ raise "Unknown Travis CI database type: #{dbtype.inspect}"
153
+ end
154
+ end
155
+ end
156
+
157
+ def config_file_path
158
+ @config_file_path ||= File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_database_config.rb'))
159
+ end
160
+
161
+ def invalid_config_file!
162
+ raise Errno::ENOENT, %{In order to run specs for ObjectIdColumns, you need to create a file at:
163
+
164
+ #{config_file_path}
165
+
166
+ ...that defines a top-level OBJECTID_COLUMNS_SPEC_DATABASE_CONFIG hash, with members:
167
+
168
+ :require => 'name_of_adapter_to_require',
169
+ :database_gem_name => 'name_of_gem_for_adapter',
170
+ :config => { ...whatever ActiveRecord::Base.establish_connection should be passed... }
171
+
172
+ Alternatively, if you're running under Travis CI, you can set the environment variable
173
+ OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE to 'postgres', 'mysql', or 'sqlite', and it will
174
+ use the correct configuration for testing on Travis CI.}
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,92 @@
1
+ require 'active_record'
2
+ require 'active_record/migration'
3
+ require 'objectid_columns/helpers/database_helper'
4
+
5
+ module ObjectidColumns
6
+ module Helpers
7
+ module SystemHelpers
8
+ def migrate(&block)
9
+ migration_class = Class.new(::ActiveRecord::Migration)
10
+ metaclass = migration_class.class_eval { class << self; self; end }
11
+ metaclass.instance_eval { define_method(:up, &block) }
12
+
13
+ ::ActiveRecord::Migration.suppress_messages do
14
+ migration_class.migrate(:up)
15
+ end
16
+ end
17
+
18
+ def define_model_class(name, table_name, options = { }, &block)
19
+ superclass = options[:superclass] || ::ActiveRecord::Base
20
+ model_class = Class.new(superclass)
21
+ ::Object.send(:remove_const, name) if ::Object.const_defined?(name)
22
+ ::Object.const_set(name, model_class)
23
+ model_class.table_name = table_name if table_name
24
+ model_class.class_eval(&block) if block
25
+ model_class
26
+ end
27
+
28
+ def ensure_database_is_set_up!
29
+ ::ObjectidColumns::Helpers::SystemHelpers.database_helper
30
+ end
31
+
32
+ class << self
33
+ def database_helper
34
+ @database_helper ||= begin
35
+ out = ObjectidColumns::Helpers::DatabaseHelper.new
36
+ out.setup_activerecord!
37
+ out
38
+ end
39
+ end
40
+
41
+ def binary_column(length)
42
+ case ObjectidColumns::Helpers::SystemHelpers.database_helper.adapter_name.to_s
43
+ when /mysql/, /sqlite/ then "BINARY(#{length})"
44
+ when /postgres/ then "BYTEA"
45
+ else raise "Don't yet know how to define a binary column for database #{OBJECTID_COLUMNS_SPEC_DATABASE_CONFIG[:config][:adapter].inspect}"
46
+ end
47
+ end
48
+
49
+ def supports_length_limits_on_binary_columns?
50
+ case ObjectidColumns::Helpers::SystemHelpers.database_helper.adapter_name.to_s
51
+ when /mysql/, /sqlite/ then true
52
+ when /postgres/ then false
53
+ else raise "Don't yet know whether database #{OBJECTID_COLUMNS_SPEC_DATABASE_CONFIG[:config][:adapter].inspect} supports limits on binary columns"
54
+ end
55
+ end
56
+ end
57
+
58
+ def create_standard_system_spec_tables!
59
+ migrate do
60
+ drop_table :objectidcols_spec_table rescue nil
61
+ create_table :objectidcols_spec_table do |t|
62
+ t.column :perfect_b_oid, ObjectidColumns::Helpers::SystemHelpers.binary_column(12)
63
+ t.column :longer_b_oid, ObjectidColumns::Helpers::SystemHelpers.binary_column(15)
64
+
65
+ t.column :too_short_b, ObjectidColumns::Helpers::SystemHelpers.binary_column(11)
66
+ t.column :perfect_b, ObjectidColumns::Helpers::SystemHelpers.binary_column(12)
67
+ t.column :longer_b, ObjectidColumns::Helpers::SystemHelpers.binary_column(15)
68
+
69
+ t.column :perfect_s_oid, 'VARCHAR(24)'
70
+ t.column :longer_s_oid, 'VARCHAR(30)'
71
+
72
+ t.column :too_short_s, 'VARCHAR(23)'
73
+ t.column :perfect_s, 'VARCHAR(24)'
74
+ t.column :longer_s, 'VARCHAR(30)'
75
+
76
+ t.integer :some_int_column
77
+ end
78
+ end
79
+ end
80
+
81
+ def create_standard_system_spec_models!
82
+ define_model_class(:Spectable, 'objectidcols_spec_table') { }
83
+ end
84
+
85
+ def drop_standard_system_spec_tables!
86
+ migrate do
87
+ drop_table :objectidcols_spec_table rescue nil
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,600 @@
1
+ require 'objectid_columns'
2
+ require 'objectid_columns/helpers/system_helpers'
3
+
4
+ # See the gemspec for more details -- basically, we don't always load composite_primary_keys, because it's pretty
5
+ # broken and doesn't work with a fair number of combinations of Ruby versions, databases, and so on. So if it's not
6
+ # available, we skip those tests.
7
+ begin
8
+ require 'composite_primary_keys'
9
+ $composite_primary_keys_available = true
10
+ rescue LoadError => le
11
+ # nothing here
12
+ end
13
+
14
+ unless defined?(VALID_OBJECTID_CLASSES)
15
+ VALID_OBJECTID_CLASSES = [ BSON::ObjectId ]
16
+ VALID_OBJECTID_CLASSES << Moped::BSON::ObjectId if defined?(Moped::BSON::ObjectId)
17
+ end
18
+
19
+ RSpec::Matchers.define :be_an_objectid_object do
20
+ match do |actual|
21
+ VALID_OBJECTID_CLASSES.detect { |c| actual.kind_of?(c) }
22
+ end
23
+ failure_message_for_should do |actual|
24
+ "expected that #{actual} (#{actual.class}) would be an instance of BSON::ObjectId or Moped::BSON::ObjectId"
25
+ end
26
+ end
27
+
28
+ RSpec::Matchers.define :be_the_same_objectid_as do |expected|
29
+ match do |actual|
30
+ net_expected = expected ? expected.to_bson_id.to_s : expected
31
+ net_actual = actual ? actual.to_bson_id.to_s : actual
32
+ net_expected == net_actual
33
+ end
34
+ failure_message_for_should do |actual|
35
+ "expected that #{actual} (#{actual.class}) would be the same ObjectId as #{expected} (#{expected.class})"
36
+ end
37
+ end
38
+
39
+ RSpec::Matchers.define :be_an_objectid_object_matching do |expected|
40
+ match do |actual|
41
+ net_expected = expected ? expected.to_bson_id.to_s : expected
42
+ net_actual = actual ? actual.to_bson_id.to_s : actual
43
+ (net_expected == net_actual) && (VALID_OBJECTID_CLASSES.detect { |c| actual.kind_of?(c) })
44
+ end
45
+ failure_message_for_should do |actual|
46
+ "expected that #{actual} (#{actual.class}) would be an ObjectId object equal to #{expected} (#{expected.class})"
47
+ end
48
+ end
49
+
50
+ describe "ObjectidColumns basic operations" do
51
+ include ObjectidColumns::Helpers::SystemHelpers
52
+
53
+ before :each do
54
+ ensure_database_is_set_up!
55
+
56
+ create_standard_system_spec_tables!
57
+ create_standard_system_spec_models!
58
+ end
59
+
60
+ after :each do
61
+ drop_standard_system_spec_tables!
62
+ end
63
+
64
+ VALID_OBJECTID_CLASSES.each do |test_class|
65
+ context "using test class #{test_class}" do
66
+ before :each do
67
+ @tc = test_class
68
+ end
69
+
70
+ def new_oid
71
+ @tc.new
72
+ end
73
+
74
+ context "with an STI table and model" do
75
+ before :each do
76
+ migrate do
77
+ drop_table :objectidcols_spec_table_sti rescue nil
78
+ create_table :objectidcols_spec_table_sti do |t|
79
+ t.string :type
80
+ t.column :some_oid, 'VARCHAR(24)'
81
+ end
82
+ end
83
+ end
84
+
85
+ after :each do
86
+ migrate do
87
+ drop_table :objectidcols_spec_table_sti rescue nil
88
+ end
89
+ end
90
+
91
+ let(:parent_model_class) { define_model_class(:SpectableStiParent, 'objectidcols_spec_table_sti') { has_objectid_columns } }
92
+ let(:child_model_class) { define_model_class(:SpectableStiChild, 'objectidcols_spec_table_sti', :superclass => parent_model_class) { } }
93
+
94
+ it "should work from both the parent and child class" do
95
+ id_1 = new_oid
96
+ id_2 = new_oid
97
+
98
+ parent_instance = parent_model_class.new(:some_oid => id_1)
99
+ parent_instance.save!
100
+
101
+ child_instance = child_model_class.new(:some_oid => id_2)
102
+ child_instance.save!
103
+
104
+ all_models = parent_model_class.all.to_a
105
+ expect(all_models.length).to eq(2)
106
+ parent_instance_again = all_models.detect { |m| m.id == parent_instance.id }
107
+ child_instance_again = all_models.detect { |m| m.id == child_instance.id }
108
+
109
+ expect(parent_instance_again.some_oid).to be_an_objectid_object_matching(id_1)
110
+ expect(child_instance_again.some_oid).to be_an_objectid_object_matching(id_2)
111
+
112
+ child_models = child_model_class.all.to_a
113
+ expect(child_models.length).to eq(1)
114
+ expect(child_models[0].some_oid).to be_an_objectid_object_matching(id_2)
115
+ end
116
+ end
117
+
118
+ it "should not allow defining a column that's too short" do
119
+ if ObjectidColumns::Helpers::SystemHelpers.supports_length_limits_on_binary_columns?
120
+ expect { ::Spectable.class_eval { has_objectid_column :too_short_b } }.to raise_error(ArgumentError)
121
+ expect { ::Spectable.class_eval { has_objectid_column :too_short_s } }.to raise_error(ArgumentError)
122
+ end
123
+ end
124
+
125
+ it "should not allow defining a column that's the wrong type" do
126
+ expect { ::Spectable.class_eval { has_objectid_column :some_int_column } }.to raise_error(ArgumentError)
127
+ end
128
+
129
+ it "should not allow defining a column that doesn't exist" do
130
+ expect { ::Spectable.class_eval { has_objectid_column :unknown_column } }.to raise_error(ArgumentError)
131
+ end
132
+
133
+ it "should not fail if the table doesn't exist" do
134
+ define_model_class(:SpectableNonexistent, 'objectidcols_spec_table_nonexistent') { }
135
+ expect { ::SpectableNonexistent.class_eval { has_objectid_column :foo } }.to_not raise_error
136
+ end
137
+
138
+ it "should not fail if declared as a primary key and the table doesn't exist" do
139
+ define_model_class(:SpectableNonexistent, 'objectidcols_spec_table_nonexistent') { }
140
+ expect { ::SpectableNonexistent.class_eval { has_objectid_primary_key } }.to_not raise_error
141
+ expect { ::SpectableNonexistent.class_eval { has_objectid_primary_key :foo } }.to_not raise_error
142
+ end
143
+
144
+ if $composite_primary_keys_available
145
+ describe "composite primary key support" do
146
+ context "with an implicit PK" do
147
+ before :each do
148
+ migrate do
149
+ drop_table :objectidcols_spec_pk_cmp rescue nil
150
+ create_table :objectidcols_spec_pk_cmp, :id => false do |t|
151
+ t.binary :some_oid, :null => false
152
+ t.string :more_pk, :null => false
153
+ t.string :value
154
+ end
155
+ end
156
+
157
+ define_model_class(:SpectablePkCmp, :objectidcols_spec_pk_cmp) do
158
+ if respond_to?(:primary_keys=)
159
+ self.primary_keys = [ 'some_oid', 'more_pk' ]
160
+ else
161
+ self.set_primary_keys('some_oid', 'more_pk')
162
+ end
163
+ end
164
+ ::SpectablePkCmp.class_eval { has_objectid_primary_key }
165
+ @model_class = ::SpectablePkCmp
166
+ end
167
+
168
+ it "should allow using a composite primary key in individual parts" do
169
+ pending "disabled" unless $composite_primary_keys_available
170
+
171
+ instance = @model_class.new
172
+ instance.some_oid = new_oid
173
+ instance.more_pk = "foo"
174
+ instance.value = "foo value"
175
+ instance.save!
176
+
177
+ instance_again = @model_class.find([ instance.some_oid, instance.more_pk ])
178
+ expect(instance_again.value).to eq(instance.value)
179
+ expect(instance_again.some_oid).to eq(instance.some_oid)
180
+ expect(instance_again.more_pk).to eq(instance.more_pk)
181
+ end
182
+
183
+ it "should allow using a composite primary key as a whole" do
184
+ pending "disabled" unless $composite_primary_keys_available
185
+
186
+ oid = new_oid
187
+ instance = @model_class.new
188
+ instance.id = [ oid, "foo" ]
189
+ instance.value = "foo value"
190
+ instance.save!
191
+
192
+ expect(instance.some_oid).to be_an_objectid_object_matching(oid)
193
+ expect(instance.more_pk).to eq("foo")
194
+ expect(instance.value).to eq("foo value")
195
+
196
+ instance_again = @model_class.find(instance.id)
197
+ expect(instance_again.id).to eq(instance.id)
198
+ expect(instance_again.some_oid).to be_an_objectid_object_matching(oid)
199
+ expect(instance_again.more_pk).to eq("foo")
200
+ expect(instance_again.value).to eq("foo value")
201
+ expect(instance_again.id).to be_kind_of(Array)
202
+ expect(instance_again.id.length).to eq(2)
203
+ expect(instance_again.id[0]).to be_an_objectid_object_matching(oid)
204
+ expect(instance_again.id[1]).to eq("foo")
205
+ end
206
+ end
207
+
208
+ context "with an explicit PK" do
209
+ before :each do
210
+ migrate do
211
+ drop_table :objectidcols_spec_pk_cmp_2 rescue nil
212
+ create_table :objectidcols_spec_pk_cmp_2, :id => false do |t|
213
+ t.binary :one, :null => false
214
+ t.string :two, :null => false
215
+ t.string :three, :null => false
216
+ t.string :value
217
+ end
218
+ end
219
+
220
+ define_model_class(:SpectablePkCmp2, :objectidcols_spec_pk_cmp_2) do
221
+ if respond_to?(:primary_keys=)
222
+ self.primary_keys = [ 'one', 'two', 'three' ]
223
+ else
224
+ self.set_primary_keys('one', 'two', 'three')
225
+ end
226
+ end
227
+ ::SpectablePkCmp2.class_eval { has_objectid_primary_key :one, :three }
228
+ @model_class = ::SpectablePkCmp2
229
+ end
230
+
231
+ it "should allow using a composite primary key that's partially ObjectId and partially not" do
232
+ instance = @model_class.new
233
+ instance.two = "foo"
234
+ instance.value = "foo_value"
235
+ instance.save!
236
+
237
+ expect(instance.id).to be_kind_of(Array)
238
+ expect(instance.id[0]).to be_an_objectid_object
239
+ expect(instance.id[1]).to eq("foo")
240
+ expect(instance.id[2]).to be_an_objectid_object
241
+
242
+ id = instance.id
243
+ instance_again = @model_class.find(id)
244
+ expect(instance_again.id).to eq(id)
245
+ expect(instance_again.id[0]).to be_an_objectid_object_matching(id[0])
246
+ expect(instance_again.id[1]).to eq("foo")
247
+ expect(instance_again.id[2]).to be_an_objectid_object_matching(id[2])
248
+ end
249
+ end
250
+ end
251
+ end
252
+
253
+ describe "primary key column support" do
254
+ before :each do
255
+ migrate do
256
+ drop_table :objectidcols_spec_pk_bin rescue nil
257
+ create_table :objectidcols_spec_pk_bin, :id => false do |t|
258
+ t.binary :id, :null => false
259
+ t.string :name
260
+ end
261
+
262
+ drop_table :objectidcols_spec_pk_str rescue nil
263
+ create_table :objectidcols_spec_pk_str, :id => false do |t|
264
+ t.string :id, :null => false
265
+ t.string :name
266
+ end
267
+
268
+ drop_table :objectidcols_spec_pk_alt rescue nil
269
+ create_table :objectidcols_spec_pk_alt, :id => false do |t|
270
+ t.binary :some_name, :null => false
271
+ t.string :name
272
+ end
273
+
274
+ drop_table :objectidcols_spec_pk_implicit rescue nil
275
+ create_table :objectidcols_spec_pk_implicit, :id => false do |t|
276
+ t.binary :some_name, :null => false
277
+ t.string :name
278
+ end
279
+ end
280
+
281
+ define_model_class(:SpectablePkBin, :objectidcols_spec_pk_bin) { self.primary_key = 'id' }
282
+ define_model_class(:SpectablePkStr, :objectidcols_spec_pk_str) { self.primary_key = 'id' }
283
+ define_model_class(:SpectablePkAlt, :objectidcols_spec_pk_alt) { self.primary_key = 'some_name' }
284
+ define_model_class(:SpectablePkImplicit, :objectidcols_spec_pk_implicit) { }
285
+
286
+ ::SpectablePkBin.class_eval { has_objectid_primary_key }
287
+ ::SpectablePkStr.class_eval { has_objectid_primary_key }
288
+ ::SpectablePkAlt.class_eval { has_objectid_primary_key }
289
+ ::SpectablePkImplicit.class_eval { has_objectid_primary_key :some_name }
290
+ end
291
+
292
+ after :each do
293
+ drop_table :objectidcols_spec_pk_bin rescue nil
294
+ drop_table :objectidcols_spec_pk_str rescue nil
295
+ drop_table :objectidcols_spec_pk_table_alt rescue nil
296
+ drop_table :objectidcols_spec_pk_implicit rescue nil
297
+ end
298
+
299
+ [ :SpectablePkBin, :SpectablePkStr, :SpectablePkAlt, :SpectablePkImplicit ].each do |model_class|
300
+ context "on model #{model_class}" do
301
+ before :each do
302
+ @model_class = model_class.to_s.constantize
303
+ end
304
+
305
+ it "should fail autodetection, since there are no columns ending in _oid" do
306
+ expect { @model_class.has_objectid_columns }.to raise_error(ArgumentError)
307
+ end
308
+
309
+ it "should allow using a binary ObjectId column as a primary key" do
310
+ r1 = @model_class.new
311
+ r1.name = 'row 1'
312
+ expect(r1.id).to be_nil
313
+ r1.save!
314
+ expect(r1.id).to_not be_nil
315
+ expect(r1.id).to be_an_objectid_object
316
+ r1_id = r1.id
317
+
318
+ r2 = @model_class.new
319
+ r2.name = 'row 2'
320
+ expect(r2.id).to be_nil
321
+ r2.save!
322
+ expect(r2.id).to_not be_nil
323
+ expect(r2.id).to be_an_objectid_object
324
+ r2_id = r2.id
325
+
326
+ expect(r1.send(@model_class.primary_key)).to be_an_objectid_object_matching(r1.id)
327
+ expect(r2.send(@model_class.primary_key)).to be_an_objectid_object_matching(r2.id)
328
+
329
+ r1_again = @model_class.find(r1.id)
330
+ expect(r1_again.name).to eq('row 1')
331
+
332
+ r2_again = @model_class.find(r2.id)
333
+ expect(r2_again.name).to eq('row 2')
334
+
335
+ expect(@model_class.find([ r1.id, r2. id ]).map(&:id).sort_by(&:to_s)).to eq([ r1_id, r2_id ].sort_by(&:to_s))
336
+
337
+ expect(@model_class.where(:name => 'row 1').first.id).to eq(r1_id)
338
+ expect(@model_class.where(:name => 'row 2').first.id).to eq(r2_id)
339
+
340
+ find_by_id_method = "find_by_#{@model_class.primary_key}"
341
+ expect(@model_class.send(find_by_id_method, r1.id).id).to eq(r1_id)
342
+ expect(@model_class.send(find_by_id_method, r2.id).id).to eq(r2_id)
343
+ expect(@model_class.send(find_by_id_method, new_oid)).to be_nil
344
+ end
345
+
346
+ it "should let you load and save objects properly" do
347
+ r1 = @model_class.new
348
+ r1.name = 'row 1'
349
+ r1.id = new_oid
350
+ r1.save!
351
+
352
+ r1_again = @model_class.find(@tc.from_string(r1.id.to_s))
353
+ expect(r1_again.name).to eq('row 1')
354
+ r1_again.id = @tc.from_string(r1.id.to_s)
355
+ r1_again.name = 'row 1 again'
356
+ r1_again.save!
357
+
358
+ r1_yet_again = @model_class.find(r1_again.id)
359
+ expect(r1_yet_again.name).to eq('row 1 again')
360
+ end
361
+
362
+ it "should not pick up primary-key columns automatically, even if they're named _oid" do
363
+ migrate do
364
+ drop_table :objectidcols_spec_pk_auto rescue nil
365
+ create_table :objectidcols_spec_pk_auto, :id => false do |t|
366
+ t.binary :foo_oid, :null => false
367
+ t.binary :bar_oid
368
+ t.string :name
369
+ end
370
+ end
371
+
372
+ define_model_class(:SpectablePkAuto, :objectidcols_spec_pk_auto) { self.primary_key = 'foo_oid' }
373
+
374
+ ::SpectablePkAuto.has_objectid_columns
375
+ r = ::SpectablePkAuto.new
376
+ r.foo_oid = 'foobar' # this will only work if we do NOT think it's an ObjectId
377
+ expect { r.bar_oid = 'foobar' }.to raise_error(ArgumentError)
378
+ r.bar_oid = the_bar_oid = new_oid.to_s
379
+
380
+ expect(r.bar_oid).to be_an_objectid_object_matching(the_bar_oid)
381
+
382
+ migrate do
383
+ drop_table :objectidcols_spec_pk_auto rescue nil
384
+ end
385
+ end
386
+ end
387
+ end
388
+ end
389
+
390
+ context "with a single, manually-defined column" do
391
+ before :each do
392
+ ::Spectable.class_eval { has_objectid_column :perfect_s_oid }
393
+ end
394
+
395
+ it "should allow writing and reading via an ObjectId object" do
396
+ the_oid = new_oid
397
+
398
+ r = ::Spectable.new
399
+ r.perfect_s_oid = the_oid
400
+ expect(r.perfect_s_oid).to be_the_same_objectid_as(the_oid)
401
+ expect(r.perfect_s_oid).to be_an_objectid_object
402
+ r.save!
403
+ expect(r.perfect_s_oid).to be_the_same_objectid_as(the_oid.to_s)
404
+ expect(r.perfect_s_oid).to be_an_objectid_object
405
+
406
+ r_again = ::Spectable.find(r.id)
407
+ expect(r_again.perfect_s_oid).to be_the_same_objectid_as(the_oid.to_s)
408
+ expect(r_again.perfect_s_oid).to be_an_objectid_object
409
+ end
410
+
411
+ it "should raise a good exception if you try to assign something that isn't a valid ObjectId" do
412
+ r = ::Spectable.new
413
+
414
+ expect { r.perfect_s_oid = 12345 }.to raise_error(ArgumentError, /12345/)
415
+ expect { r.perfect_s_oid = /foobar/ }.to raise_error(ArgumentError, /foobar/i)
416
+ end
417
+
418
+ if "".respond_to?(:encoding)
419
+ it "should not allow assigning binary strings unless their encoding is BINARY" do
420
+ r = ::Spectable.new
421
+
422
+ binary = new_oid.to_binary
423
+ binary = binary.force_encoding(Encoding::ISO_8859_1)
424
+ expect { r.perfect_s_oid = binary }.to raise_error(ArgumentError)
425
+ end
426
+ end
427
+
428
+ it "should not allow assigning strings that are the wrong format" do
429
+ r = ::Spectable.new
430
+
431
+ expect { r.perfect_s_oid = new_oid.to_binary[0..10] }.to raise_error(ArgumentError)
432
+ expect { r.perfect_s_oid = new_oid.to_binary + "\x00" }.to raise_error(ArgumentError)
433
+ end
434
+
435
+ it "should let you set columns to nil" do
436
+ r = ::Spectable.create!(:perfect_s_oid => (@oid = new_oid))
437
+
438
+ r_again = ::Spectable.find(r.id)
439
+ expect(r_again.perfect_s_oid).to be_an_objectid_object_matching(@oid)
440
+ r.perfect_s_oid = nil
441
+ r.save!
442
+
443
+ r_yet_again = ::Spectable.find(r.id)
444
+ expect(r_yet_again.perfect_s_oid).to be_nil
445
+ end
446
+
447
+ it "should accept ObjectIds for input in binary, String, or either object format" do
448
+ VALID_OBJECTID_CLASSES.each do |klass|
449
+ r = ::Spectable.create!(:perfect_s_oid => (@oid = klass.new))
450
+ expect(::Spectable.find(r.id).perfect_s_oid).to be_an_objectid_object_matching(@oid)
451
+ end
452
+
453
+ r = ::Spectable.create!(:perfect_s_oid => (@oid = new_oid.to_s))
454
+ expect(::Spectable.find(r.id).perfect_s_oid).to be_an_objectid_object_matching(@oid)
455
+
456
+ r = ::Spectable.create!(:perfect_s_oid => (@oid = new_oid.to_binary))
457
+ expect(::Spectable.find(r.id).perfect_s_oid).to be_an_objectid_object_matching(@oid)
458
+ end
459
+
460
+ it "should not do anything to the other columns" do
461
+ r = ::Spectable.new
462
+
463
+ r.perfect_b_oid = 'perfect_b_1'
464
+ r.longer_b_oid = 'longer_b_1'
465
+
466
+ r.too_short_b = 'short_b_2'
467
+ r.perfect_b = 'perfect_b_2'
468
+ r.longer_b = 'longer_b_2'
469
+
470
+ the_oid = new_oid
471
+ r.perfect_s_oid = the_oid
472
+ r.longer_s_oid = 'longer_s_1'
473
+
474
+ r.too_short_s = 'short_s_1'
475
+ r.perfect_s = 'perfect_s_2'
476
+ r.longer_s = 'longer_s'
477
+
478
+ r.save!
479
+
480
+ r_again = ::Spectable.find(r.id)
481
+
482
+ expect(r_again.perfect_b_oid.strip).to eq('perfect_b_1')
483
+ expect(r_again.longer_b_oid.strip).to eq('longer_b_1')
484
+
485
+ expect(r_again.too_short_b.strip).to eq('short_b_2')
486
+ expect(r_again.perfect_b.strip).to eq('perfect_b_2')
487
+ expect(r_again.longer_b.strip).to eq('longer_b_2')
488
+
489
+ expect(r_again.perfect_s_oid).to be_the_same_objectid_as(the_oid)
490
+ expect(r_again.perfect_s_oid).to be_an_objectid_object
491
+ expect(r_again.longer_s_oid).to eq('longer_s_1')
492
+
493
+ expect(r_again.too_short_s).to eq('short_s_1')
494
+ expect(r_again.perfect_s).to eq('perfect_s_2')
495
+ expect(r_again.longer_s).to eq('longer_s')
496
+ end
497
+
498
+ it "should allow querying on ObjectId columns via Hash, but not change other queries" do
499
+ r1 = ::Spectable.create!(:perfect_s_oid => (@oid1 = new_oid), :longer_s_oid => "foobar")
500
+ r2 = ::Spectable.create!(:perfect_s_oid => (@oid2 = new_oid), :longer_s_oid => "barfoo")
501
+
502
+ expect(::Spectable.where(:perfect_s_oid => @oid1).to_a.map(&:id)).to eq([ r1.id ])
503
+ expect(::Spectable.where(:perfect_s_oid => @oid2).to_a.map(&:id)).to eq([ r2.id ])
504
+ expect(::Spectable.where(:perfect_s_oid => [ @oid1, @oid2 ]).to_a.map(&:id).sort).to eq([ r1.id, r2.id ].sort)
505
+
506
+ expect(::Spectable.where(:perfect_s_oid => @oid1.to_s).to_a.map(&:id)).to eq([ r1.id ])
507
+ expect(::Spectable.where(:perfect_s_oid => @oid2.to_s).to_a.map(&:id)).to eq([ r2.id ])
508
+ expect(::Spectable.where(:perfect_s_oid => [ @oid1, @oid2 ].map(&:to_s)).to_a.map(&:id).sort).to eq([ r1.id, r2.id ].sort)
509
+
510
+ expect(::Spectable.where(:perfect_s_oid => @oid1.to_binary).to_a.map(&:id)).to eq([ r1.id ])
511
+ expect(::Spectable.where(:perfect_s_oid => @oid2.to_binary).to_a.map(&:id)).to eq([ r2.id ])
512
+ expect(::Spectable.where(:perfect_s_oid => [ @oid1, @oid2 ].map(&:to_binary)).to_a.map(&:id).sort).to eq([ r1.id, r2.id ].sort)
513
+ end
514
+
515
+ it "should give a good exception if you try to pass an ObjectID for a column that isn't an official ObjectID column" do
516
+ oid = new_oid
517
+ expect { ::Spectable.where(:perfect_b_oid => oid).to_a }.to raise_error(/ObjectidColumns:.*perfect_b_oid/mi)
518
+ end
519
+
520
+ it "should not blow up (and instead let ActiveRecord blow up) if you try to pass an ObjectID for a column that isn't a column at all" do
521
+ oid = new_oid
522
+ expect { ::Spectable.where(:something => oid).to_a }.to raise_error(/something/mi)
523
+ end
524
+
525
+ it "should not blow up (and instead let ActiveRecord just find no records, or blow up) if you try to pass an ObjectID for a column on a table that isn't using this gem at all" do
526
+ define_model_class(:SpectableNondeclared, 'objectidcols_spec_table') { }
527
+ expect(::SpectableNondeclared.where(:perfect_s_oid => new_oid).to_a).to eq([ ])
528
+
529
+ expect { ::SpectableNondeclared.where(:something => new_oid).to_a }.to raise_error(/something/i)
530
+ end
531
+ end
532
+
533
+ it "should allow using any column that's long enough, including binary or string columns" do
534
+ ::Spectable.class_eval do
535
+ has_objectid_columns :perfect_b_oid, :longer_b_oid
536
+ has_objectid_columns :perfect_s_oid, :longer_s_oid, :perfect_s, :longer_s
537
+ end
538
+
539
+ r = ::Spectable.new
540
+
541
+ r.perfect_b_oid = @perfect_b_oid = new_oid
542
+ r.longer_b_oid = @longer_b_oid = new_oid
543
+ r.perfect_s_oid = @perfect_s_oid = new_oid
544
+ r.longer_s_oid = @longer_s_oid = new_oid
545
+ r.perfect_s = @perfect_s = new_oid
546
+ r.longer_s = @longer_s = new_oid
547
+
548
+ r.save!
549
+
550
+ r_again = ::Spectable.find(r.id)
551
+ expect(r_again.perfect_b_oid).to be_an_objectid_object_matching(@perfect_b_oid)
552
+ expect(r_again.longer_b_oid).to be_an_objectid_object_matching(@longer_b_oid)
553
+ expect(r_again.perfect_s_oid).to be_an_objectid_object_matching(@perfect_s_oid)
554
+ expect(r_again.longer_s_oid).to be_an_objectid_object_matching(@longer_s_oid)
555
+ expect(r_again.perfect_s).to be_an_objectid_object_matching(@perfect_s)
556
+ expect(r_again.longer_s).to be_an_objectid_object_matching(@longer_s)
557
+ end
558
+
559
+ it "should automatically pick up any _oid columns" do
560
+ ::Spectable.class_eval do
561
+ has_objectid_columns
562
+ end
563
+
564
+ r = ::Spectable.new
565
+
566
+ r.perfect_b_oid = @perfect_b_oid = new_oid
567
+ r.longer_b_oid = @longer_b_oid = new_oid
568
+
569
+ r.too_short_b = 'short_b_2'
570
+ r.perfect_b = 'perfect_b_2'
571
+ r.longer_b = 'longer_b_2'
572
+
573
+ r.perfect_s_oid = @perfect_s_oid = new_oid
574
+ r.longer_s_oid = @longer_s_oid = new_oid
575
+
576
+ r.too_short_s = 'short_s_1'
577
+ r.perfect_s = 'perfect_s_2'
578
+ r.longer_s = 'longer_s'
579
+
580
+ r.save!
581
+
582
+ r_again = ::Spectable.find(r.id)
583
+
584
+ expect(r_again.perfect_b_oid).to be_an_objectid_object_matching(@perfect_b_oid)
585
+ expect(r_again.longer_b_oid).to be_an_objectid_object_matching(@longer_b_oid)
586
+
587
+ expect(r_again.too_short_b.strip).to eq('short_b_2')
588
+ expect(r_again.perfect_b.strip).to eq('perfect_b_2')
589
+ expect(r_again.longer_b.strip).to eq('longer_b_2')
590
+
591
+ r_again.perfect_s_oid.should be_an_objectid_object_matching(@perfect_s_oid)
592
+ r_again.longer_s_oid.should be_an_objectid_object_matching(@longer_s_oid)
593
+
594
+ expect(r_again.too_short_s).to eq('short_s_1')
595
+ expect(r_again.perfect_s).to eq('perfect_s_2')
596
+ expect(r_again.longer_s).to eq('longer_s')
597
+ end
598
+ end
599
+ end
600
+ end