objectid_columns 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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