torque-postgresql 1.1.5 → 2.0.3

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