torque-postgresql 1.1.8 → 2.0.0

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