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 +4 -4
- data/CHANGES.md +8 -0
- data/lib/objectid_columns.rb +6 -0
- data/lib/objectid_columns/arel/visitors/to_sql.rb +89 -0
- data/lib/objectid_columns/has_objectid_columns.rb +1 -1
- data/lib/objectid_columns/objectid_columns_manager.rb +179 -45
- data/lib/objectid_columns/version.rb +1 -1
- data/objectid_columns.gemspec +20 -0
- data/spec/objectid_columns/system/basic_system_spec.rb +135 -0
- metadata +12 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA512:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 216e3a0b18aa9ff589f728a7ba6d95fa56ada067b51a99cff26dac0b19cb3f49cb4fa9deb2b2ae227498123bb5d8aa286aa76760eed1fbf96cfc6599dd7e423f
|
4
|
+
data.tar.gz: c9a48a9b4e065da3a3b45555cf7c2acd0c3a5687c6e5cf752318934553110fed9906d7d72b4e87cef431a4df7386e5fab1001c0ffbcf4cef655b8e59f8d1a169
|
5
5
|
SHA1:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
data/lib/objectid_columns.rb
CHANGED
@@ -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.
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
#
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
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
|
-
#
|
56
|
-
#
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
#
|
63
|
-
raise "
|
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
|
67
|
-
|
68
|
-
#
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
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 =
|
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},
|
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
|
286
|
-
|
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 -=
|
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
|
data/objectid_columns.gemspec
CHANGED
@@ -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.
|
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-
|
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
|