torque-postgresql 1.1.1 → 2.0.1

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 (72) hide show
  1. checksums.yaml +5 -5
  2. data/Rakefile +5 -2
  3. data/lib/torque/postgresql.rb +0 -2
  4. data/lib/torque/postgresql/adapter.rb +0 -1
  5. data/lib/torque/postgresql/adapter/database_statements.rb +4 -15
  6. data/lib/torque/postgresql/adapter/schema_creation.rb +13 -23
  7. data/lib/torque/postgresql/adapter/schema_definitions.rb +7 -21
  8. data/lib/torque/postgresql/adapter/schema_dumper.rb +71 -11
  9. data/lib/torque/postgresql/adapter/schema_statements.rb +2 -12
  10. data/lib/torque/postgresql/associations.rb +0 -3
  11. data/lib/torque/postgresql/associations/association_scope.rb +18 -61
  12. data/lib/torque/postgresql/associations/belongs_to_many_association.rb +2 -1
  13. data/lib/torque/postgresql/associations/preloader.rb +0 -24
  14. data/lib/torque/postgresql/associations/preloader/association.rb +13 -9
  15. data/lib/torque/postgresql/auxiliary_statement.rb +12 -17
  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/relation.rb +11 -16
  21. data/lib/torque/postgresql/relation/auxiliary_statement.rb +9 -15
  22. data/lib/torque/postgresql/relation/distinct_on.rb +1 -1
  23. data/lib/torque/postgresql/schema_cache.rb +19 -11
  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 +240 -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 +390 -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 +103 -16
  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