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 +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
|