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