torque-postgresql 1.1.8 → 2.0.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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/lib/torque/postgresql.rb +0 -2
  3. data/lib/torque/postgresql/adapter.rb +0 -1
  4. data/lib/torque/postgresql/adapter/database_statements.rb +4 -15
  5. data/lib/torque/postgresql/adapter/schema_creation.rb +13 -23
  6. data/lib/torque/postgresql/adapter/schema_definitions.rb +7 -21
  7. data/lib/torque/postgresql/adapter/schema_dumper.rb +71 -11
  8. data/lib/torque/postgresql/adapter/schema_statements.rb +2 -12
  9. data/lib/torque/postgresql/associations.rb +0 -3
  10. data/lib/torque/postgresql/associations/association.rb +0 -4
  11. data/lib/torque/postgresql/associations/association_scope.rb +18 -60
  12. data/lib/torque/postgresql/associations/belongs_to_many_association.rb +12 -15
  13. data/lib/torque/postgresql/associations/preloader.rb +0 -32
  14. data/lib/torque/postgresql/associations/preloader/association.rb +13 -10
  15. data/lib/torque/postgresql/autosave_association.rb +4 -4
  16. data/lib/torque/postgresql/auxiliary_statement.rb +1 -13
  17. data/lib/torque/postgresql/coder.rb +1 -2
  18. data/lib/torque/postgresql/config.rb +0 -6
  19. data/lib/torque/postgresql/inheritance.rb +13 -17
  20. data/lib/torque/postgresql/reflection/abstract_reflection.rb +19 -25
  21. data/lib/torque/postgresql/reflection/belongs_to_many_reflection.rb +4 -38
  22. data/lib/torque/postgresql/relation.rb +11 -16
  23. data/lib/torque/postgresql/relation/auxiliary_statement.rb +2 -8
  24. data/lib/torque/postgresql/relation/distinct_on.rb +1 -1
  25. data/lib/torque/postgresql/version.rb +1 -1
  26. data/spec/en.yml +19 -0
  27. data/spec/factories/authors.rb +6 -0
  28. data/spec/factories/comments.rb +13 -0
  29. data/spec/factories/posts.rb +6 -0
  30. data/spec/factories/tags.rb +5 -0
  31. data/spec/factories/texts.rb +5 -0
  32. data/spec/factories/users.rb +6 -0
  33. data/spec/factories/videos.rb +5 -0
  34. data/spec/mocks/cache_query.rb +16 -0
  35. data/spec/mocks/create_table.rb +35 -0
  36. data/spec/models/activity.rb +3 -0
  37. data/spec/models/activity_book.rb +4 -0
  38. data/spec/models/activity_post.rb +7 -0
  39. data/spec/models/activity_post/sample.rb +4 -0
  40. data/spec/models/author.rb +4 -0
  41. data/spec/models/author_journalist.rb +4 -0
  42. data/spec/models/comment.rb +3 -0
  43. data/spec/models/course.rb +2 -0
  44. data/spec/models/geometry.rb +2 -0
  45. data/spec/models/guest_comment.rb +4 -0
  46. data/spec/models/post.rb +6 -0
  47. data/spec/models/tag.rb +2 -0
  48. data/spec/models/text.rb +2 -0
  49. data/spec/models/time_keeper.rb +2 -0
  50. data/spec/models/user.rb +8 -0
  51. data/spec/models/video.rb +2 -0
  52. data/spec/schema.rb +141 -0
  53. data/spec/spec_helper.rb +59 -0
  54. data/spec/tests/arel_spec.rb +72 -0
  55. data/spec/tests/auxiliary_statement_spec.rb +593 -0
  56. data/spec/tests/belongs_to_many_spec.rb +240 -0
  57. data/spec/tests/coder_spec.rb +367 -0
  58. data/spec/tests/collector_spec.rb +59 -0
  59. data/spec/tests/distinct_on_spec.rb +65 -0
  60. data/spec/tests/enum_set_spec.rb +306 -0
  61. data/spec/tests/enum_spec.rb +621 -0
  62. data/spec/tests/geometric_builder_spec.rb +221 -0
  63. data/spec/tests/has_many_spec.rb +390 -0
  64. data/spec/tests/interval_spec.rb +167 -0
  65. data/spec/tests/lazy_spec.rb +24 -0
  66. data/spec/tests/period_spec.rb +954 -0
  67. data/spec/tests/quoting_spec.rb +24 -0
  68. data/spec/tests/range_spec.rb +36 -0
  69. data/spec/tests/relation_spec.rb +57 -0
  70. data/spec/tests/table_inheritance_spec.rb +403 -0
  71. metadata +103 -15
  72. data/lib/torque/postgresql/associations/join_dependency/join_association.rb +0 -15
  73. data/lib/torque/postgresql/schema_dumper.rb +0 -101
@@ -0,0 +1,59 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe 'Data collector', type: :helper do
4
+ let(:methods_list) { [:foo, :bar] }
5
+ subject { Torque::PostgreSQL::Collector.new(*methods_list) }
6
+
7
+ it 'is a class creator' do
8
+ expect(subject).to be_a(Class)
9
+ end
10
+
11
+ it 'has the requested methods' do
12
+ instance = subject.new
13
+ methods_list.each do |name|
14
+ expect(instance).to respond_to(name)
15
+ expect(instance).to respond_to("#{name}=")
16
+ end
17
+ end
18
+
19
+ it 'instace values starts as nil' do
20
+ instance = subject.new
21
+ methods_list.each do |name|
22
+ expect(instance.send(name)).to be_nil
23
+ end
24
+ end
25
+
26
+ it 'set values on the same method' do
27
+ instance = subject.new
28
+ methods_list.each do |name|
29
+ expect(instance.send(name, name)).to eql(name)
30
+ end
31
+ end
32
+
33
+ it 'get value on the same method' do
34
+ instance = subject.new
35
+ methods_list.each do |name|
36
+ instance.send(name, name)
37
+ expect(instance.send(name)).to eql(name)
38
+ end
39
+ end
40
+
41
+ it 'accepts any kind of value' do
42
+ instance = subject.new
43
+
44
+ instance.foo 123
45
+ expect(instance.foo).to eql(123)
46
+
47
+ instance.foo 'chars'
48
+ expect(instance.foo).to eql('chars')
49
+
50
+ instance.foo :test, :test
51
+ expect(instance.foo).to eql([:test, :test])
52
+
53
+ instance.foo test: :test
54
+ expect(instance.foo).to eql({test: :test})
55
+
56
+ instance.foo nil
57
+ expect(instance.foo).to be_nil
58
+ end
59
+ end
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe 'DistinctOn' do
4
+
5
+ context 'on relation' do
6
+ subject { Post.unscoped }
7
+
8
+ it 'has its method' do
9
+ expect(subject).to respond_to(:distinct_on)
10
+ end
11
+
12
+ it 'does not mess with original distinct form without select' do
13
+ expect(subject.distinct.to_sql).to \
14
+ eql('SELECT DISTINCT "posts".* FROM "posts"')
15
+ end
16
+
17
+ it 'does not mess with original distinct form with select' do
18
+ expect(subject.select(:name).distinct.to_sql).to \
19
+ eql('SELECT DISTINCT "name" FROM "posts"')
20
+ end
21
+
22
+ it 'is able to do the basic form' do
23
+ expect(subject.distinct_on(:title).to_sql).to \
24
+ eql('SELECT DISTINCT ON ( "posts"."title" ) "posts".* FROM "posts"')
25
+ end
26
+
27
+ it 'is able to do with multiple attributes' do
28
+ expect(subject.distinct_on(:title, :content).to_sql).to \
29
+ eql('SELECT DISTINCT ON ( "posts"."title", "posts"."content" ) "posts".* FROM "posts"')
30
+ end
31
+
32
+ it 'is able to do with relation' do
33
+ expect(subject.distinct_on(author: :name).to_sql).to \
34
+ eql('SELECT DISTINCT ON ( "authors"."name" ) "posts".* FROM "posts"')
35
+ end
36
+
37
+ it 'is able to do with relation and multiple attributes' do
38
+ expect(subject.distinct_on(author: [:name, :age]).to_sql).to \
39
+ eql('SELECT DISTINCT ON ( "authors"."name", "authors"."age" ) "posts".* FROM "posts"')
40
+ end
41
+
42
+ it 'raises with invalid relation' do
43
+ expect { subject.distinct_on(tags: :name).to_sql }.to \
44
+ raise_error(ArgumentError, /Relation for/)
45
+ end
46
+
47
+ it 'raises with third level hash' do
48
+ expect { subject.distinct_on(author: [comments: :body]).to_sql }.to \
49
+ raise_error(ArgumentError, /on third level/)
50
+ end
51
+ end
52
+
53
+ context 'on model' do
54
+ subject { Post }
55
+
56
+ it 'has its method' do
57
+ expect(subject).to respond_to(:distinct_on)
58
+ end
59
+
60
+ it 'returns a relation when using the method' do
61
+ expect(subject.distinct_on(:title)).to be_a(ActiveRecord::Relation)
62
+ end
63
+ end
64
+
65
+ end
@@ -0,0 +1,306 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe 'Enum' do
4
+ let(:connection) { ActiveRecord::Base.connection }
5
+ let(:attribute_klass) { Torque::PostgreSQL::Attributes::EnumSet }
6
+
7
+ def decorate(model, field, options = {})
8
+ attribute_klass.include_on(model, :enum_set)
9
+ model.enum_set(field, **options)
10
+ end
11
+
12
+ before :each do
13
+ Torque::PostgreSQL.config.enum.set_method = :pg_set_enum
14
+ Torque::PostgreSQL::Attributes::EnumSet.include_on(ActiveRecord::Base)
15
+
16
+ # Define a method to find yet to define constants
17
+ Torque::PostgreSQL.config.enum.namespace.define_singleton_method(:const_missing) do |name|
18
+ Torque::PostgreSQL::Attributes::EnumSet.lookup(name)
19
+ end
20
+
21
+ # Define a helper method to get a sample value
22
+ Torque::PostgreSQL.config.enum.namespace.define_singleton_method(:sample) do |name|
23
+ Torque::PostgreSQL::Attributes::EnumSet.lookup(name).sample
24
+ end
25
+ end
26
+
27
+ context 'on table definition' do
28
+ subject { ActiveRecord::ConnectionAdapters::PostgreSQL::TableDefinition.new('articles') }
29
+
30
+ it 'can be defined as an array' do
31
+ subject.enum(:content_status, array: true)
32
+ expect(subject['content_status'].name).to be_eql('content_status')
33
+ expect(subject['content_status'].type).to be_eql(:content_status)
34
+
35
+ array = subject['content_status'].respond_to?(:options) \
36
+ ? subject['content_status'].options[:array] \
37
+ : subject['content_status'].array
38
+
39
+ expect(array).to be_eql(true)
40
+ end
41
+ end
42
+
43
+ context 'on schema' do
44
+ it 'can be used on tables' do
45
+ dump_io = StringIO.new
46
+ checker = /t\.enum +"conflicts", +array: true, +subtype: :conflicts/
47
+ ActiveRecord::SchemaDumper.dump(connection, dump_io)
48
+ expect(dump_io.string).to match checker
49
+ end
50
+
51
+ it 'can have a default value as an array of symbols' do
52
+ dump_io = StringIO.new
53
+ checker = /t\.enum +"types", +default: \[:A, :B\], +array: true, +subtype: :types/
54
+ ActiveRecord::SchemaDumper.dump(connection, dump_io)
55
+ expect(dump_io.string).to match checker
56
+ end
57
+ end
58
+
59
+ context 'on value' do
60
+ subject { Enum::TypesSet }
61
+ let(:values) { %w(A B C D) }
62
+ let(:error) { Torque::PostgreSQL::Attributes::EnumSet::EnumSetError }
63
+ let(:mock_enum) do
64
+ enum_klass = Class.new(subject::EnumSource.superclass)
65
+ enum_klass.instance_variable_set(:@values, values << '15')
66
+
67
+ klass = Class.new(subject.superclass)
68
+ klass.const_set('EnumSource', enum_klass)
69
+ klass
70
+ end
71
+
72
+ it 'class exists' do
73
+ namespace = Torque::PostgreSQL.config.enum.namespace
74
+ expect(namespace.const_defined?('TypesSet')).to be_truthy
75
+ expect(subject.const_defined?('EnumSource')).to be_truthy
76
+ expect(subject < Torque::PostgreSQL::Attributes::EnumSet).to be_truthy
77
+ end
78
+
79
+ it 'returns the db type name' do
80
+ expect(subject.type_name).to be_eql('types[]')
81
+ end
82
+
83
+ it 'values match database values' do
84
+ expect(subject.values).to be_eql(values)
85
+ end
86
+
87
+ it 'values can be reach using fetch, as in hash enums' do
88
+ expect(subject).to respond_to(:fetch)
89
+
90
+ value = subject.fetch('A', 'A')
91
+ expect(value).to be_a(subject)
92
+ expect(value).to be_eql(subject.A)
93
+
94
+ value = subject.fetch('other', 'other')
95
+ expect(value).to be_nil
96
+ end
97
+
98
+ it 'values can be reach using [], as in hash enums' do
99
+ expect(subject).to respond_to(:[])
100
+
101
+ value = subject['A']
102
+ expect(value).to be_a(subject)
103
+ expect(value).to be_eql(subject.A)
104
+
105
+ value = subject['other']
106
+ expect(value).to be_nil
107
+ end
108
+
109
+ it 'accepts respond_to against value' do
110
+ expect(subject).to respond_to(:A)
111
+ end
112
+
113
+ it 'allows fast creation of values' do
114
+ value = subject.A
115
+ expect(value).to be_a(subject)
116
+ end
117
+
118
+ it 'keeps blank values as Lazy' do
119
+ expect(subject.new(nil)).to be_nil
120
+ expect(subject.new([])).to be_blank
121
+ end
122
+
123
+ it 'can start from nil value using lazy' do
124
+ lazy = Torque::PostgreSQL::Attributes::Lazy
125
+ value = subject.new(nil)
126
+
127
+ expect(value.__class__).to be_eql(lazy)
128
+ expect(value.to_s).to be_eql('')
129
+ expect(value.to_i).to be_nil
130
+
131
+ expect(value.A?).to be_falsey
132
+ end
133
+
134
+ it 'accepts values to come from numeric as power' do
135
+ expect(subject.new(0)).to be_blank
136
+ expect(subject.new(1)).to be_eql(subject.A)
137
+ expect(subject.new(3)).to be_eql(subject.A | subject.B)
138
+ expect { subject.new(16) }.to raise_error(error, /out of bounds/)
139
+ end
140
+
141
+ it 'accepts values to come from numeric list' do
142
+ expect(subject.new([0])).to be_eql(subject.A)
143
+ expect(subject.new([0, 1])).to be_eql(subject.A | subject.B)
144
+ expect { subject.new([4]) }.to raise_error(error.superclass, /out of bounds/)
145
+ end
146
+
147
+ it 'accepts string initialization' do
148
+ expect(subject.new('A')).to be_eql(subject.A)
149
+ expect { subject.new('E') }.to raise_error(error.superclass, /not valid for/)
150
+ end
151
+
152
+ it 'allows values bitwise operations' do
153
+ expect((subject.A | subject.B).to_i).to be_eql(3)
154
+ expect((subject.A & subject.B).to_i).to be_nil
155
+ expect(((subject.A | subject.B) & subject.B).to_i).to be_eql(2)
156
+ end
157
+
158
+ it 'allows values comparison' do
159
+ value = subject.B | subject.C
160
+ expect(value).to be > subject.A
161
+ expect(value).to be < subject.D
162
+ expect(value).to be_eql(6)
163
+ expect(value).to_not be_eql(1)
164
+ expect(subject.A == mock_enum.A).to be_falsey
165
+ end
166
+
167
+ it 'accepts value checking' do
168
+ value = subject.B | subject.C
169
+ expect(value).to respond_to(:B?)
170
+ expect(value.B?).to be_truthy
171
+ expect(value.C?).to be_truthy
172
+ expect(value.A?).to be_falsey
173
+ expect(value.D?).to be_falsey
174
+ end
175
+
176
+ it 'accepts replace and bang value' do
177
+ value = subject.B | subject.C
178
+ expect(value).to respond_to(:B!)
179
+ expect(value.A!).to be_eql(7)
180
+ expect(value.replace(:D)).to be_eql(subject.D)
181
+ end
182
+
183
+ it 'accepts values turn into integer by its power' do
184
+ expect(subject.B.to_i).to be_eql(2)
185
+ expect(subject.C.to_i).to be_eql(4)
186
+ end
187
+
188
+ it 'accepts values turn into an array of integer by index' do
189
+ expect((subject.B | subject.C).map(&:to_i)).to be_eql([1, 2])
190
+ end
191
+
192
+ it 'can return a sample for resting purposes' do
193
+ expect(subject).to receive(:new).with(Numeric)
194
+ subject.sample
195
+ end
196
+ end
197
+
198
+ context 'on OID' do
199
+ let(:enum) { Enum::TypesSet }
200
+ let(:enum_source) { enum::EnumSource }
201
+ subject { Torque::PostgreSQL::Adapter::OID::EnumSet.new('types', enum_source) }
202
+
203
+ context 'on deserialize' do
204
+ it 'returns nil' do
205
+ expect(subject.deserialize(nil)).to be_nil
206
+ end
207
+
208
+ it 'returns enum' do
209
+ value = subject.deserialize('{B,C}')
210
+ expect(value).to be_a(enum)
211
+ expect(value).to be_eql(enum.B | enum.C)
212
+ end
213
+ end
214
+
215
+ context 'on serialize' do
216
+ it 'returns nil' do
217
+ expect(subject.serialize(nil)).to be_nil
218
+ expect(subject.serialize(0)).to be_nil
219
+ end
220
+
221
+ it 'returns as string' do
222
+ expect(subject.serialize(enum.B | enum.C)).to be_eql('{B,C}')
223
+ expect(subject.serialize(3)).to be_eql('{A,B}')
224
+ end
225
+ end
226
+
227
+ context 'on cast' do
228
+ it 'accepts nil' do
229
+ expect(subject.cast(nil)).to be_nil
230
+ end
231
+
232
+ it 'accepts invalid values as nil' do
233
+ expect(subject.cast([])).to be_nil
234
+ end
235
+
236
+ it 'accepts array of strings' do
237
+ value = subject.cast(['A'])
238
+ expect(value).to be_a(enum)
239
+ expect(value).to be_eql(enum.A)
240
+ end
241
+
242
+ it 'accepts array of numbers' do
243
+ value = subject.cast([1])
244
+ expect(value).to be_a(enum)
245
+ expect(value).to be_eql(enum.B)
246
+ end
247
+ end
248
+ end
249
+
250
+ context 'on I18n' do
251
+ subject { Enum::TypesSet }
252
+
253
+ it 'has the text method' do
254
+ expect(subject.new(0)).to respond_to(:text)
255
+ end
256
+
257
+ it 'brings the correct values' do
258
+ expect(subject.new(0).text).to be_eql('')
259
+ expect(subject.new(1).text).to be_eql('A')
260
+ expect(subject.new(2).text).to be_eql('B')
261
+ expect(subject.new(3).text).to be_eql('A and B')
262
+ expect(subject.new(7).text).to be_eql('A, B, and C')
263
+ end
264
+ end
265
+
266
+ context 'on model' do
267
+ before(:each) { decorate(Course, :types) }
268
+
269
+ subject { Course }
270
+ let(:instance) { Course.new }
271
+
272
+ it 'has all enum set methods' do
273
+ expect(subject).to respond_to(:types)
274
+ expect(subject).to respond_to(:types_keys)
275
+ expect(subject).to respond_to(:types_texts)
276
+ expect(subject).to respond_to(:types_options)
277
+
278
+ expect(subject).to respond_to(:has_types)
279
+ expect(subject).to respond_to(:has_any_types)
280
+
281
+ expect(instance).to respond_to(:types_text)
282
+
283
+ subject.types.each do |value|
284
+ value = value.underscore
285
+ expect(subject).to respond_to(value)
286
+ expect(instance).to respond_to(value + '?')
287
+ expect(instance).to respond_to(value + '!')
288
+ end
289
+ end
290
+
291
+ it 'scope the model correctly' do
292
+ query = subject.a.to_sql
293
+ expect(query).to match(/"courses"."types" @> ARRAY\['A'\]::types\[\]/)
294
+ end
295
+
296
+ it 'has a match all scope' do
297
+ query = subject.has_types('B', 'A').to_sql
298
+ expect(query).to match(/"courses"."types" @> ARRAY\['B', 'A'\]::types\[\]/)
299
+ end
300
+
301
+ it 'has a match any scope' do
302
+ query = subject.has_any_types('B', 'A').to_sql
303
+ expect(query).to match(/"courses"."types" && ARRAY\['B', 'A'\]::types\[\]/)
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,621 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe 'Enum' do
4
+ let(:connection) { ActiveRecord::Base.connection }
5
+ let(:attribute_klass) { Torque::PostgreSQL::Attributes::Enum }
6
+
7
+ def decorate(model, field, options = {})
8
+ attribute_klass.include_on(model, :pg_enum)
9
+ model.pg_enum(field, **options)
10
+ end
11
+
12
+ before :each do
13
+ Torque::PostgreSQL.config.enum.base_method = :pg_enum
14
+ Torque::PostgreSQL::Attributes::Enum.include_on(ActiveRecord::Base)
15
+
16
+ # Define a method to find yet to define constants
17
+ Torque::PostgreSQL.config.enum.namespace.define_singleton_method(:const_missing) do |name|
18
+ Torque::PostgreSQL::Attributes::Enum.lookup(name)
19
+ end
20
+
21
+ # Define a helper method to get a sample value
22
+ Torque::PostgreSQL.config.enum.namespace.define_singleton_method(:sample) do |name|
23
+ Torque::PostgreSQL::Attributes::Enum.lookup(name).sample
24
+ end
25
+ end
26
+
27
+ context 'on migration' do
28
+ it 'can be created' do
29
+ connection.create_enum(:status, %i(foo bar))
30
+ expect(connection.type_exists?(:status)).to be_truthy
31
+ expect(connection.enum_values(:status)).to be_eql(['foo', 'bar'])
32
+ end
33
+
34
+ it 'can be deleted' do
35
+ connection.create_enum(:status, %i(foo bar))
36
+ expect(connection.type_exists?(:status)).to be_truthy
37
+
38
+ connection.drop_type(:status)
39
+ expect(connection.type_exists?(:status)).to be_falsey
40
+ end
41
+
42
+ it 'can be renamed' do
43
+ connection.rename_type(:content_status, :status)
44
+ expect(connection.type_exists?(:content_status)).to be_falsey
45
+ expect(connection.type_exists?(:status)).to be_truthy
46
+ end
47
+
48
+ it 'can have prefix' do
49
+ connection.create_enum(:status, %i(foo bar), prefix: true)
50
+ expect(connection.enum_values(:status)).to be_eql(['status_foo', 'status_bar'])
51
+ end
52
+
53
+ it 'can have suffix' do
54
+ connection.create_enum(:status, %i(foo bar), suffix: 'tst')
55
+ expect(connection.enum_values(:status)).to be_eql(['foo_tst', 'bar_tst'])
56
+ end
57
+
58
+ it 'inserts values at the end' do
59
+ connection.create_enum(:status, %i(foo bar))
60
+ connection.add_enum_values(:status, %i(baz qux))
61
+ expect(connection.enum_values(:status)).to be_eql(['foo', 'bar', 'baz', 'qux'])
62
+ end
63
+
64
+ it 'inserts values in the beginning' do
65
+ connection.create_enum(:status, %i(foo bar))
66
+ connection.add_enum_values(:status, %i(baz qux), prepend: true)
67
+ expect(connection.enum_values(:status)).to be_eql(['baz', 'qux', 'foo', 'bar'])
68
+ end
69
+
70
+ it 'inserts values in the middle' do
71
+ connection.create_enum(:status, %i(foo bar))
72
+ connection.add_enum_values(:status, %i(baz), after: 'foo')
73
+ expect(connection.enum_values(:status)).to be_eql(['foo', 'baz', 'bar'])
74
+
75
+ connection.add_enum_values(:status, %i(qux), before: 'bar')
76
+ expect(connection.enum_values(:status)).to be_eql(['foo', 'baz', 'qux', 'bar'])
77
+ end
78
+
79
+ it 'inserts values with prefix or suffix' do
80
+ connection.create_enum(:status, %i(foo bar))
81
+ connection.add_enum_values(:status, %i(baz), prefix: true)
82
+ connection.add_enum_values(:status, %i(qux), suffix: 'tst')
83
+ expect(connection.enum_values(:status)).to be_eql(['foo', 'bar', 'status_baz', 'qux_tst'])
84
+ end
85
+ end
86
+
87
+ context 'on table definition' do
88
+ subject { ActiveRecord::ConnectionAdapters::PostgreSQL::TableDefinition.new('articles') }
89
+
90
+ it 'has the enum method' do
91
+ expect(subject).to respond_to(:enum)
92
+ end
93
+
94
+ it 'can be used in a single form' do
95
+ subject.enum('content_status')
96
+ expect(subject['content_status'].name).to be_eql('content_status')
97
+ expect(subject['content_status'].type).to be_eql(:content_status)
98
+ end
99
+
100
+ it 'can be used in a multiple form' do
101
+ subject.enum('foo', 'bar', 'baz', subtype: :content_status)
102
+ expect(subject['foo'].type).to be_eql(:content_status)
103
+ expect(subject['bar'].type).to be_eql(:content_status)
104
+ expect(subject['baz'].type).to be_eql(:content_status)
105
+ end
106
+
107
+ it 'can have custom type' do
108
+ subject.enum('foo', subtype: :content_status)
109
+ expect(subject['foo'].name).to be_eql('foo')
110
+ expect(subject['foo'].type).to be_eql(:content_status)
111
+ end
112
+
113
+ it 'raises StatementInvalid when type isn\'t defined' do
114
+ subject.enum('foo')
115
+ creation = connection.send(:schema_creation).accept subject
116
+ expect{ connection.execute creation }.to raise_error(ActiveRecord::StatementInvalid)
117
+ end
118
+ end
119
+
120
+ context 'on schema' do
121
+ it 'dumps when has it' do
122
+ dump_io = StringIO.new
123
+ ActiveRecord::SchemaDumper.dump(connection, dump_io)
124
+ expect(dump_io.string).to match /create_enum \"content_status\", \[/
125
+ end
126
+
127
+ it 'do not dump when has none' do
128
+ connection.drop_type(:content_status, force: :cascade)
129
+
130
+ dump_io = StringIO.new
131
+ ActiveRecord::SchemaDumper.dump(connection, dump_io)
132
+ expect(dump_io.string).not_to match /create_enum \"content_status\", \[/
133
+ end
134
+
135
+ it 'can be used on tables too' do
136
+ dump_io = StringIO.new
137
+ ActiveRecord::SchemaDumper.dump(connection, dump_io)
138
+ expect(dump_io.string).to match /t\.enum +"status", +subtype: :content_status/
139
+ end
140
+
141
+ it 'can have a default value as symbol' do
142
+ dump_io = StringIO.new
143
+ ActiveRecord::SchemaDumper.dump(connection, dump_io)
144
+ expect(dump_io.string).to match /t\.enum +"role", +default: :visitor, +subtype: :roles/
145
+ end
146
+ end
147
+
148
+ context 'on value' do
149
+ subject { Enum::ContentStatus }
150
+ let(:values) { %w(created draft published archived) }
151
+ let(:error) { Torque::PostgreSQL::Attributes::Enum::EnumError }
152
+ let(:mock_enum) do
153
+ klass = Class.new(subject.superclass)
154
+ klass.instance_variable_set(:@values, values << '15')
155
+ klass
156
+ end
157
+
158
+ it 'class exists' do
159
+ namespace = Torque::PostgreSQL.config.enum.namespace
160
+ expect(namespace.const_defined?('ContentStatus')).to be_truthy
161
+ expect(subject < Torque::PostgreSQL::Attributes::Enum).to be_truthy
162
+ end
163
+
164
+ it 'lazy loads values' do
165
+ expect(subject.instance_variable_defined?(:@values)).to be_falsey
166
+ end
167
+
168
+ it 'returns the db type name' do
169
+ expect(subject.type_name).to be_eql('content_status')
170
+ end
171
+
172
+ it 'values match database values' do
173
+ expect(subject.values).to be_eql(values)
174
+ end
175
+
176
+ it 'can return a sample value' do
177
+ expect(Enum).to respond_to(:sample)
178
+ expect(Enum::ContentStatus).to respond_to(:sample)
179
+ expect(Enum::ContentStatus.sample).to satisfy { |v| values.include?(v) }
180
+ expect(Enum.sample(:content_status)).to satisfy { |v| values.include?(v) }
181
+ end
182
+
183
+ it 'values can be iterated by using each direct on class' do
184
+ expect(subject).to respond_to(:each)
185
+ expect(subject.each).to be_a(Enumerator)
186
+ expect(subject.each.entries).to be_eql(values)
187
+ end
188
+
189
+ it 'values can be reach using fetch, as in hash enums' do
190
+ expect(subject).to respond_to(:fetch)
191
+
192
+ value = subject.fetch('archived', 'archived')
193
+ expect(value).to be_a(subject)
194
+ expect(value).to be_eql(subject.archived)
195
+
196
+ value = subject.fetch('other', 'other')
197
+ expect(value).to be_nil
198
+ end
199
+
200
+ it 'values can be reach using [], as in hash enums' do
201
+ expect(subject).to respond_to(:[])
202
+
203
+ value = subject['archived']
204
+ expect(value).to be_a(subject)
205
+ expect(value).to be_eql(subject.archived)
206
+
207
+ value = subject['other']
208
+ expect(value).to be_nil
209
+ end
210
+
211
+ it 'accepts respond_to against value' do
212
+ expect(subject).to respond_to(:archived)
213
+ end
214
+
215
+ it 'allows fast creation of values' do
216
+ value = subject.draft
217
+ expect(value).to be_a(subject)
218
+ end
219
+
220
+ it 'keeps blank values as Lazy' do
221
+ expect(subject.new(nil)).to be_nil
222
+ expect(subject.new([])).to be_nil
223
+ expect(subject.new('')).to be_nil
224
+ end
225
+
226
+ it 'can start from nil value using lazy' do
227
+ lazy = Torque::PostgreSQL::Attributes::Lazy
228
+ value = subject.new(nil)
229
+
230
+ expect(value.__class__).to be_eql(lazy)
231
+ expect(value.to_s).to be_eql('')
232
+ expect(value.to_i).to be_nil
233
+
234
+ expect(value.draft?).to be_falsey
235
+ end
236
+
237
+ it 'accepts values to come from numeric' do
238
+ expect(subject.new(0)).to be_eql(subject.created)
239
+ expect { subject.new(5) }.to raise_error(error, /out of bounds/)
240
+ end
241
+
242
+ it 'accepts string initialization' do
243
+ expect(subject.new('created')).to be_eql(subject.created)
244
+ expect { subject.new('updated') }.to raise_error(error, /not valid for/)
245
+ end
246
+
247
+ it 'allows values comparison' do
248
+ value = subject.draft
249
+ expect(value).to be > subject.created
250
+ expect(value).to be < subject.archived
251
+ expect(value).to be_eql(subject.draft)
252
+ expect(value).to_not be_eql(subject.published)
253
+ end
254
+
255
+ it 'allows values comparison with string' do
256
+ value = subject.draft
257
+ expect(value).to be > :created
258
+ expect(value).to be < :archived
259
+ expect(value).to be_eql(:draft)
260
+ expect(value).to_not be_eql(:published)
261
+ end
262
+
263
+ it 'allows values comparison with symbol' do
264
+ value = subject.draft
265
+ expect(value).to be > 'created'
266
+ expect(value).to be < 'archived'
267
+ expect(value).to be_eql('draft')
268
+ expect(value).to_not be_eql('published')
269
+ end
270
+
271
+ it 'allows values comparison with number' do
272
+ value = subject.draft
273
+ expect(value).to be > 0
274
+ expect(value).to be < 3
275
+ expect(value).to be_eql(1)
276
+ expect(value).to_not be_eql(2.5)
277
+ end
278
+
279
+ it 'does not allow cross-enum comparison' do
280
+ expect { subject.draft < mock_enum.published }.to raise_error(error, /^Comparison/)
281
+ expect { subject.draft > mock_enum.created }.to raise_error(error, /^Comparison/)
282
+ end
283
+
284
+ it 'does not allow other types comparison' do
285
+ expect { subject.draft > true }.to raise_error(error, /^Comparison/)
286
+ expect { subject.draft < [] }.to raise_error(error, /^Comparison/)
287
+ end
288
+
289
+ it 'accepts value checking' do
290
+ value = subject.draft
291
+ expect(value).to respond_to(:archived?)
292
+ expect(value.draft?).to be_truthy
293
+ expect(value.published?).to be_falsey
294
+ end
295
+
296
+ it 'accepts replace and bang value' do
297
+ value = subject.draft
298
+ expect(value).to respond_to(:archived!)
299
+ expect(value.archived!).to be_eql(subject.archived)
300
+ expect(value.replace('created')).to be_eql(subject.created)
301
+ end
302
+
303
+ it 'accepts values turn into integer by its index' do
304
+ mock_value = mock_enum.new('15')
305
+ expect(subject.created.to_i).to be_eql(0)
306
+ expect(subject.archived.to_i).to be_eql(3)
307
+ expect(mock_value.to_i).to_not be_eql(15)
308
+ expect(mock_value.to_i).to be_eql(4)
309
+ end
310
+
311
+ context 'on members' do
312
+ it 'has enumerable operations' do
313
+ expect(subject).to respond_to(:all?)
314
+ expect(subject).to respond_to(:any?)
315
+ expect(subject).to respond_to(:collect)
316
+ expect(subject).to respond_to(:count)
317
+ expect(subject).to respond_to(:cycle)
318
+ expect(subject).to respond_to(:detect)
319
+ expect(subject).to respond_to(:drop)
320
+ expect(subject).to respond_to(:drop_while)
321
+ expect(subject).to respond_to(:each)
322
+ expect(subject).to respond_to(:each_with_index)
323
+ expect(subject).to respond_to(:entries)
324
+ expect(subject).to respond_to(:find)
325
+ expect(subject).to respond_to(:find_all)
326
+ expect(subject).to respond_to(:find_index)
327
+ expect(subject).to respond_to(:first)
328
+ expect(subject).to respond_to(:flat_map)
329
+ expect(subject).to respond_to(:include?)
330
+ expect(subject).to respond_to(:inject)
331
+ expect(subject).to respond_to(:lazy)
332
+ expect(subject).to respond_to(:map)
333
+ expect(subject).to respond_to(:member?)
334
+ expect(subject).to respond_to(:one?)
335
+ expect(subject).to respond_to(:reduce)
336
+ expect(subject).to respond_to(:reject)
337
+ expect(subject).to respond_to(:reverse_each)
338
+ expect(subject).to respond_to(:select)
339
+ expect(subject).to respond_to(:sort)
340
+ expect(subject).to respond_to(:zip)
341
+ end
342
+
343
+ it 'works with map' do
344
+ result = subject.map(&:to_i)
345
+ expect(result).to be_eql([0, 1, 2, 3])
346
+ end
347
+ end
348
+ end
349
+
350
+ context 'on OID' do
351
+ let(:enum) { Enum::ContentStatus }
352
+ subject { Torque::PostgreSQL::Adapter::OID::Enum.new('content_status') }
353
+
354
+ context 'on deserialize' do
355
+ it 'returns nil' do
356
+ expect(subject.deserialize(nil)).to be_nil
357
+ end
358
+
359
+ it 'returns enum' do
360
+ value = subject.deserialize('created')
361
+ expect(value).to be_a(enum)
362
+ expect(value).to be_eql(enum.created)
363
+ end
364
+ end
365
+
366
+ context 'on serialize' do
367
+ it 'returns nil' do
368
+ expect(subject.serialize(nil)).to be_nil
369
+ expect(subject.serialize('test')).to be_nil
370
+ expect(subject.serialize(15)).to be_nil
371
+ end
372
+
373
+ it 'returns as string' do
374
+ expect(subject.serialize(enum.created)).to be_eql('created')
375
+ expect(subject.serialize(1)).to be_eql('draft')
376
+ end
377
+ end
378
+
379
+ context 'on cast' do
380
+ it 'accepts nil' do
381
+ expect(subject.cast(nil)).to be_nil
382
+ end
383
+
384
+ it 'accepts invalid values as nil' do
385
+ expect(subject.cast(false)).to be_nil
386
+ expect(subject.cast(true)).to be_nil
387
+ expect(subject.cast([])).to be_nil
388
+ end
389
+
390
+ it 'accepts string' do
391
+ value = subject.cast('created')
392
+ expect(value).to be_a(enum)
393
+ expect(value).to be_eql(enum.created)
394
+ end
395
+
396
+ it 'accepts numeric' do
397
+ value = subject.cast(1)
398
+ expect(value).to be_a(enum)
399
+ expect(value).to be_eql(enum.draft)
400
+ end
401
+ end
402
+ end
403
+
404
+ context 'on I18n' do
405
+ subject { Enum::ContentStatus }
406
+
407
+ it 'has the text method' do
408
+ expect(subject.new(0)).to respond_to(:text)
409
+ end
410
+
411
+ it 'brings the correct values' do
412
+ expect(subject.new(0).text).to be_eql('1 - Created')
413
+ expect(subject.new(1).text).to be_eql('Draft (2)')
414
+ expect(subject.new(2).text).to be_eql('Finally published')
415
+ expect(subject.new(3).text).to be_eql('Archived')
416
+ end
417
+ end
418
+
419
+ context 'on model' do
420
+ before(:each) { decorate(User, :role) }
421
+
422
+ subject { User }
423
+ let(:instance) { FactoryBot.build(:user) }
424
+
425
+ it 'has all enum methods' do
426
+ expect(subject).to respond_to(:roles)
427
+ expect(subject).to respond_to(:roles_keys)
428
+ expect(subject).to respond_to(:roles_texts)
429
+ expect(subject).to respond_to(:roles_options)
430
+ expect(instance).to respond_to(:role_text)
431
+
432
+ subject.roles.each do |value|
433
+ expect(subject).to respond_to(value)
434
+ expect(instance).to respond_to(value + '?')
435
+ expect(instance).to respond_to(value + '!')
436
+ end
437
+ end
438
+
439
+ it 'plural method brings the list of values' do
440
+ result = subject.roles
441
+ expect(result).to be_a(Array)
442
+ expect(result).to be_eql(Enum::Roles.values)
443
+ end
444
+
445
+ it 'text value now uses model and attribute references' do
446
+ instance.role = :visitor
447
+ expect(instance.role_text).to be_eql('A simple Visitor')
448
+
449
+ instance.role = :assistant
450
+ expect(instance.role_text).to be_eql('An Assistant')
451
+
452
+ instance.role = :manager
453
+ expect(instance.role_text).to be_eql('The Manager')
454
+
455
+ instance.role = :admin
456
+ expect(instance.role_text).to be_eql('Super Duper Admin')
457
+ end
458
+
459
+ it 'has scopes correctly applied' do
460
+ subject.roles.each do |value|
461
+ expect(subject.send(value).to_sql).to match(/WHERE "users"."role" = '#{value}'/)
462
+ end
463
+ end
464
+
465
+ it 'has scopes available on associations' do
466
+ author = FactoryBot.create(:author)
467
+ FactoryBot.create(:post, author: author)
468
+
469
+ decorate(Post, :status)
470
+ expect(author.posts).to respond_to(:test_scope)
471
+
472
+ Enum::ContentStatus.each do |value|
473
+ expect(author.posts).to be_a(ActiveRecord::Associations::CollectionProxy)
474
+ expect(author.posts).to respond_to(value.to_sym)
475
+ expect(author.posts.send(value).to_sql).to match(/AND "posts"."status" = '#{value}'/)
476
+ end
477
+ end
478
+
479
+ it 'ask methods work' do
480
+ instance.role = :assistant
481
+ expect(instance.manager?).to be_falsey
482
+ expect(instance.assistant?).to be_truthy
483
+ end
484
+
485
+ it 'bang methods work' do
486
+ instance.admin!
487
+ expect(instance.persisted?).to be_truthy
488
+
489
+ updated_at = instance.updated_at
490
+ Torque::PostgreSQL.config.enum.save_on_bang = false
491
+ instance.visitor!
492
+ Torque::PostgreSQL.config.enum.save_on_bang = true
493
+
494
+ expect(instance.role).to be_eql(:visitor)
495
+ expect(instance.updated_at).to be_eql(updated_at)
496
+
497
+ instance.reload
498
+ expect(instance.role).to be_eql(:admin)
499
+ end
500
+
501
+ it 'raises when starting an enum with conflicting methods' do
502
+ Torque::PostgreSQL.config.enum.raise_conflicting = true
503
+ AText = Class.new(ActiveRecord::Base)
504
+ AText.table_name = 'texts'
505
+
506
+ expect { decorate(AText, :conflict) }.to raise_error(ArgumentError, /already exists in/)
507
+ Torque::PostgreSQL.config.enum.raise_conflicting = false
508
+ end
509
+
510
+ it 'scope the model correctly' do
511
+ query = subject.manager.to_sql
512
+ expect(query).to match(/"users"."role" = 'manager'/)
513
+ end
514
+
515
+ context 'on inherited classes' do
516
+ it 'has all enum methods' do
517
+ klass = Class.new(User)
518
+ instance = klass.new
519
+
520
+ expect(klass).to respond_to(:roles)
521
+ expect(klass).to respond_to(:roles_keys)
522
+ expect(klass).to respond_to(:roles_texts)
523
+ expect(klass).to respond_to(:roles_options)
524
+ expect(instance).to respond_to(:role_text)
525
+
526
+ klass.roles.each do |value|
527
+ expect(klass).to respond_to(value)
528
+ expect(instance).to respond_to(value + '?')
529
+ expect(instance).to respond_to(value + '!')
530
+ end
531
+ end
532
+ end
533
+
534
+ context 'without autoload' do
535
+ subject { Author }
536
+ let(:instance) { FactoryBot.build(:author) }
537
+
538
+ it 'has both rails original enum and the new pg_enum' do
539
+ expect(subject).to respond_to(:enum)
540
+ expect(subject).to respond_to(:pg_enum)
541
+ expect(subject.method(:pg_enum).arity).to eql(-1)
542
+ end
543
+
544
+ it 'does not create all methods' do
545
+ AAuthor = Class.new(ActiveRecord::Base)
546
+ AAuthor.table_name = 'authors'
547
+
548
+ expect(AAuthor).to_not respond_to(:specialties)
549
+ expect(AAuthor).to_not respond_to(:specialties_keys)
550
+ expect(AAuthor).to_not respond_to(:specialties_texts)
551
+ expect(AAuthor).to_not respond_to(:specialties_options)
552
+ expect(AAuthor.instance_methods).to_not include(:specialty_text)
553
+
554
+ Enum::Specialties.values.each do |value|
555
+ expect(AAuthor).to_not respond_to(value)
556
+ expect(AAuthor.instance_methods).to_not include(value + '?')
557
+ expect(AAuthor.instance_methods).to_not include(value + '!')
558
+ end
559
+ end
560
+
561
+ it 'can be manually initiated' do
562
+ decorate(Author, :specialty)
563
+ expect(subject).to respond_to(:specialties)
564
+ expect(subject).to respond_to(:specialties_keys)
565
+ expect(subject).to respond_to(:specialties_texts)
566
+ expect(subject).to respond_to(:specialties_options)
567
+ expect(instance).to respond_to(:specialty_text)
568
+
569
+ Enum::Specialties.values.each do |value|
570
+ expect(subject).to respond_to(value)
571
+ expect(instance).to respond_to(value + '?')
572
+ expect(instance).to respond_to(value + '!')
573
+ end
574
+ end
575
+ end
576
+
577
+ context 'with prefix' do
578
+ before(:each) { decorate(Author, :specialty, prefix: 'in') }
579
+ subject { Author }
580
+ let(:instance) { FactoryBot.build(:author) }
581
+
582
+ it 'creates all methods correctly' do
583
+ expect(subject).to respond_to(:specialties)
584
+ expect(subject).to respond_to(:specialties_keys)
585
+ expect(subject).to respond_to(:specialties_texts)
586
+ expect(subject).to respond_to(:specialties_options)
587
+ expect(instance).to respond_to(:specialty_text)
588
+
589
+ subject.specialties.each do |value|
590
+ expect(subject).to respond_to('in_' + value)
591
+ expect(instance).to respond_to('in_' + value + '?')
592
+ expect(instance).to respond_to('in_' + value + '!')
593
+ end
594
+ end
595
+ end
596
+
597
+ context 'with suffix, only, and except' do
598
+ before(:each) do
599
+ decorate(Author, :specialty, suffix: 'expert', only: %w(books movies), except: 'books')
600
+ end
601
+
602
+ subject { Author }
603
+ let(:instance) { FactoryBot.build(:author) }
604
+
605
+ it 'creates only the requested methods' do
606
+ expect(subject).to respond_to('movies_expert')
607
+ expect(instance).to respond_to('movies_expert?')
608
+ expect(instance).to respond_to('movies_expert!')
609
+
610
+ expect(subject).to_not respond_to('books_expert')
611
+ expect(instance).to_not respond_to('books_expert?')
612
+ expect(instance).to_not respond_to('books_expert!')
613
+
614
+ expect(subject).to_not respond_to('plays_expert')
615
+ expect(instance).to_not respond_to('plays_expert?')
616
+ expect(instance).to_not respond_to('plays_expert!')
617
+
618
+ end
619
+ end
620
+ end
621
+ end