objectid_columns 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA512:
3
- metadata.gz: 8752dd26c637b7ba079438edd6abedce5c7df3fe44965c176f3aa3b3c137b1fbd504d5193e69e6d6ac200d445fe1e2808b04d494395c304b92c0b6878838f43e
4
- data.tar.gz: aecea712ee39f11a59e245bce3d89136415b3429317500f4b8087c8e3b90b53ca6df937b904ca7f0abfc328f6961016105422ca94a6b4bfd99ad98644758e472
3
+ metadata.gz: 216e3a0b18aa9ff589f728a7ba6d95fa56ada067b51a99cff26dac0b19cb3f49cb4fa9deb2b2ae227498123bb5d8aa286aa76760eed1fbf96cfc6599dd7e423f
4
+ data.tar.gz: c9a48a9b4e065da3a3b45555cf7c2acd0c3a5687c6e5cf752318934553110fed9906d7d72b4e87cef431a4df7386e5fab1001c0ffbcf4cef655b8e59f8d1a169
5
5
  SHA1:
6
- metadata.gz: a55261b9a4c1cdf209e6b14c9d04a5a8aa0ff38d
7
- data.tar.gz: fc27e32b0629998e0aa28180ba01cbb71ae1fc29
6
+ metadata.gz: 522783d13b6a7e03735ae0c8d793a545ef4926aa
7
+ data.tar.gz: c69aed130528349b56f2c60effed8ca0e73213eb
data/CHANGES.md ADDED
@@ -0,0 +1,8 @@
1
+ # Change History for ObjectidColumns
2
+
3
+ ### Version 1.0.1: March 7, 2014
4
+
5
+ * Compatibility with the [`composite_primary_keys`](https://github.com/composite-primary-keys/composite_primary_keys)
6
+ gem, so that you can use object-ID columns as part of a composite primary key.
7
+ * Fixed an issue where you could not save an ActiveRecord model that had an ObjectId column as its primary key.
8
+ Implemented this by teaching Arel how to deal with BSON ObjectIds, which should have broader benefits, too.
@@ -1,6 +1,7 @@
1
1
  require "objectid_columns/version"
2
2
  require "objectid_columns/active_record/base"
3
3
  require "objectid_columns/active_record/relation"
4
+ require "objectid_columns/arel/visitors/to_sql"
4
5
  require "active_record"
5
6
 
6
7
  # This is the root module for ObjectidColumns. It contains largely just configuration and integration information;
@@ -118,4 +119,9 @@ end
118
119
  include ::ObjectidColumns::ActiveRecord::Relation
119
120
  end
120
121
 
122
+ # require 'arel/visitors/to_sql'
123
+ ::Arel::Visitors::ToSql.class_eval do
124
+ include ::ObjectidColumns::Arel::Visitors::ToSql
125
+ end
126
+
121
127
  require "objectid_columns/extensions"
@@ -0,0 +1,89 @@
1
+ require 'active_support'
2
+
3
+ module ObjectidColumns
4
+ module Arel
5
+ module Visitors
6
+ # This module gets mixed into Arel::Visitors::ToSql, which is the class that the Arel gem (which is really the
7
+ # backbone of ActiveRecord's query language) uses to generate SQL. This teaches Arel what to do when it bumps
8
+ # into an object of a BSON ID class -- _i.e._, how to convert it to a SQL literal.
9
+ #
10
+ # How this works depends on which version of ActiveRecord -- and therefore AREL -- you're using:
11
+ #
12
+ # * In Arel 4.x, the #visit... methods get called with two arguments. The first is the actual BSON ID that needs
13
+ # to be converted; the second provides context. From the second parameter, we can get the table name and
14
+ # column name. We use this to get a hold of the ObjectidColumnsManager via its class method .for_table, and,
15
+ # from there, a converted, valid value for the column in question (whether hex or binary).
16
+ # * In Arel 2.x (AR 3.0.x) and 3.x, we have to monkeypatch the #visit_Arel_Attributes_Attribute method -- it
17
+ # already picks up and stashes away the .last_column, but we need to add the .last_relation, too.
18
+ #
19
+ module ToSql
20
+ extend ActiveSupport::Concern
21
+
22
+ require 'arel'
23
+ if ::Arel::VERSION =~ /^[23]\./
24
+ def visit_Arel_Attributes_Attribute_with_objectid_columns(o, *args)
25
+ out = visit_Arel_Attributes_Attribute_without_objectid_columns(o, *args)
26
+ self.last_relation = o.relation
27
+ out
28
+ end
29
+
30
+ included do
31
+ alias_method_chain :visit_Arel_Attributes_Attribute, :objectid_columns
32
+
33
+ alias :visit_Arel_Attributes_Integer :visit_Arel_Attributes_Attribute_with_objectid_columns
34
+ alias :visit_Arel_Attributes_Float :visit_Arel_Attributes_Attribute_with_objectid_columns
35
+ alias :visit_Arel_Attributes_Decimal :visit_Arel_Attributes_Attribute_with_objectid_columns
36
+ alias :visit_Arel_Attributes_String :visit_Arel_Attributes_Attribute_with_objectid_columns
37
+ alias :visit_Arel_Attributes_Time :visit_Arel_Attributes_Attribute_with_objectid_columns
38
+ alias :visit_Arel_Attributes_Boolean :visit_Arel_Attributes_Attribute_with_objectid_columns
39
+
40
+ attr_accessor :last_relation
41
+ end
42
+ end
43
+
44
+ def visit_BSON_ObjectId(o, a = nil)
45
+ column = if a then column_for(a) else last_column end
46
+ relation = if a then a.relation else last_relation end
47
+
48
+ raise "no column?!?" unless column
49
+ raise "no relation?!?" unless relation
50
+
51
+ quote(bson_objectid_value_from_parameter(o, column, relation), column)
52
+ end
53
+
54
+ alias_method :visit_Moped_BSON_ObjectId, :visit_BSON_ObjectId
55
+
56
+ private
57
+ def bson_objectid_value_from_parameter(o, column, relation)
58
+ column_name = column.name
59
+
60
+ manager = ObjectidColumns::ObjectidColumnsManager.for_table(relation.name)
61
+ unless manager
62
+ raise %{ObjectidColumns: You're trying to evaluate a SQL statement (in Arel, probably via ActiveRecord)
63
+ that contains a BSON ObjectId value -- you're trying to use the value '#{o}'
64
+ (of class #{o.class.name}) with column #{column_name.inspect} of table
65
+ #{relation.name.inspect}. However, we can't find any record of any ObjectId
66
+ columns being declared for that table anywhere.
67
+
68
+ As a result, we don't know whether this column should be treated as a binary or
69
+ a hexadecimal ObjectId, and hence don't know how to transform this value properly.}
70
+ end
71
+
72
+ unless manager.is_objectid_column?(column_name)
73
+ raise %{ObjectidColumns: You're trying to evaluate a SQL statement (in Arel, probably via ActiveRecord)
74
+ that contains a BSON ObjectId value -- you're trying to use the value '#{o}'
75
+ (of class #{o.class.name}) with column #{column_name.inspect} of table
76
+ #{relation.name.inspect}.
77
+
78
+ While we can find a record of some ObjectId columns being declared for
79
+ that table, they don't appear to include #{column_name.inspect}. As such,
80
+ we don't knwo whether this column should be treated as a binary or a hexadecimal
81
+ ObjectId, and hence don't know how to transform this value properly.}
82
+ end
83
+
84
+ manager.to_valid_value_for_column(column_name, o)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -25,7 +25,7 @@ module ObjectidColumns
25
25
  # Called as a +before_create+ hook, if (and only if) this class has declared +has_objectid_primary_key+ -- sets
26
26
  # the primary key to a newly-generated ObjectId, unless it has one already.
27
27
  def assign_objectid_primary_key
28
- self.id ||= ObjectidColumns.new_objectid
28
+ self.class.objectid_columns_manager.assign_objectid_primary_key(self)
29
29
  end
30
30
 
31
31
  module ClassMethods
@@ -26,6 +26,128 @@ module ObjectidColumns
26
26
  # methods directly on the class, for a number of very good reasons -- see the class comment on
27
27
  # DynamicMethodsModule for more information.
28
28
  @dynamic_methods_module = ObjectidColumns::DynamicMethodsModule.new(active_record_class, :ObjectidColumnsDynamicMethods)
29
+
30
+ self.class.register_for_table(active_record_class.table_name, self)
31
+ end
32
+
33
+ class << self
34
+ # ObjectidColumns::Arel::Visitors::ToSql needs to be able to figure out whether an ObjectId column is of binary
35
+ # or text format, in order to properly transform/quote the value it has. However, by the time the code gets there,
36
+ # we no longer have access to the ActiveRecord model at all. So, instead, we need an entry point to be able to
37
+ # find the ObjectidColumnsManager for a table by name. That's .for_table, below; this is the method called at
38
+ # the end of the constructor of every ObjectidColumnsManager, registering the instance by table name.
39
+ def register_for_table(table_name, instance)
40
+ @_registered_instances ||= { }
41
+ @_registered_instances[table_name] = instance
42
+ end
43
+
44
+ # See above. Given a table name, this returns the ObjectidColumnsManager for it, or +nil+ if none has been
45
+ # defined for that table.
46
+ def for_table(table_name)
47
+ @_registered_instances[table_name]
48
+ end
49
+ end
50
+
51
+ # This method basically says: does our +active_record_class+ have a primary key defined, for real? There are two
52
+ # reasons this is anything more than (<tt>!! active_record_class.primary_key</tt>):
53
+ #
54
+ # * In earlier versions of ActiveRecord (like 3.0.x), this will return +id+ even if you haven't set it and there is
55
+ # no column named +id+.
56
+ # * The +composite_primary_keys+ gem can make this an array instead.
57
+ def activerecord_class_has_no_real_primary_key?
58
+ (! active_record_class.primary_key) ||
59
+ (active_record_class.primary_key == [ ]) ||
60
+ ( ([ [ 'id' ], [ :id ] ].include?(Array(active_record_class.primary_key))) &&
61
+ (! active_record_class.columns_hash.has_key?('id')) &&
62
+ (! active_record_class.columns_hash.has_key?(:id)))
63
+ end
64
+
65
+ # If you haven't specified a primary key on your model (using <tt>self.primary_key=</tt>), and you call
66
+ # +has_objectid_primary_key+, we want to tell the ActiveRecord model that that's the new primary key. This takes
67
+ # care of that, and handles the fact that this may be a composite primary key, too.
68
+ def set_primary_key_from!(primary_keys)
69
+ if primary_keys.length > 1
70
+ active_record_class.primary_key = primary_keys.map(&:to_s)
71
+ elsif primary_keys.length == 1
72
+ active_record_class.primary_key = primary_keys[0].to_s
73
+ else
74
+ # nothing here; we handle this elsewhere
75
+ end
76
+ end
77
+
78
+ # Assigns a new ObjectId primary key to a brand-new model that's about to be created, if needed. This handles
79
+ # composite primary keys correctly.
80
+ def assign_objectid_primary_key(model)
81
+ Array(model.class.primary_key).each do |pk_column|
82
+ if is_objectid_column?(pk_column) && model[pk_column].blank?
83
+ model.send("#{pk_column}=", ObjectidColumns.new_objectid)
84
+ end
85
+ end
86
+ end
87
+
88
+ # Given a model, returns the correct value for #id. This takes into account composite primary keys where some
89
+ # columns may be ObjectId columns and some may not.
90
+ def read_objectid_primary_key(model)
91
+ pks = Array(model.class.primary_key)
92
+ out = [ ]
93
+ pks.each do |pk_column|
94
+ out << if is_objectid_column?(pk_column)
95
+ read_objectid_column(model, pk_column)
96
+ else
97
+ model[pk_column]
98
+ end
99
+ end
100
+ out = out[0] if out.length == 1
101
+ out
102
+ end
103
+
104
+ # Given a model, stores a new value for #id. This takes into account composite primary keys where some
105
+ # columns may be ObjectId columns and some may not.
106
+ def write_objectid_primary_key(model, new_value)
107
+ pks = Array(model.class.primary_key)
108
+ if pks.length == 1
109
+ write_objectid_column(model, pks[0], new_value)
110
+ else
111
+ pks.each_with_index do |pk_column, index|
112
+ value = new_value[index]
113
+ if is_objectid_column?(pk_column)
114
+ write_objectid_column(model, pk_column, value)
115
+ else
116
+ model[pk_column] = value
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ # Implements .find or .find_by_id for classes that have a primary key that has at least one ObjectId column in it;
123
+ # this takes care of handling both normal primary keys and composite primary keys.
124
+ def find_or_find_by_id(*args)
125
+ primary_key = active_record_class.primary_key
126
+ pk_length = primary_key.kind_of?(Array) ? primary_key.length : 1
127
+
128
+ # If we just have a single primary key, we flatten any input, just because that's exactly what base
129
+ # ActiveRecord does...
130
+ if pk_length == 1
131
+ args = args.flatten
132
+ args = args.map { |x| to_valid_value_for_column(primary_key, x) if x }
133
+ yield(*args)
134
+ else
135
+ # composite_primary_keys, however, requires that you pass each key as a single, separate argument to .find or
136
+ # .find_by_id; we transform them here.
137
+ keys = args.map do |key|
138
+ new_key = [ ]
139
+ key.each_with_index do |key_component, index|
140
+ column = primary_key[index]
141
+ new_key << if is_objectid_column?(column)
142
+ to_valid_value_for_column(column, key_component) if key_component
143
+ else
144
+ key_component
145
+ end
146
+ end
147
+ new_key
148
+ end
149
+ yield(*keys)
150
+ end
29
151
  end
30
152
 
31
153
  # Declares that this class is using an ObjectId as its primary key. Ordinarily, this requires no arguments;
@@ -37,40 +159,52 @@ module ObjectidColumns
37
159
  # ObjectIds are safe to generate client-side, and very difficult to properly generate server-side in a relational
38
160
  # database. However, we will respect (and not overwrite) any primary key already assigned to the record before it's
39
161
  # saved, so if you want to assign your own ObjectId primary keys, you can.
40
- def has_objectid_primary_key(primary_key_name = nil)
41
- # The Symbol-vs.-String distinction is critical when dealing with old versions of ActiveRecord; for example, if
42
- # you say <tt>self.primary_key = :foo</tt> (instead of <tt>self.primary_key = 'foo'</tt>) to older versions of
43
- # ActiveRecord, you can end up with some seriously weird errors later (like models trying to save themselves with
44
- # _both_ a +:foo+ and a +'foo'+ attribute -- ick!). Boo, AR.
45
- primary_key_name = primary_key_name.to_s if primary_key_name
46
- pk = active_record_class.primary_key
47
-
48
- # Make sure we know what the primary key is!
49
- if (! pk) && (! primary_key_name)
50
- raise ArgumentError, "Class #{active_record_class.name} has no primary key set, and you haven't supplied one to .has_objectid_primary_key. Either set one before this call (using self.primary_key = :foo), or supply one to this call (has_objectid_primary_key :foo) and we'll set it for you."
162
+ #
163
+ # This method handles composite primary keys, as provided by the +composite_primary_keys+ gem, correctly.
164
+ def has_objectid_primary_key(*primary_keys_that_are_objectid_columns)
165
+ # First, normalize our set of primary keys that are ObjectId columns...
166
+ primary_keys_that_are_objectid_columns = primary_keys_that_are_objectid_columns.compact.map(&:to_s).uniq
167
+
168
+ # Now, see what all the primary keys are. If the user hasn't specified any primary keys on the class at all yet,
169
+ # but has told us what they are, then we need to tell ActiveRecord what they are.
170
+ all_primary_keys = if activerecord_class_has_no_real_primary_key?
171
+ set_primary_key_from!(primary_keys_that_are_objectid_columns)
172
+ primary_keys_that_are_objectid_columns
173
+ else
174
+ Array(active_record_class.primary_key)
51
175
  end
176
+ # Normalize the set of all primary keys.
177
+ all_primary_keys = all_primary_keys.compact.map(&:to_s).uniq
52
178
 
53
- pk = pk.to_s if pk
179
+ # Let's make sure we have a primary key...
180
+ raise ArgumentError, "Class #{active_record_class.name} has no primary key set, and you haven't supplied one to #has_objectid_primary_key" if all_primary_keys.empty?
54
181
 
55
- # Initially, this was a simple +||=+ statement. However, older versions of ActiveRecord will return the string or
56
- # symbol +id+ for the primary key if you haven't set an explicit primary key, even if there is no such column on
57
- # the underlying table. Again, ick.
58
- if (! pk) || (primary_key_name && pk.to_s != primary_key_name.to_s)
59
- active_record_class.primary_key = pk = primary_key_name
182
+ # If you didn't specify any ObjectId columns explicitly, use what we know about the class to figure out which
183
+ # ones you mean.
184
+ if primary_keys_that_are_objectid_columns.empty?
185
+ if all_primary_keys.length == 1
186
+ primary_keys_that_are_objectid_columns = all_primary_keys
187
+ else
188
+ primary_keys_that_are_objectid_columns = autodetect_columns_from(all_primary_keys, true)
189
+ end
60
190
  end
61
191
 
62
- # In case someone is using composite_primary_keys (http://compositekeys.rubyforge.org/).
63
- raise "You can't have an ObjectId primary key that's not a String or Symbol: #{pk.inspect}" unless pk.kind_of?(String) || pk.kind_of?(Symbol)
192
+ # Make sure we have at least one ObjectId primary key, if we're in this method.
193
+ raise "Class #{active_record_class.name} has no columns in its primary key that qualify as object IDs automatically; you must specify their names explicitly." if primary_keys_that_are_objectid_columns.empty?
194
+
195
+ # Make sure all the columns the user named actually exist as columns on the model.
196
+ missing = primary_keys_that_are_objectid_columns.select { |c| ! active_record_class.columns_hash.has_key?(c) }
197
+ raise "The following primary-key column(s) do not appear to actually exist on #{active_record_class.name}: #{missing.inspect}; we have these columns: #{active_record_class.columns_hash.keys.inspect}" unless missing.empty?
64
198
 
65
199
  # Declare our primary-key column as an ObjectId column.
66
- has_objectid_column pk
67
-
68
- # If it's not called just "id", we need to explicitly define an "id" method that correctly reads from and writes
69
- # to the table.
70
- unless pk.to_s == 'id'
71
- p = pk
72
- dynamic_methods_module.define_method("id") { read_objectid_column(p) }
73
- dynamic_methods_module.define_method("id=") { |new_value| write_objectid_column(p, new_value) }
200
+ has_objectid_column *primary_keys_that_are_objectid_columns
201
+
202
+ # Override #id and #id= to do the right thing...
203
+ dynamic_methods_module.define_method("id") do
204
+ self.class.objectid_columns_manager.read_objectid_primary_key(self)
205
+ end
206
+ dynamic_methods_module.define_method("id=") do |new_value|
207
+ self.class.objectid_columns_manager.write_objectid_primary_key(self, new_value)
74
208
  end
75
209
 
76
210
  # Allow us to autogenerate the primary key, if needed, on save.
@@ -79,17 +213,7 @@ module ObjectidColumns
79
213
  # Override a couple of methods that, if you're using an ObjectId column as your primary key, need overriding. ;)
80
214
  [ :find, :find_by_id ].each do |class_method_name|
81
215
  @dynamic_methods_module.define_class_method(class_method_name) do |*args, &block|
82
- if args.length == 1 && args[0].kind_of?(String) || ObjectidColumns.is_valid_bson_object?(args[0]) || args[0].kind_of?(Array)
83
- args[0] = if args[0].kind_of?(Array)
84
- args[0].map { |x| objectid_columns_manager.to_valid_value_for_column(primary_key, x) if x }
85
- else
86
- objectid_columns_manager.to_valid_value_for_column(primary_key, args[0]) if args[0]
87
- end
88
-
89
- super(args[0], &block)
90
- else
91
- super(*args, &block)
92
- end
216
+ objectid_columns_manager.find_or_find_by_id(*args) { |*new_args| super(*new_args, &block) }
93
217
  end
94
218
  end
95
219
  end
@@ -104,7 +228,7 @@ module ObjectidColumns
104
228
  return unless active_record_class.table_exists?
105
229
 
106
230
  # Autodetect columns ending in +_oid+ if needed
107
- columns = autodetect_columns if columns.length == 0
231
+ columns = autodetect_columns_from(active_record_class.columns_hash.keys) if columns.length == 0
108
232
 
109
233
  columns = columns.map { |c| c.to_s.strip.downcase.to_sym }
110
234
  columns.each do |column_name|
@@ -150,10 +274,11 @@ module ObjectidColumns
150
274
  column_name = column_name.to_s
151
275
  value = model[column_name]
152
276
  return value unless value # in case it's nil
277
+ return value if ObjectidColumns.is_valid_bson_object?(value) # we can get this when reading the 'id' pseudocolumn
153
278
 
154
279
  # If it's not nil, the database should always be giving us back a String...
155
280
  unless value.kind_of?(String)
156
- raise "When trying to read the ObjectId column #{column_name.inspect} on #{inspect}, we got the following data from the database; we expected a String: #{value.inspect}"
281
+ raise "When trying to read the ObjectId column #{column_name.inspect} on #{active_record_class.name} ID=#{model.id.inspect}, we got the following data from the database; we expected a String: #{value.inspect}"
157
282
  end
158
283
 
159
284
  # ugh...ActiveRecord 3.1.x can return this in certain circumstances
@@ -163,7 +288,7 @@ module ObjectidColumns
163
288
  # you get back all 16 anyway, with 0x00 bytes at the end. Converting this to an ObjectId will fail, so we make
164
289
  # sure we chop those bytes off. (Note that while String#strip will, in fact, remove these bytes too, it is not
165
290
  # safe: if the ObjectId itself ends in one or more 0x00 bytes, then these will get incorrectly removed.)
166
- case objectid_column_type(column_name)
291
+ case type = objectid_column_type(column_name)
167
292
  when :binary then value = value[0..(BINARY_OBJECTID_LENGTH - 1)]
168
293
  when :string then value = value[0..(STRING_OBJECTID_LENGTH - 1)]
169
294
  else unknown_type(type)
@@ -252,6 +377,11 @@ module ObjectidColumns
252
377
  end
253
378
  end
254
379
 
380
+ # Given the name of a column, tell whether or not it is an ObjectId column.
381
+ def is_objectid_column?(column_name)
382
+ oid_columns.has_key?(column_name.to_sym)
383
+ end
384
+
255
385
  private
256
386
  attr_reader :active_record_class, :dynamic_methods_module, :oid_columns
257
387
 
@@ -282,14 +412,18 @@ module ObjectidColumns
282
412
 
283
413
  # If someone called +has_objectid_columns+ but didn't pass an argument, this method detects which columns we should
284
414
  # automatically turn into ObjectId columns -- which means any columns ending in +_oid+, except for the primary key.
285
- def autodetect_columns
286
- out = active_record_class.columns.select { |c| c.name =~ /_oid$/i }.map(&:name).map(&:to_s)
415
+ def autodetect_columns_from(column_names, allow_primary_key = false)
416
+ column_names = column_names.map(&:to_s)
417
+ out = column_names.select do |column_name|
418
+ column = active_record_class.columns_hash[column_name]
419
+ column && column.name =~ /_oid$/i
420
+ end
287
421
 
288
422
  # Make sure we never, ever automatically make the primary-key column an ObjectId column.
289
- out -= [ active_record_class.primary_key ].compact.map(&:to_s)
423
+ out -= Array(active_record_class.primary_key).compact.map(&:to_s) unless allow_primary_key
290
424
 
291
425
  unless out.length > 0
292
- raise ArgumentError, "You didn't pass in the names of any ObjectId columns, and we couldn't find any columns ending in _oid to pick up automatically (primary key is always excluded). Either name some columns explicitly, or remove the has_objectid_columns call."
426
+ raise ArgumentError, "You didn't pass in the names of any ObjectId columns, and we couldn't find any columns ending in _oid to pick up automatically (primary key is always excluded). Either name some columns explicitly, or remove the has_objectid_columns call. We found columns named: #{column_names.inspect}"
293
427
  end
294
428
 
295
429
  out
@@ -1,4 +1,4 @@
1
1
  # What's the current version of this gem?
2
2
  module ObjectidColumns
3
- VERSION = "1.0.0"
3
+ VERSION = "1.0.1"
4
4
  end
@@ -48,4 +48,24 @@ Gem::Specification.new do |spec|
48
48
  else
49
49
  spec.add_development_dependency(database_gem_name)
50
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\./
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
51
71
  end
@@ -1,6 +1,16 @@
1
1
  require 'objectid_columns'
2
2
  require 'objectid_columns/helpers/system_helpers'
3
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
+
4
14
  unless defined?(VALID_OBJECTID_CLASSES)
5
15
  VALID_OBJECTID_CLASSES = [ BSON::ObjectId ]
6
16
  VALID_OBJECTID_CLASSES << Moped::BSON::ObjectId if defined?(Moped::BSON::ObjectId)
@@ -81,6 +91,115 @@ describe "ObjectidColumns basic operations" do
81
91
  expect { ::SpectableNonexistent.class_eval { has_objectid_column :foo } }.to_not raise_error
82
92
  end
83
93
 
94
+ if $composite_primary_keys_available
95
+ describe "composite primary key support" do
96
+ context "with an implicit PK" do
97
+ before :each do
98
+ migrate do
99
+ drop_table :objectidcols_spec_pk_cmp rescue nil
100
+ create_table :objectidcols_spec_pk_cmp, :id => false do |t|
101
+ t.binary :some_oid, :null => false
102
+ t.string :more_pk, :null => false
103
+ t.string :value
104
+ end
105
+ end
106
+
107
+ define_model_class(:SpectablePkCmp, :objectidcols_spec_pk_cmp) do
108
+ if respond_to?(:primary_keys=)
109
+ self.primary_keys = [ 'some_oid', 'more_pk' ]
110
+ else
111
+ self.set_primary_keys('some_oid', 'more_pk')
112
+ end
113
+ end
114
+ ::SpectablePkCmp.class_eval { has_objectid_primary_key }
115
+ @model_class = ::SpectablePkCmp
116
+ end
117
+
118
+ it "should allow using a composite primary key in individual parts" do
119
+ pending "disabled" unless $composite_primary_keys_available
120
+
121
+ instance = @model_class.new
122
+ instance.some_oid = new_oid
123
+ instance.more_pk = "foo"
124
+ instance.value = "foo value"
125
+ instance.save!
126
+
127
+ instance_again = @model_class.find([ instance.some_oid, instance.more_pk ])
128
+ expect(instance_again.value).to eq(instance.value)
129
+ expect(instance_again.some_oid).to eq(instance.some_oid)
130
+ expect(instance_again.more_pk).to eq(instance.more_pk)
131
+ end
132
+
133
+ it "should allow using a composite primary key as a whole" do
134
+ pending "disabled" unless $composite_primary_keys_available
135
+
136
+ oid = new_oid
137
+ instance = @model_class.new
138
+ instance.id = [ oid, "foo" ]
139
+ instance.value = "foo value"
140
+ instance.save!
141
+
142
+ expect(instance.some_oid).to be_an_objectid_object_matching(oid)
143
+ expect(instance.more_pk).to eq("foo")
144
+ expect(instance.value).to eq("foo value")
145
+
146
+ instance_again = @model_class.find(instance.id)
147
+ expect(instance_again.id).to eq(instance.id)
148
+ expect(instance_again.some_oid).to be_an_objectid_object_matching(oid)
149
+ expect(instance_again.more_pk).to eq("foo")
150
+ expect(instance_again.value).to eq("foo value")
151
+ expect(instance_again.id).to be_kind_of(Array)
152
+ expect(instance_again.id.length).to eq(2)
153
+ expect(instance_again.id[0]).to be_an_objectid_object_matching(oid)
154
+ expect(instance_again.id[1]).to eq("foo")
155
+ end
156
+ end
157
+
158
+ context "with an explicit PK" do
159
+ before :each do
160
+ migrate do
161
+ drop_table :objectidcols_spec_pk_cmp_2 rescue nil
162
+ create_table :objectidcols_spec_pk_cmp_2, :id => false do |t|
163
+ t.binary :one, :null => false
164
+ t.string :two, :null => false
165
+ t.string :three, :null => false
166
+ t.string :value
167
+ end
168
+ end
169
+
170
+ define_model_class(:SpectablePkCmp2, :objectidcols_spec_pk_cmp_2) do
171
+ if respond_to?(:primary_keys=)
172
+ self.primary_keys = [ 'one', 'two', 'three' ]
173
+ else
174
+ self.set_primary_keys('one', 'two', 'three')
175
+ end
176
+ end
177
+ ::SpectablePkCmp2.class_eval { has_objectid_primary_key :one, :three }
178
+ @model_class = ::SpectablePkCmp2
179
+ end
180
+
181
+ it "should allow using a composite primary key that's partially ObjectId and partially not" do
182
+ instance = @model_class.new
183
+ instance.two = "foo"
184
+ instance.value = "foo_value"
185
+ instance.save!
186
+
187
+ expect(instance.id).to be_kind_of(Array)
188
+ expect(instance.id[0]).to be_an_objectid_object
189
+ expect(instance.id[1]).to eq("foo")
190
+ expect(instance.id[2]).to be_an_objectid_object
191
+
192
+ id = instance.id
193
+ instance_again = @model_class.find(id)
194
+ expect(instance_again.id).to eq(id)
195
+ expect(instance_again.id[0]).to be_an_objectid_object_matching(id[0])
196
+ expect(instance_again.id[1]).to eq("foo")
197
+ expect(instance_again.id[2]).to be_an_objectid_object_matching(id[2])
198
+ end
199
+ end
200
+ end
201
+ end
202
+
84
203
  describe "primary key column support" do
85
204
  before :each do
86
205
  migrate do
@@ -174,6 +293,22 @@ describe "ObjectidColumns basic operations" do
174
293
  expect(@model_class.send(find_by_id_method, new_oid)).to be_nil
175
294
  end
176
295
 
296
+ it "should let you load and save objects properly" do
297
+ r1 = @model_class.new
298
+ r1.name = 'row 1'
299
+ r1.id = new_oid
300
+ r1.save!
301
+
302
+ r1_again = @model_class.find(@tc.from_string(r1.id.to_s))
303
+ expect(r1_again.name).to eq('row 1')
304
+ r1_again.id = @tc.from_string(r1.id.to_s)
305
+ r1_again.name = 'row 1 again'
306
+ r1_again.save!
307
+
308
+ r1_yet_again = @model_class.find(r1_again.id)
309
+ expect(r1_yet_again.name).to eq('row 1 again')
310
+ end
311
+
177
312
  it "should not pick up primary-key columns automatically, even if they're named _oid" do
178
313
  migrate do
179
314
  drop_table :objectidcols_spec_pk_auto rescue nil
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: objectid_columns
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Geweke
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2014-01-31 00:00:00 Z
12
+ date: 2014-03-08 00:00:00 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -86,6 +86,14 @@ dependencies:
86
86
  - *id007
87
87
  type: :development
88
88
  version_requirements: *id008
89
+ - !ruby/object:Gem::Dependency
90
+ name: composite_primary_keys
91
+ prerelease: false
92
+ requirement: &id009 !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - *id007
95
+ type: :development
96
+ version_requirements: *id009
89
97
  description:
90
98
  email:
91
99
  - ageweke@swiftype.com
@@ -98,6 +106,7 @@ extra_rdoc_files: []
98
106
  files:
99
107
  - .gitignore
100
108
  - .travis.yml
109
+ - CHANGES.md
101
110
  - Gemfile
102
111
  - LICENSE.txt
103
112
  - README.md
@@ -105,6 +114,7 @@ files:
105
114
  - lib/objectid_columns.rb
106
115
  - lib/objectid_columns/active_record/base.rb
107
116
  - lib/objectid_columns/active_record/relation.rb
117
+ - lib/objectid_columns/arel/visitors/to_sql.rb
108
118
  - lib/objectid_columns/dynamic_methods_module.rb
109
119
  - lib/objectid_columns/extensions.rb
110
120
  - lib/objectid_columns/has_objectid_columns.rb