hstore_accessor 0.6.1 → 0.9.0
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/.gitignore +1 -0
- data/.rubocop.yml +41 -0
- data/Appraisals +11 -0
- data/Gemfile +1 -5
- data/README.md +128 -57
- data/Rakefile +8 -2
- data/gemfiles/activerecord_4.0.gemfile +11 -0
- data/gemfiles/activerecord_4.1.gemfile +11 -0
- data/gemfiles/activerecord_4.2.gemfile +11 -0
- data/hstore_accessor.gemspec +16 -10
- data/lib/hstore_accessor.rb +10 -4
- data/lib/hstore_accessor/active_record_4.2/type_helpers.rb +34 -0
- data/lib/hstore_accessor/{time_helper.rb → active_record_<_4.2/time_helper.rb} +2 -4
- data/lib/hstore_accessor/active_record_<_4.2/type_helpers.rb +42 -0
- data/lib/hstore_accessor/macro.rb +110 -46
- data/lib/hstore_accessor/serialization.rb +39 -27
- data/lib/hstore_accessor/version.rb +1 -1
- data/spec/hstore_accessor_spec.rb +281 -85
- data/spec/spec_helper.rb +14 -0
- metadata +85 -17
- data/lib/hstore_accessor/type_helpers.rb +0 -20
data/lib/hstore_accessor.rb
CHANGED
@@ -1,10 +1,16 @@
|
|
1
|
+
require "active_support"
|
2
|
+
require "active_record"
|
1
3
|
require "hstore_accessor/version"
|
2
|
-
|
4
|
+
|
5
|
+
if ::ActiveRecord::VERSION::STRING.to_f >= 4.2
|
6
|
+
require "hstore_accessor/active_record_4.2/type_helpers"
|
7
|
+
else
|
8
|
+
require "hstore_accessor/active_record_<_4.2/type_helpers"
|
9
|
+
require "hstore_accessor/active_record_<_4.2/time_helper"
|
10
|
+
end
|
11
|
+
|
3
12
|
require "hstore_accessor/serialization"
|
4
13
|
require "hstore_accessor/macro"
|
5
|
-
require "hstore_accessor/time_helper"
|
6
|
-
require "active_support"
|
7
|
-
require "active_record"
|
8
14
|
require "bigdecimal"
|
9
15
|
|
10
16
|
module HstoreAccessor
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module HstoreAccessor
|
2
|
+
module TypeHelpers
|
3
|
+
TYPES = {
|
4
|
+
boolean: ActiveRecord::Type::Boolean,
|
5
|
+
date: ActiveRecord::Type::Date,
|
6
|
+
datetime: ActiveRecord::Type::DateTime,
|
7
|
+
decimal: ActiveRecord::Type::Decimal,
|
8
|
+
float: ActiveRecord::Type::Float,
|
9
|
+
integer: ActiveRecord::Type::Integer,
|
10
|
+
string: ActiveRecord::Type::String
|
11
|
+
}
|
12
|
+
|
13
|
+
TYPES.default = ActiveRecord::Type::Value
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def column_type_for(attribute, data_type)
|
17
|
+
ActiveRecord::ConnectionAdapters::Column.new(attribute.to_s, nil, TYPES[data_type].new)
|
18
|
+
end
|
19
|
+
|
20
|
+
def cast(type, value)
|
21
|
+
return nil if value.nil?
|
22
|
+
|
23
|
+
case type
|
24
|
+
when :string, :hash, :array, :decimal
|
25
|
+
value
|
26
|
+
when :integer, :float, :datetime, :date, :boolean
|
27
|
+
TYPES[type].new.type_cast_from_user(value)
|
28
|
+
else value
|
29
|
+
# Nothing.
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
module HstoreAccessor
|
2
2
|
module TimeHelper
|
3
|
-
|
4
3
|
# There is a bug in ActiveRecord::ConnectionAdapters::Column#string_to_time
|
5
4
|
# which drops the timezone. This has been fixed, but not released.
|
6
5
|
# This method includes the fix. See: https://github.com/rails/rails/pull/12290
|
@@ -11,10 +10,10 @@ module HstoreAccessor
|
|
11
10
|
|
12
11
|
time_hash = Date._parse(string)
|
13
12
|
time_hash[:sec_fraction] = ActiveRecord::ConnectionAdapters::Column.send(:microseconds, time_hash)
|
14
|
-
|
13
|
+
year, mon, mday, hour, min, sec, microsec, offset = time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)
|
15
14
|
|
16
15
|
# Treat 0000-00-00 00:00:00 as nil.
|
17
|
-
return nil if year.nil? ||
|
16
|
+
return nil if year.nil? || [year, mon, mday].all?(&:zero?)
|
18
17
|
|
19
18
|
if offset
|
20
19
|
time = Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
|
@@ -26,6 +25,5 @@ module HstoreAccessor
|
|
26
25
|
Time.public_send(ActiveRecord::Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
|
27
26
|
end
|
28
27
|
end
|
29
|
-
|
30
28
|
end
|
31
29
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module HstoreAccessor
|
2
|
+
module TypeHelpers
|
3
|
+
TYPES = {
|
4
|
+
string: "char",
|
5
|
+
datetime: "datetime",
|
6
|
+
date: "date",
|
7
|
+
float: "float",
|
8
|
+
boolean: "boolean",
|
9
|
+
decimal: "decimal",
|
10
|
+
integer: "int"
|
11
|
+
}
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def column_type_for(attribute, data_type)
|
15
|
+
ActiveRecord::ConnectionAdapters::Column.new(attribute.to_s, nil, TYPES[data_type])
|
16
|
+
end
|
17
|
+
|
18
|
+
def cast(type, value)
|
19
|
+
return nil if value.nil?
|
20
|
+
|
21
|
+
column_class = ActiveRecord::ConnectionAdapters::Column
|
22
|
+
|
23
|
+
case type
|
24
|
+
when :string, :hash, :array, :decimal
|
25
|
+
value
|
26
|
+
when :integer
|
27
|
+
column_class.value_to_integer(value)
|
28
|
+
when :float
|
29
|
+
value.to_f
|
30
|
+
when :datetime
|
31
|
+
TimeHelper.string_to_time(value)
|
32
|
+
when :date
|
33
|
+
column_class.value_to_date(value)
|
34
|
+
when :boolean
|
35
|
+
column_class.value_to_boolean(value)
|
36
|
+
else value
|
37
|
+
# Nothing.
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -1,18 +1,52 @@
|
|
1
1
|
module HstoreAccessor
|
2
2
|
module Macro
|
3
|
-
|
4
3
|
module ClassMethods
|
5
|
-
|
6
4
|
def hstore_accessor(hstore_attribute, fields)
|
7
|
-
|
8
|
-
|
5
|
+
@@hstore_keys_and_types ||= {}
|
6
|
+
|
7
|
+
"hstore_metadata_for_#{hstore_attribute}".tap do |method_name|
|
8
|
+
singleton_class.send(:define_method, method_name) do
|
9
|
+
fields
|
10
|
+
end
|
11
|
+
|
12
|
+
delegate method_name, to: :class
|
9
13
|
end
|
10
14
|
|
11
15
|
field_methods = Module.new
|
12
|
-
fields.each do |key, type|
|
13
16
|
|
17
|
+
if ActiveRecord::VERSION::STRING.to_f >= 4.2
|
18
|
+
singleton_class.send(:define_method, :type_for_attribute) do |attribute|
|
19
|
+
data_type = @@hstore_keys_and_types[attribute]
|
20
|
+
if data_type
|
21
|
+
TypeHelpers::TYPES[data_type].new
|
22
|
+
else
|
23
|
+
super(attribute)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
singleton_class.send(:define_method, :column_for_attribute) do |attribute|
|
28
|
+
data_type = @@hstore_keys_and_types[attribute.to_s]
|
29
|
+
if data_type
|
30
|
+
TypeHelpers.column_type_for(attribute.to_s, data_type)
|
31
|
+
else
|
32
|
+
super(attribute)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
else
|
36
|
+
field_methods.send(:define_method, :column_for_attribute) do |attribute|
|
37
|
+
data_type = @@hstore_keys_and_types[attribute.to_s]
|
38
|
+
if data_type
|
39
|
+
TypeHelpers.column_type_for(attribute.to_s, data_type)
|
40
|
+
else
|
41
|
+
super(attribute)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
fields.each do |key, type|
|
14
47
|
data_type = type
|
15
48
|
store_key = key
|
49
|
+
|
16
50
|
if type.is_a?(Hash)
|
17
51
|
type = type.with_indifferent_access
|
18
52
|
data_type = type[:data_type]
|
@@ -23,71 +57,101 @@ module HstoreAccessor
|
|
23
57
|
|
24
58
|
raise Serialization::InvalidDataTypeError unless Serialization::VALID_TYPES.include?(data_type)
|
25
59
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
60
|
+
@@hstore_keys_and_types[key.to_s] = data_type
|
61
|
+
|
62
|
+
field_methods.instance_eval do
|
63
|
+
define_method("#{key}=") do |value|
|
64
|
+
casted_value = TypeHelpers.cast(data_type, value)
|
65
|
+
serialized_value = Serialization.serialize(data_type, casted_value)
|
66
|
+
|
67
|
+
unless send(key) == casted_value
|
68
|
+
send("#{hstore_attribute}_will_change!")
|
69
|
+
end
|
70
|
+
|
71
|
+
send("#{hstore_attribute}=", (send(hstore_attribute) || {}).merge(store_key.to_s => serialized_value))
|
32
72
|
end
|
33
|
-
send("#{hstore_attribute}=", (send(hstore_attribute) || {}).merge(store_key.to_s => serialized_value))
|
34
|
-
end
|
35
73
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
74
|
+
define_method(key) do
|
75
|
+
value = send(hstore_attribute) && send(hstore_attribute).with_indifferent_access[store_key.to_s]
|
76
|
+
Serialization.deserialize(data_type, value)
|
77
|
+
end
|
40
78
|
|
41
|
-
|
42
|
-
|
43
|
-
|
79
|
+
define_method("#{key}?") do
|
80
|
+
send(key).present?
|
81
|
+
end
|
44
82
|
|
45
|
-
|
46
|
-
|
47
|
-
|
83
|
+
define_method("#{key}_changed?") do
|
84
|
+
send("#{key}_change").present?
|
85
|
+
end
|
48
86
|
|
49
|
-
|
50
|
-
|
51
|
-
|
87
|
+
define_method("#{key}_was") do
|
88
|
+
(send(:attribute_was, hstore_attribute.to_s) || {})[key.to_s]
|
89
|
+
end
|
90
|
+
|
91
|
+
define_method("#{key}_change") do
|
92
|
+
hstore_changes = send("#{hstore_attribute}_change")
|
93
|
+
return if hstore_changes.nil?
|
94
|
+
attribute_changes = hstore_changes.map { |change| change.try(:[], store_key.to_s) }
|
95
|
+
attribute_changes.compact.present? ? attribute_changes : nil
|
96
|
+
end
|
52
97
|
|
53
|
-
|
54
|
-
|
98
|
+
define_method("restore_#{key}!") do
|
99
|
+
old_hstore = send("#{hstore_attribute}_change").try(:first) || {}
|
100
|
+
send("#{key}=", old_hstore[key.to_s])
|
101
|
+
end
|
102
|
+
|
103
|
+
define_method("reset_#{key}!") do
|
104
|
+
if ActiveRecord::VERSION::STRING.to_f >= 4.2
|
105
|
+
ActiveSupport::Deprecation.warn(<<-MSG.squish)
|
106
|
+
`#reset_#{key}!` is deprecated and will be removed on Rails 5.
|
107
|
+
Please use `#restore_#{key}!` instead.
|
108
|
+
MSG
|
109
|
+
end
|
110
|
+
send("restore_#{key}!")
|
111
|
+
end
|
112
|
+
|
113
|
+
define_method("#{key}_will_change!") do
|
114
|
+
send("#{hstore_attribute}_will_change!")
|
115
|
+
end
|
55
116
|
end
|
56
117
|
|
57
118
|
query_field = "#{hstore_attribute} -> '#{store_key}'"
|
119
|
+
eq_query_field = "#{hstore_attribute} @> hstore('#{store_key}', ?)"
|
58
120
|
|
59
121
|
case data_type
|
60
122
|
when :string
|
61
|
-
send(:scope, "with_#{key}", -> value { where(
|
62
|
-
when :integer
|
63
|
-
send(:scope, "#{key}_lt",
|
123
|
+
send(:scope, "with_#{key}", -> value { where(eq_query_field, value.to_s) })
|
124
|
+
when :integer
|
125
|
+
send(:scope, "#{key}_lt", -> value { where("(#{query_field})::#{data_type} < ?", value.to_s) })
|
126
|
+
send(:scope, "#{key}_lte", -> value { where("(#{query_field})::#{data_type} <= ?", value.to_s) })
|
127
|
+
send(:scope, "#{key}_eq", -> value { where(eq_query_field, value.to_s) })
|
128
|
+
send(:scope, "#{key}_gte", -> value { where("(#{query_field})::#{data_type} >= ?", value.to_s) })
|
129
|
+
send(:scope, "#{key}_gt", -> value { where("(#{query_field})::#{data_type} > ?", value.to_s) })
|
130
|
+
when :float, :decimal
|
131
|
+
send(:scope, "#{key}_lt", -> value { where("(#{query_field})::#{data_type} < ?", value.to_s) })
|
64
132
|
send(:scope, "#{key}_lte", -> value { where("(#{query_field})::#{data_type} <= ?", value.to_s) })
|
65
|
-
send(:scope, "#{key}_eq",
|
133
|
+
send(:scope, "#{key}_eq", -> value { where("(#{query_field})::#{data_type} = ?", value.to_s) })
|
66
134
|
send(:scope, "#{key}_gte", -> value { where("(#{query_field})::#{data_type} >= ?", value.to_s) })
|
67
|
-
send(:scope, "#{key}_gt",
|
68
|
-
when :
|
135
|
+
send(:scope, "#{key}_gt", -> value { where("(#{query_field})::#{data_type} > ?", value.to_s) })
|
136
|
+
when :datetime
|
69
137
|
send(:scope, "#{key}_before", -> value { where("(#{query_field})::integer < ?", value.to_i) })
|
70
|
-
send(:scope, "#{key}_eq",
|
71
|
-
send(:scope, "#{key}_after",
|
138
|
+
send(:scope, "#{key}_eq", -> value { where(eq_query_field, value.to_i.to_s) })
|
139
|
+
send(:scope, "#{key}_after", -> value { where("(#{query_field})::integer > ?", value.to_i) })
|
72
140
|
when :date
|
73
141
|
send(:scope, "#{key}_before", -> value { where("#{query_field} < ?", value.to_s) })
|
74
|
-
send(:scope, "#{key}_eq",
|
75
|
-
send(:scope, "#{key}_after",
|
142
|
+
send(:scope, "#{key}_eq", -> value { where(eq_query_field, value.to_s) })
|
143
|
+
send(:scope, "#{key}_after", -> value { where("#{query_field} > ?", value.to_s) })
|
76
144
|
when :boolean
|
77
|
-
send(:scope, "is_#{key}", -> { where("
|
78
|
-
send(:scope, "not_#{key}", -> { where("
|
145
|
+
send(:scope, "is_#{key}", -> { where(eq_query_field, "true") })
|
146
|
+
send(:scope, "not_#{key}", -> { where(eq_query_field, "false") })
|
79
147
|
when :array
|
80
|
-
send(:scope, "#{key}_eq",
|
81
|
-
send(:scope, "#{key}_contains",
|
82
|
-
where("string_to_array(#{query_field}, '#{Serialization::SEPARATOR}') @> string_to_array(?, '#{Serialization::SEPARATOR}')", Array[value].flatten.join(Serialization::SEPARATOR))
|
83
|
-
end)
|
148
|
+
send(:scope, "#{key}_eq", -> value { where("#{query_field} = ?", value.join(Serialization::SEPARATOR)) })
|
149
|
+
send(:scope, "#{key}_contains", -> value { where("string_to_array(#{query_field}, '#{Serialization::SEPARATOR}') @> string_to_array(?, '#{Serialization::SEPARATOR}')", Array[value].flatten.join(Serialization::SEPARATOR)) })
|
84
150
|
end
|
85
151
|
end
|
86
152
|
|
87
153
|
include field_methods
|
88
154
|
end
|
89
|
-
|
90
155
|
end
|
91
|
-
|
92
156
|
end
|
93
157
|
end
|
@@ -1,45 +1,57 @@
|
|
1
1
|
module HstoreAccessor
|
2
2
|
module Serialization
|
3
|
-
|
4
3
|
InvalidDataTypeError = Class.new(StandardError)
|
5
4
|
|
6
|
-
VALID_TYPES = [
|
7
|
-
|
8
|
-
|
5
|
+
VALID_TYPES = [
|
6
|
+
:array,
|
7
|
+
:boolean,
|
8
|
+
:date,
|
9
|
+
:datetime,
|
10
|
+
:decimal,
|
11
|
+
:float,
|
12
|
+
:hash,
|
13
|
+
:integer,
|
14
|
+
:string
|
15
|
+
]
|
9
16
|
|
10
17
|
DEFAULT_SERIALIZER = ->(value) { value.to_s }
|
11
18
|
DEFAULT_DESERIALIZER = DEFAULT_SERIALIZER
|
12
19
|
|
20
|
+
SEPARATOR = "||;||"
|
21
|
+
|
13
22
|
SERIALIZERS = {
|
14
|
-
array:
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
23
|
+
array: -> value { (value && value.join(SEPARATOR)) || nil },
|
24
|
+
boolean: -> value { (value.to_s == "true").to_s },
|
25
|
+
date: -> value { value && value.to_s },
|
26
|
+
hash: -> value { (value && value.to_json) || nil },
|
27
|
+
datetime: -> value { value && value.to_i }
|
19
28
|
}
|
29
|
+
SERIALIZERS.default = DEFAULT_SERIALIZER
|
20
30
|
|
21
31
|
DESERIALIZERS = {
|
22
|
-
array:
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
32
|
+
array: -> value { (value && value.split(SEPARATOR)) || nil },
|
33
|
+
boolean: -> value { TypeHelpers.cast(:boolean, value) },
|
34
|
+
date: -> value { value && Date.parse(value) },
|
35
|
+
decimal: -> value { value && BigDecimal.new(value) },
|
36
|
+
float: -> value { value && value.to_f },
|
37
|
+
hash: -> value { (value && JSON.parse(value)) || nil },
|
38
|
+
integer: -> value { value && value.to_i },
|
39
|
+
datetime: -> value { value && Time.at(value.to_i).in_time_zone }
|
30
40
|
}
|
41
|
+
DESERIALIZERS.default = DEFAULT_DESERIALIZER
|
31
42
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
serializer.call(value)
|
36
|
-
end
|
43
|
+
class << self
|
44
|
+
def serialize(type, value, serializer=SERIALIZERS[type])
|
45
|
+
return nil if value.nil?
|
37
46
|
|
38
|
-
|
39
|
-
|
40
|
-
deserializer ||= (DESERIALIZERS[type] || DEFAULT_DESERIALIZER)
|
41
|
-
deserializer.call(value)
|
42
|
-
end
|
47
|
+
serializer.call(value)
|
48
|
+
end
|
43
49
|
|
50
|
+
def deserialize(type, value, deserializer=DESERIALIZERS[type])
|
51
|
+
return nil if value.nil?
|
52
|
+
|
53
|
+
deserializer.call(value)
|
54
|
+
end
|
55
|
+
end
|
44
56
|
end
|
45
57
|
end
|
@@ -7,30 +7,34 @@ FIELDS = {
|
|
7
7
|
published: { data_type: :boolean, store_key: "p" },
|
8
8
|
weight: { data_type: :float, store_key: "w" },
|
9
9
|
popular: :boolean,
|
10
|
-
build_timestamp: :
|
10
|
+
build_timestamp: :datetime,
|
11
11
|
tags: :array,
|
12
12
|
reviews: :hash,
|
13
13
|
released_at: :date,
|
14
14
|
miles: :decimal
|
15
15
|
}
|
16
16
|
|
17
|
+
DATA_FIELDS = {
|
18
|
+
color_data: :string
|
19
|
+
}
|
20
|
+
|
17
21
|
class Product < ActiveRecord::Base
|
18
22
|
hstore_accessor :options, FIELDS
|
23
|
+
hstore_accessor :data, DATA_FIELDS
|
19
24
|
end
|
20
25
|
|
21
|
-
|
26
|
+
class SuperProduct < Product
|
27
|
+
end
|
22
28
|
|
29
|
+
describe HstoreAccessor do
|
23
30
|
context "macro" do
|
24
|
-
|
25
31
|
let(:product) { Product.new }
|
26
32
|
|
27
33
|
FIELDS.keys.each do |field|
|
28
34
|
it "creates a getter for the hstore field: #{field}" do
|
29
35
|
expect(product).to respond_to(field)
|
30
36
|
end
|
31
|
-
end
|
32
37
|
|
33
|
-
FIELDS.keys.each do |field|
|
34
38
|
it "creates a setter for the hstore field: #{field}=" do
|
35
39
|
expect(product).to respond_to(:"#{field}=")
|
36
40
|
end
|
@@ -52,43 +56,42 @@ describe HstoreAccessor do
|
|
52
56
|
expect(product.options["w"]).to eq "38.5"
|
53
57
|
expect(product.weight).to eq 38.5
|
54
58
|
end
|
55
|
-
|
56
59
|
end
|
57
60
|
|
58
|
-
context "#
|
59
|
-
|
60
|
-
let(:product) { Product.new }
|
61
|
+
context "#hstore_metadata_for_*" do
|
62
|
+
let(:product) { Product }
|
61
63
|
|
62
64
|
it "returns the metadata hash for the specified field" do
|
63
65
|
expect(product.hstore_metadata_for_options).to eq FIELDS
|
66
|
+
expect(product.hstore_metadata_for_data).to eq DATA_FIELDS
|
64
67
|
end
|
65
68
|
|
69
|
+
context "instance method" do
|
70
|
+
subject { Product.new }
|
71
|
+
it { is_expected.to delegate_method(:hstore_metadata_for_options).to(:class) }
|
72
|
+
it { is_expected.to delegate_method(:hstore_metadata_for_data).to(:class) }
|
73
|
+
end
|
66
74
|
end
|
67
75
|
|
68
76
|
context "nil values" do
|
69
|
-
|
70
77
|
let!(:timestamp) { Time.now }
|
71
78
|
let!(:datestamp) { Date.today }
|
72
|
-
let
|
73
|
-
let
|
79
|
+
let(:product) { Product.new }
|
80
|
+
let(:persisted_product) { Product.create!(color: "green", price: 10, weight: 10.1, tags: %w(tag1 tag2 tag3), popular: true, build_timestamp: (timestamp - 10.days), released_at: (datestamp - 8.days), miles: BigDecimal.new("9.133790001")) }
|
74
81
|
|
75
82
|
FIELDS.keys.each do |field|
|
76
83
|
it "responds with nil when #{field} is not set" do
|
77
84
|
expect(product.send(field)).to be_nil
|
78
85
|
end
|
79
|
-
end
|
80
86
|
|
81
|
-
FIELDS.keys.each do |field|
|
82
87
|
it "responds with nil when #{field} is set back to nil after being set initially" do
|
83
|
-
|
84
|
-
expect(
|
88
|
+
persisted_product.send("#{field}=", nil)
|
89
|
+
expect(persisted_product.send(field)).to be_nil
|
85
90
|
end
|
86
91
|
end
|
87
|
-
|
88
92
|
end
|
89
93
|
|
90
94
|
describe "predicate methods" do
|
91
|
-
|
92
95
|
let!(:product) { Product.new }
|
93
96
|
|
94
97
|
it "exist for each field" do
|
@@ -105,7 +108,6 @@ describe HstoreAccessor do
|
|
105
108
|
end
|
106
109
|
|
107
110
|
context "boolean fields" do
|
108
|
-
|
109
111
|
it "return the state for true boolean fields" do
|
110
112
|
product.popular = true
|
111
113
|
product.save
|
@@ -140,27 +142,22 @@ describe HstoreAccessor do
|
|
140
142
|
expect(product.popular?).to be false
|
141
143
|
end
|
142
144
|
end
|
143
|
-
|
144
145
|
end
|
145
146
|
|
146
147
|
describe "scopes" do
|
147
|
-
|
148
148
|
let!(:timestamp) { Time.now }
|
149
149
|
let!(:datestamp) { Date.today }
|
150
|
-
let!(:product_a) { Product.create(color: "green",
|
151
|
-
let!(:product_b) { Product.create(color: "orange", price: 20, weight: 20.2, tags:
|
152
|
-
let!(:product_c) { Product.create(color: "blue",
|
150
|
+
let!(:product_a) { Product.create(color: "green", price: 10, weight: 10.1, tags: %w(tag1 tag2 tag3), popular: true, build_timestamp: (timestamp - 10.days), released_at: (datestamp - 8.days), miles: BigDecimal.new("10.113379001")) }
|
151
|
+
let!(:product_b) { Product.create(color: "orange", price: 20, weight: 20.2, tags: %w(tag2 tag3 tag4), popular: false, build_timestamp: (timestamp - 5.days), released_at: (datestamp - 4.days), miles: BigDecimal.new("20.213379001")) }
|
152
|
+
let!(:product_c) { Product.create(color: "blue", price: 30, weight: 30.3, tags: %w(tag3 tag4 tag5), popular: true, build_timestamp: timestamp, released_at: datestamp, miles: BigDecimal.new("30.313379001")) }
|
153
153
|
|
154
154
|
context "for string fields support" do
|
155
|
-
|
156
155
|
it "equality" do
|
157
156
|
expect(Product.with_color("orange").to_a).to eq [product_b]
|
158
157
|
end
|
159
|
-
|
160
158
|
end
|
161
159
|
|
162
160
|
context "for integer fields support" do
|
163
|
-
|
164
161
|
it "less than" do
|
165
162
|
expect(Product.price_lt(20).to_a).to eq [product_a]
|
166
163
|
end
|
@@ -180,11 +177,9 @@ describe HstoreAccessor do
|
|
180
177
|
it "greater than" do
|
181
178
|
expect(Product.price_gt(20).to_a).to eq [product_c]
|
182
179
|
end
|
183
|
-
|
184
180
|
end
|
185
181
|
|
186
182
|
context "for float fields support" do
|
187
|
-
|
188
183
|
it "less than" do
|
189
184
|
expect(Product.weight_lt(20.0).to_a).to eq [product_a]
|
190
185
|
end
|
@@ -204,50 +199,44 @@ describe HstoreAccessor do
|
|
204
199
|
it "greater than" do
|
205
200
|
expect(Product.weight_gt(20.5).to_a).to eq [product_c]
|
206
201
|
end
|
207
|
-
|
208
202
|
end
|
209
203
|
|
210
204
|
context "for decimal fields support" do
|
211
|
-
|
212
205
|
it "less than" do
|
213
|
-
expect(Product.miles_lt(BigDecimal.new(
|
206
|
+
expect(Product.miles_lt(BigDecimal.new("10.55555")).to_a).to eq [product_a]
|
214
207
|
end
|
215
208
|
|
216
209
|
it "less than or equal" do
|
217
|
-
expect(Product.miles_lte(BigDecimal.new(
|
210
|
+
expect(Product.miles_lte(BigDecimal.new("20.213379001")).to_a).to eq [product_a, product_b]
|
218
211
|
end
|
219
212
|
|
220
213
|
it "equality" do
|
221
|
-
expect(Product.miles_eq(BigDecimal.new(
|
214
|
+
expect(Product.miles_eq(BigDecimal.new("10.113379001")).to_a).to eq [product_a]
|
222
215
|
end
|
223
216
|
|
224
217
|
it "greater than or equal" do
|
225
|
-
expect(Product.miles_gte(BigDecimal.new(
|
218
|
+
expect(Product.miles_gte(BigDecimal.new("20.213379001")).to_a).to eq [product_b, product_c]
|
226
219
|
end
|
227
220
|
|
228
221
|
it "greater than" do
|
229
|
-
expect(Product.miles_gt(BigDecimal.new(
|
222
|
+
expect(Product.miles_gt(BigDecimal.new("20.555555")).to_a).to eq [product_c]
|
230
223
|
end
|
231
|
-
|
232
224
|
end
|
233
225
|
|
234
226
|
context "for array fields support" do
|
235
|
-
|
236
227
|
it "equality" do
|
237
|
-
expect(Product.tags_eq(
|
228
|
+
expect(Product.tags_eq(%w(tag1 tag2 tag3)).to_a).to eq [product_a]
|
238
229
|
end
|
239
230
|
|
240
231
|
it "contains" do
|
241
232
|
expect(Product.tags_contains("tag2").to_a).to eq [product_a, product_b]
|
242
|
-
expect(Product.tags_contains(
|
243
|
-
expect(Product.tags_contains(
|
244
|
-
expect(Product.tags_contains(
|
233
|
+
expect(Product.tags_contains(%w(tag2 tag3)).to_a).to eq [product_a, product_b]
|
234
|
+
expect(Product.tags_contains(%w(tag1 tag2 tag3)).to_a).to eq [product_a]
|
235
|
+
expect(Product.tags_contains(%w(tag1 tag2 tag3 tag4)).to_a).to eq []
|
245
236
|
end
|
246
|
-
|
247
237
|
end
|
248
238
|
|
249
239
|
context "for time fields support" do
|
250
|
-
|
251
240
|
it "before" do
|
252
241
|
expect(Product.build_timestamp_before(timestamp)).to eq [product_a, product_b]
|
253
242
|
end
|
@@ -259,11 +248,9 @@ describe HstoreAccessor do
|
|
259
248
|
it "after" do
|
260
249
|
expect(Product.build_timestamp_after(timestamp - 6.days)).to eq [product_b, product_c]
|
261
250
|
end
|
262
|
-
|
263
251
|
end
|
264
252
|
|
265
253
|
context "for date fields support" do
|
266
|
-
|
267
254
|
it "before" do
|
268
255
|
expect(Product.released_at_before(datestamp)).to eq [product_a, product_b]
|
269
256
|
end
|
@@ -275,11 +262,9 @@ describe HstoreAccessor do
|
|
275
262
|
it "after" do
|
276
263
|
expect(Product.released_at_after(datestamp - 6.days)).to eq [product_b, product_c]
|
277
264
|
end
|
278
|
-
|
279
265
|
end
|
280
266
|
|
281
267
|
context "for boolean field support" do
|
282
|
-
|
283
268
|
it "true" do
|
284
269
|
expect(Product.is_popular).to eq [product_a, product_c]
|
285
270
|
end
|
@@ -287,13 +272,81 @@ describe HstoreAccessor do
|
|
287
272
|
it "false" do
|
288
273
|
expect(Product.not_popular).to eq [product_b]
|
289
274
|
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
describe "#type_for_attribute" do
|
279
|
+
if ::ActiveRecord::VERSION::STRING.to_f >= 4.2
|
280
|
+
subject { SuperProduct }
|
290
281
|
|
282
|
+
def self.it_returns_the_type_for_the_attribute(type, attribute_name, active_record_type)
|
283
|
+
context "#{type}" do
|
284
|
+
it "returns the type for the column" do
|
285
|
+
expect(subject.type_for_attribute(attribute_name.to_s)).to eq(active_record_type.new)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
it_returns_the_type_for_the_attribute "default behavior", :string_type, ActiveRecord::Type::String
|
291
|
+
it_returns_the_type_for_the_attribute :string, :color, ActiveRecord::Type::String
|
292
|
+
it_returns_the_type_for_the_attribute :integer, :price, ActiveRecord::Type::Integer
|
293
|
+
it_returns_the_type_for_the_attribute :float, :weight, ActiveRecord::Type::Float
|
294
|
+
it_returns_the_type_for_the_attribute :datetime, :build_timestamp, ActiveRecord::Type::DateTime
|
295
|
+
it_returns_the_type_for_the_attribute :date, :released_at, ActiveRecord::Type::Date
|
296
|
+
it_returns_the_type_for_the_attribute :boolean, :published, ActiveRecord::Type::Boolean
|
297
|
+
else
|
298
|
+
subject { Product }
|
299
|
+
|
300
|
+
it "is not defined" do
|
301
|
+
expect(subject).to_not respond_to(:type_for_attribute)
|
302
|
+
end
|
291
303
|
end
|
304
|
+
end
|
305
|
+
|
306
|
+
describe "#column_for_attribute" do
|
307
|
+
if ActiveRecord::VERSION::STRING.to_f >= 4.2
|
308
|
+
|
309
|
+
def self.it_returns_the_properly_typed_column(type, attribute_name, cast_type_class)
|
310
|
+
context "#{type}" do
|
311
|
+
subject { SuperProduct.column_for_attribute(attribute_name) }
|
312
|
+
it "returns a column with a #{type} cast type" do
|
313
|
+
expect(subject).to be_a(ActiveRecord::ConnectionAdapters::Column)
|
314
|
+
expect(subject.cast_type).to eq(cast_type_class.new)
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
it_returns_the_properly_typed_column :string, :color, ActiveRecord::Type::String
|
320
|
+
it_returns_the_properly_typed_column :integer, :price, ActiveRecord::Type::Integer
|
321
|
+
it_returns_the_properly_typed_column :boolean, :published, ActiveRecord::Type::Boolean
|
322
|
+
it_returns_the_properly_typed_column :float, :weight, ActiveRecord::Type::Float
|
323
|
+
it_returns_the_properly_typed_column :datetime, :build_timestamp, ActiveRecord::Type::DateTime
|
324
|
+
it_returns_the_properly_typed_column :date, :released_at, ActiveRecord::Type::Date
|
325
|
+
it_returns_the_properly_typed_column :decimal, :miles, ActiveRecord::Type::Decimal
|
326
|
+
it_returns_the_properly_typed_column :array, :tags, ActiveRecord::Type::Value
|
327
|
+
it_returns_the_properly_typed_column :hash, :reviews, ActiveRecord::Type::Value
|
328
|
+
else
|
329
|
+
def self.it_returns_the_properly_typed_column(hstore_type, attribute_name, active_record_type)
|
330
|
+
context "#{hstore_type}" do
|
331
|
+
subject { SuperProduct.new.column_for_attribute(attribute_name) }
|
332
|
+
it "returns a column with a #{hstore_type} cast type" do
|
333
|
+
expect(subject).to be_a(ActiveRecord::ConnectionAdapters::Column)
|
334
|
+
expect(subject.type).to eq(active_record_type)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
292
338
|
|
339
|
+
it_returns_the_properly_typed_column :string, :color, :string
|
340
|
+
it_returns_the_properly_typed_column :integer, :price, :integer
|
341
|
+
it_returns_the_properly_typed_column :boolean, :published, :boolean
|
342
|
+
it_returns_the_properly_typed_column :float, :weight, :float
|
343
|
+
it_returns_the_properly_typed_column :time, :build_timestamp, :datetime
|
344
|
+
it_returns_the_properly_typed_column :date, :released_at, :date
|
345
|
+
it_returns_the_properly_typed_column :decimal, :miles, :decimal
|
346
|
+
end
|
293
347
|
end
|
294
348
|
|
295
349
|
context "when assigning values it" do
|
296
|
-
|
297
350
|
let(:product) { Product.new }
|
298
351
|
|
299
352
|
it "correctly stores string values" do
|
@@ -335,26 +388,80 @@ describe HstoreAccessor do
|
|
335
388
|
expect(product.weight).to eq 93.45
|
336
389
|
end
|
337
390
|
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
391
|
+
context "array values" do
|
392
|
+
it "correctly stores nothing" do
|
393
|
+
product.tags = nil
|
394
|
+
product.save
|
395
|
+
product.reload
|
396
|
+
expect(product.tags).to be_nil
|
397
|
+
end
|
398
|
+
|
399
|
+
it "correctly stores strings" do
|
400
|
+
product.tags = ["household", "living room", "kitchen"]
|
401
|
+
product.save
|
402
|
+
product.reload
|
403
|
+
expect(product.tags).to eq ["household", "living room", "kitchen"]
|
404
|
+
end
|
343
405
|
end
|
344
406
|
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
407
|
+
context "hash values" do
|
408
|
+
it "correctly stores nothing" do
|
409
|
+
product.reviews = nil
|
410
|
+
product.save
|
411
|
+
product.reload
|
412
|
+
expect(product.reviews).to be_nil
|
413
|
+
end
|
414
|
+
|
415
|
+
it "correctly stores hash values as json" do
|
416
|
+
hash = product.reviews = { "user_123" => "4 stars", "user_994" => "3 stars" }
|
417
|
+
product.save
|
418
|
+
product.reload
|
419
|
+
expect(product.reviews).to eq("user_123" => "4 stars", "user_994" => "3 stars")
|
420
|
+
expect(product.options["reviews"]).to eq(hash.to_json)
|
421
|
+
end
|
350
422
|
end
|
351
423
|
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
424
|
+
context "multipart values" do
|
425
|
+
it "stores multipart dates correctly" do
|
426
|
+
product.update_attributes!(
|
427
|
+
"released_at(1i)" => "2014",
|
428
|
+
"released_at(2i)" => "04",
|
429
|
+
"released_at(3i)" => "14"
|
430
|
+
)
|
431
|
+
product.reload
|
432
|
+
expect(product.released_at).to eq(Date.new(2014, 4, 14))
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
context "time values" do
|
437
|
+
let(:chicago_timezone) { ActiveSupport::TimeZone["America/Chicago"] }
|
438
|
+
let(:new_york_timezone) { ActiveSupport::TimeZone["America/New_York"] }
|
439
|
+
|
440
|
+
it "correctly stores value" do
|
441
|
+
timestamp = Time.now - 10.days
|
442
|
+
product.build_timestamp = timestamp
|
443
|
+
product.save!
|
444
|
+
product.reload
|
445
|
+
expect(product.build_timestamp.to_i).to eq timestamp.to_i
|
446
|
+
end
|
447
|
+
|
448
|
+
it "stores the value in utc" do
|
449
|
+
timestamp = Time.now.in_time_zone(chicago_timezone) - 10.days
|
450
|
+
product.build_timestamp = timestamp
|
451
|
+
product.save!
|
452
|
+
product.reload
|
453
|
+
expect(product.options["build_timestamp"].to_i).to eq timestamp.utc.to_i
|
454
|
+
end
|
455
|
+
|
456
|
+
it "returns the time value in the current time zone" do
|
457
|
+
timestamp = Time.now.in_time_zone(chicago_timezone) - 10.days
|
458
|
+
product.build_timestamp = timestamp
|
459
|
+
product.save!
|
460
|
+
product.reload
|
461
|
+
Time.use_zone(new_york_timezone) do
|
462
|
+
expect(product.build_timestamp.utc_offset).to eq new_york_timezone.utc_offset
|
463
|
+
end
|
464
|
+
end
|
358
465
|
end
|
359
466
|
|
360
467
|
it "correctly stores date values" do
|
@@ -367,7 +474,7 @@ describe HstoreAccessor do
|
|
367
474
|
end
|
368
475
|
|
369
476
|
it "correctly stores decimal values" do
|
370
|
-
decimal = BigDecimal.new(
|
477
|
+
decimal = BigDecimal.new("9.13370009001")
|
371
478
|
product.miles = decimal
|
372
479
|
product.save
|
373
480
|
product.reload
|
@@ -376,9 +483,8 @@ describe HstoreAccessor do
|
|
376
483
|
end
|
377
484
|
|
378
485
|
context "correctly stores boolean values" do
|
379
|
-
|
380
486
|
it "when string 'true' is passed" do
|
381
|
-
product.popular =
|
487
|
+
product.popular = "true"
|
382
488
|
product.save
|
383
489
|
product.reload
|
384
490
|
expect(product.popular).to be true
|
@@ -390,7 +496,6 @@ describe HstoreAccessor do
|
|
390
496
|
product.reload
|
391
497
|
expect(product.popular).to be true
|
392
498
|
end
|
393
|
-
|
394
499
|
end
|
395
500
|
|
396
501
|
it "setters call the _will_change! method of the store attribute" do
|
@@ -400,26 +505,30 @@ describe HstoreAccessor do
|
|
400
505
|
|
401
506
|
describe "type casting" do
|
402
507
|
it "type casts integer values" do
|
403
|
-
product.price =
|
508
|
+
product.price = "468"
|
404
509
|
expect(product.price).to eq 468
|
405
510
|
end
|
511
|
+
|
406
512
|
it "type casts float values" do
|
407
|
-
product.weight =
|
513
|
+
product.weight = "93.45"
|
408
514
|
expect(product.weight).to eq 93.45
|
409
515
|
end
|
516
|
+
|
410
517
|
it "type casts time values" do
|
411
518
|
timestamp = Time.now - 10.days
|
412
519
|
product.build_timestamp = timestamp.to_s
|
413
520
|
expect(product.build_timestamp.to_i).to eq timestamp.to_i
|
414
521
|
end
|
522
|
+
|
415
523
|
it "type casts date values" do
|
416
524
|
datestamp = Date.today - 9.days
|
417
525
|
product.released_at = datestamp.to_s
|
418
526
|
expect(product.released_at).to eq datestamp
|
419
527
|
end
|
528
|
+
|
420
529
|
it "type casts decimal values" do
|
421
|
-
product.miles =
|
422
|
-
expect(product.miles).to eq BigDecimal.new(
|
530
|
+
product.miles = "1.337900129339202"
|
531
|
+
expect(product.miles).to eq BigDecimal.new("1.337900129339202")
|
423
532
|
end
|
424
533
|
|
425
534
|
it "type casts boolean values" do
|
@@ -430,6 +539,7 @@ describe HstoreAccessor do
|
|
430
539
|
product.published = value
|
431
540
|
expect(product.published).to be true
|
432
541
|
end
|
542
|
+
|
433
543
|
ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.each do |value|
|
434
544
|
product.popular = value
|
435
545
|
expect(product.popular).to be false
|
@@ -480,12 +590,12 @@ describe HstoreAccessor do
|
|
480
590
|
end
|
481
591
|
|
482
592
|
describe "dirty tracking" do
|
483
|
-
|
484
593
|
let(:product) { Product.new }
|
485
594
|
|
486
595
|
it "<attr>_changed? should return the expected value" do
|
487
596
|
expect(product.color_changed?).to be false
|
488
597
|
product.color = "ORANGE"
|
598
|
+
expect(product.price_changed?).to be false
|
489
599
|
expect(product.color_changed?).to be true
|
490
600
|
product.save
|
491
601
|
expect(product.color_changed?).to be false
|
@@ -502,20 +612,106 @@ describe HstoreAccessor do
|
|
502
612
|
expect(product.price_changed?).to be false
|
503
613
|
end
|
504
614
|
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
615
|
+
describe "#<attr>_will_change!" do
|
616
|
+
it "tells ActiveRecord the hstore attribute has changed" do
|
617
|
+
expect(product).to receive(:options_will_change!)
|
618
|
+
product.color_will_change!
|
619
|
+
end
|
510
620
|
end
|
511
621
|
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
622
|
+
describe "#<attr>_was" do
|
623
|
+
it "returns the expected value" do
|
624
|
+
product.color = "ORANGE"
|
625
|
+
product.save
|
626
|
+
product.color = "GREEN"
|
627
|
+
expect(product.color_was).to eq "ORANGE"
|
628
|
+
end
|
629
|
+
|
630
|
+
it "works when the hstore attribute is nil" do
|
631
|
+
product.options = nil
|
632
|
+
product.save
|
633
|
+
product.color = "green"
|
634
|
+
expect { product.color_was }.to_not raise_error
|
635
|
+
end
|
517
636
|
end
|
518
637
|
|
519
|
-
|
638
|
+
describe "#<attr>_change" do
|
639
|
+
it "returns the old and new values" do
|
640
|
+
product.color = "ORANGE"
|
641
|
+
product.save
|
642
|
+
product.color = "GREEN"
|
643
|
+
expect(product.color_change).to eq %w(ORANGE GREEN)
|
644
|
+
end
|
645
|
+
context "when store_key differs from key" do
|
646
|
+
it "returns the old and new values" do
|
647
|
+
product.weight = 100.01
|
648
|
+
expect(product.weight_change[1]).to eq "100.01"
|
649
|
+
end
|
650
|
+
end
|
651
|
+
context "hstore attribute was nil" do
|
652
|
+
it "returns old and new values" do
|
653
|
+
product.options = nil
|
654
|
+
product.save!
|
655
|
+
green = product.color = "green"
|
656
|
+
expect(product.color_change).to eq([nil, green])
|
657
|
+
end
|
658
|
+
end
|
659
|
+
|
660
|
+
context "other hstore attributes were persisted" do
|
661
|
+
it "returns nil" do
|
662
|
+
product.price = 5
|
663
|
+
product.save!
|
664
|
+
product.price = 6
|
665
|
+
expect(product.color_change).to be_nil
|
666
|
+
end
|
667
|
+
end
|
668
|
+
|
669
|
+
context "not persisted" do
|
670
|
+
it "returns nil when there are no changes" do
|
671
|
+
expect(product.color_change).to be_nil
|
672
|
+
end
|
673
|
+
end
|
674
|
+
end
|
520
675
|
|
676
|
+
describe "#reset_<attr>!" do
|
677
|
+
before do
|
678
|
+
allow(ActiveSupport::Deprecation).to receive(:warn)
|
679
|
+
end
|
680
|
+
|
681
|
+
if ActiveRecord::VERSION::STRING.to_f >= 4.2
|
682
|
+
it "displays a deprecation warning" do
|
683
|
+
expect(ActiveSupport::Deprecation).to receive(:warn)
|
684
|
+
product.reset_color!
|
685
|
+
end
|
686
|
+
else
|
687
|
+
it "does not display a deprecation warning" do
|
688
|
+
expect(ActiveSupport::Deprecation).to_not receive(:warn)
|
689
|
+
product.reset_color!
|
690
|
+
end
|
691
|
+
end
|
692
|
+
|
693
|
+
it "restores the attribute" do
|
694
|
+
expect(product).to receive(:restore_color!)
|
695
|
+
product.reset_color!
|
696
|
+
end
|
697
|
+
end
|
698
|
+
|
699
|
+
describe "#restore_<attr>!" do
|
700
|
+
it "restores the attribute" do
|
701
|
+
product.color = "red"
|
702
|
+
product.restore_color!
|
703
|
+
expect(product.color).to be_nil
|
704
|
+
end
|
705
|
+
|
706
|
+
context "persisted" do
|
707
|
+
it "restores the attribute" do
|
708
|
+
green = product.color = "green"
|
709
|
+
product.save!
|
710
|
+
product.color = "red"
|
711
|
+
product.restore_color!
|
712
|
+
expect(product.color).to eq(green)
|
713
|
+
end
|
714
|
+
end
|
715
|
+
end
|
716
|
+
end
|
521
717
|
end
|