torque-postgresql 3.4.1 → 4.0.0.rc1

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/lib/torque/postgresql/adapter/database_statements.rb +63 -84
  3. data/lib/torque/postgresql/adapter/oid/array.rb +17 -0
  4. data/lib/torque/postgresql/adapter/oid/line.rb +2 -6
  5. data/lib/torque/postgresql/adapter/oid/range.rb +4 -4
  6. data/lib/torque/postgresql/adapter/oid.rb +1 -23
  7. data/lib/torque/postgresql/adapter/quoting.rb +13 -7
  8. data/lib/torque/postgresql/adapter/schema_creation.rb +7 -28
  9. data/lib/torque/postgresql/adapter/schema_definitions.rb +36 -0
  10. data/lib/torque/postgresql/adapter/schema_dumper.rb +90 -34
  11. data/lib/torque/postgresql/adapter/schema_overrides.rb +45 -0
  12. data/lib/torque/postgresql/adapter/schema_statements.rb +64 -49
  13. data/lib/torque/postgresql/arel/infix_operation.rb +15 -28
  14. data/lib/torque/postgresql/arel/nodes.rb +2 -2
  15. data/lib/torque/postgresql/arel/operations.rb +7 -1
  16. data/lib/torque/postgresql/arel/visitors.rb +3 -9
  17. data/lib/torque/postgresql/associations/association_scope.rb +23 -31
  18. data/lib/torque/postgresql/associations/belongs_to_many_association.rb +25 -0
  19. data/lib/torque/postgresql/associations/builder/belongs_to_many.rb +16 -0
  20. data/lib/torque/postgresql/attributes/builder/enum.rb +12 -9
  21. data/lib/torque/postgresql/attributes/builder/full_text_search.rb +121 -0
  22. data/lib/torque/postgresql/attributes/builder/period.rb +21 -21
  23. data/lib/torque/postgresql/attributes/builder.rb +49 -11
  24. data/lib/torque/postgresql/attributes/enum.rb +7 -7
  25. data/lib/torque/postgresql/attributes/enum_set.rb +7 -7
  26. data/lib/torque/postgresql/attributes/full_text_search.rb +19 -0
  27. data/lib/torque/postgresql/attributes/period.rb +2 -2
  28. data/lib/torque/postgresql/attributes.rb +0 -4
  29. data/lib/torque/postgresql/auxiliary_statement/recursive.rb +3 -3
  30. data/lib/torque/postgresql/base.rb +3 -10
  31. data/lib/torque/postgresql/collector.rb +1 -1
  32. data/lib/torque/postgresql/config.rb +95 -5
  33. data/lib/torque/postgresql/function.rb +61 -0
  34. data/lib/torque/postgresql/inheritance.rb +52 -36
  35. data/lib/torque/postgresql/predicate_builder/arel_attribute_handler.rb +33 -0
  36. data/lib/torque/postgresql/predicate_builder/array_handler.rb +47 -0
  37. data/lib/torque/postgresql/predicate_builder/enumerator_lazy_handler.rb +37 -0
  38. data/lib/torque/postgresql/predicate_builder/regexp_handler.rb +21 -0
  39. data/lib/torque/postgresql/predicate_builder.rb +35 -0
  40. data/lib/torque/postgresql/railtie.rb +112 -30
  41. data/lib/torque/postgresql/reflection/abstract_reflection.rb +12 -44
  42. data/lib/torque/postgresql/reflection/belongs_to_many_reflection.rb +4 -0
  43. data/lib/torque/postgresql/reflection/has_many_reflection.rb +4 -0
  44. data/lib/torque/postgresql/reflection/runtime_reflection.rb +1 -1
  45. data/lib/torque/postgresql/relation/inheritance.rb +4 -7
  46. data/lib/torque/postgresql/relation.rb +6 -10
  47. data/lib/torque/postgresql/schema_cache.rb +6 -12
  48. data/lib/torque/postgresql/version.rb +1 -1
  49. data/lib/torque/postgresql.rb +2 -1
  50. data/spec/initialize.rb +58 -0
  51. data/spec/mocks/cache_query.rb +21 -21
  52. data/spec/mocks/create_table.rb +6 -26
  53. data/spec/schema.rb +19 -12
  54. data/spec/spec_helper.rb +5 -1
  55. data/spec/tests/arel_spec.rb +32 -7
  56. data/spec/tests/auxiliary_statement_spec.rb +3 -3
  57. data/spec/tests/belongs_to_many_spec.rb +72 -5
  58. data/spec/tests/enum_set_spec.rb +12 -11
  59. data/spec/tests/enum_spec.rb +4 -2
  60. data/spec/tests/full_text_seach_test.rb +252 -0
  61. data/spec/tests/function_spec.rb +42 -0
  62. data/spec/tests/has_many_spec.rb +21 -8
  63. data/spec/tests/interval_spec.rb +1 -7
  64. data/spec/tests/period_spec.rb +61 -61
  65. data/spec/tests/predicate_builder_spec.rb +132 -0
  66. data/spec/tests/schema_spec.rb +2 -8
  67. data/spec/tests/table_inheritance_spec.rb +25 -26
  68. metadata +34 -39
@@ -70,7 +70,6 @@ RSpec.describe 'Enum' do
70
70
  end
71
71
 
72
72
  context 'on value' do
73
- subject { Enum::ContentStatus }
74
73
  let(:values) { %w(created draft published archived) }
75
74
  let(:error) { Torque::PostgreSQL::Attributes::Enum::EnumError }
76
75
  let(:mock_enum) do
@@ -79,6 +78,8 @@ RSpec.describe 'Enum' do
79
78
  klass
80
79
  end
81
80
 
81
+ subject { Enum::ContentStatus }
82
+
82
83
  it 'class exists' do
83
84
  namespace = Torque::PostgreSQL.config.enum.namespace
84
85
  expect(namespace.const_defined?('ContentStatus')).to be_truthy
@@ -341,10 +342,11 @@ RSpec.describe 'Enum' do
341
342
  end
342
343
 
343
344
  context 'on model' do
345
+ let(:instance) { FactoryBot.build(:user) }
346
+
344
347
  before(:each) { decorate(User, :role) }
345
348
 
346
349
  subject { User }
347
- let(:instance) { FactoryBot.build(:user) }
348
350
 
349
351
  it 'has all enum methods' do
350
352
  expect(subject).to respond_to(:roles)
@@ -0,0 +1,252 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe 'FullTextSearch' do
4
+ context 'on builder' do
5
+ let(:builder) { Torque::PostgreSQL::Attributes::Builder }
6
+
7
+ describe '.to_search_weights' do
8
+ it 'works with a single column' do
9
+ expect(builder.to_search_weights('title')).to eq({ 'title' => 'A' })
10
+ expect(builder.to_search_weights(:title)).to eq({ 'title' => 'A' })
11
+ end
12
+
13
+ it 'works with an array of columns' do
14
+ value = { 'title' => 'A', 'content' => 'B' }
15
+ expect(builder.to_search_weights(%w[title content])).to eq(value)
16
+ expect(builder.to_search_weights(%i[title content])).to eq(value)
17
+ end
18
+
19
+ it 'works with a hash of columns and weights' do
20
+ value = { 'title' => 'A', 'content' => 'B', 'summary' => 'C' }
21
+ expect(builder.to_search_weights(value.transform_keys(&:to_sym))).to eq(value)
22
+ end
23
+
24
+ it 'works with a hash of columns and invalid weights' do
25
+ value = { 'title' => 'X', 'content' => 'Y', 'summary' => 'Z' }
26
+ expect(builder.to_search_weights(value.transform_keys(&:to_sym))).to eq(value)
27
+ end
28
+ end
29
+
30
+ describe '.to_search_vector_operation' do
31
+ it 'builds a simple one' do
32
+ result = builder.to_search_vector_operation('english', { 'title' => 'A' })
33
+ expect(result.to_sql).to eq("TO_TSVECTOR('english', COALESCE(title, ''))")
34
+ end
35
+
36
+ it 'builds with 2 columns' do
37
+ columns = { 'title' => 'A', 'content' => 'B' }
38
+ result = builder.to_search_vector_operation('english', columns)
39
+ expect(result.to_sql).to eq(<<~SQL.squish)
40
+ SETWEIGHT(TO_TSVECTOR('english', COALESCE(title, '')), 'A') ||
41
+ SETWEIGHT(TO_TSVECTOR('english', COALESCE(content, '')), 'B')
42
+ SQL
43
+ end
44
+
45
+ it 'builds with a dynamic language' do
46
+ columns = { 'title' => 'A', 'content' => 'B' }
47
+ result = builder.to_search_vector_operation(:lang, columns)
48
+ expect(result.to_sql).to eq(<<~SQL.squish)
49
+ SETWEIGHT(TO_TSVECTOR(lang, COALESCE(title, '')), 'A') ||
50
+ SETWEIGHT(TO_TSVECTOR(lang, COALESCE(content, '')), 'B')
51
+ SQL
52
+ end
53
+ end
54
+
55
+ describe '.search_vector_options' do
56
+ it 'correctly translates the settings' do
57
+ options = builder.search_vector_options(columns: 'title')
58
+ expect(options).to eq(
59
+ type: :tsvector,
60
+ as: "TO_TSVECTOR('english', COALESCE(title, ''))",
61
+ stored: true,
62
+ )
63
+ end
64
+
65
+ it 'properly adds the index type' do
66
+ options = builder.search_vector_options(columns: 'title', index: true)
67
+ expect(options).to eq(
68
+ type: :tsvector,
69
+ as: "TO_TSVECTOR('english', COALESCE(title, ''))",
70
+ stored: true,
71
+ index: { using: :gin },
72
+ )
73
+ end
74
+ end
75
+ end
76
+
77
+ context 'on schema dumper' do
78
+ let(:connection) { ActiveRecord::Base.connection }
79
+ let(:source) { ActiveRecord::Base.connection_pool }
80
+ let(:dump_result) do
81
+ ActiveRecord::SchemaDumper.dump(source, (dump_result = StringIO.new))
82
+ dump_result.string
83
+ end
84
+
85
+ it 'properly supports search language' do
86
+ parts = %{t.search_language "lang", default: "english", null: false}
87
+ expect(dump_result).to include(parts)
88
+ end
89
+
90
+ it 'properly translates a simple single search vector with embedded language' do
91
+ parts = 't.search_vector "search_vector", stored: true'
92
+ parts << ', language: :lang, columns: :title'
93
+ expect(dump_result).to include(parts)
94
+ end
95
+
96
+ it 'properly translates a simple multiple column search vector with language' do
97
+ parts = 't.search_vector "search_vector", stored: true'
98
+ parts << ', language: "english", columns: [:title, :content]'
99
+ expect(dump_result).to include(parts)
100
+ end
101
+
102
+ it 'supports a custom definition of weights' do
103
+ connection.create_table :custom_search do |t|
104
+ t.string :title
105
+ t.string :content
106
+ t.string :subtitle
107
+ t.search_vector :sample_a, columns: {
108
+ title: 'A',
109
+ subtitle: 'A',
110
+ content: 'B',
111
+ }
112
+ t.search_vector :sample_b, columns: {
113
+ title: 'A',
114
+ subtitle: 'C',
115
+ content: 'D',
116
+ }
117
+ t.search_vector :sample_c, columns: {
118
+ title: 'C',
119
+ subtitle: 'B',
120
+ content: 'A',
121
+ }
122
+ end
123
+
124
+ parts = 't.search_vector "sample_a", stored: true'
125
+ parts << ', language: "english", columns: { title: "A", subtitle: "A", content: "B" }'
126
+ expect(dump_result).to include(parts)
127
+
128
+ parts = 't.search_vector "sample_b", stored: true'
129
+ parts << ', language: "english", columns: { title: "A", subtitle: "C", content: "D" }'
130
+ expect(dump_result).to include(parts)
131
+
132
+ parts = 't.search_vector "sample_c", stored: true'
133
+ parts << ', language: "english", columns: [:content, :subtitle, :title]'
134
+ expect(dump_result).to include(parts)
135
+ end
136
+ end
137
+
138
+ context 'on config' do
139
+ let(:base) { Course }
140
+ let(:scope) { 'full_text_search' }
141
+
142
+ let(:mod) { base.singleton_class.included_modules.first }
143
+
144
+ after { mod.send(:undef_method, scope) if scope.present? }
145
+
146
+ it 'has the initialization method' do
147
+ scope.replace('')
148
+ expect(base).to respond_to(:torque_search_for)
149
+ end
150
+
151
+ it 'properly generates the search scope' do
152
+ base.torque_search_for(:search_vector)
153
+ expect(base.all).to respond_to(:full_text_search)
154
+ end
155
+
156
+ it 'works with prefix and suffix' do
157
+ scope.replace('custom_full_text_search_scope')
158
+ base.torque_search_for(:search_vector, prefix: 'custom', suffix: 'scope')
159
+ expect(base.all).to respond_to(:custom_full_text_search_scope)
160
+ end
161
+ end
162
+
163
+ context 'on relation' do
164
+ let(:base) { Course }
165
+ let(:scope) { 'full_text_search' }
166
+
167
+ let(:mod) { base.singleton_class.included_modules.first }
168
+
169
+ before { Course.torque_search_for(:search_vector) }
170
+ after { mod.send(:undef_method, :full_text_search) }
171
+
172
+ it 'performs a simple query' do
173
+ result = Course.full_text_search('test')
174
+ parts = 'SELECT "courses".* FROM "courses"'
175
+ parts << ' WHERE "courses"."search_vector" @@'
176
+ parts << " PHRASETO_TSQUERY('english', 'test')"
177
+ expect(result.to_sql).to eql(parts)
178
+ end
179
+
180
+ it 'can include the order' do
181
+ result = Course.full_text_search('test', order: true)
182
+ parts = 'SELECT "courses".* FROM "courses"'
183
+ parts << ' WHERE "courses"."search_vector" @@'
184
+ parts << " PHRASETO_TSQUERY('english', 'test')"
185
+ parts << ' ORDER BY TS_RANK("courses"."search_vector",'
186
+ parts << " PHRASETO_TSQUERY('english', 'test')) ASC"
187
+ expect(result.to_sql).to eql(parts)
188
+ end
189
+
190
+ it 'can include the order descending' do
191
+ result = Course.full_text_search('test', order: :desc)
192
+ parts = 'SELECT "courses".* FROM "courses"'
193
+ parts << ' WHERE "courses"."search_vector" @@'
194
+ parts << " PHRASETO_TSQUERY('english', 'test')"
195
+ parts << ' ORDER BY TS_RANK("courses"."search_vector",'
196
+ parts << " PHRASETO_TSQUERY('english', 'test')) DESC"
197
+ expect(result.to_sql).to eql(parts)
198
+ end
199
+
200
+ it 'can include the rank' do
201
+ result = Course.full_text_search('test', rank: true)
202
+ parts = 'SELECT "courses".*, TS_RANK("courses"."search_vector",'
203
+ parts << " PHRASETO_TSQUERY('english', 'test')) AS rank"
204
+ parts << ' FROM "courses" WHERE "courses"."search_vector" @@'
205
+ parts << " PHRASETO_TSQUERY('english', 'test')"
206
+ expect(result.to_sql).to eql(parts)
207
+ end
208
+
209
+ it 'can include the rank named differently' do
210
+ result = Course.full_text_search('test', rank: :custom_rank)
211
+ parts = 'SELECT "courses".*, TS_RANK("courses"."search_vector",'
212
+ parts << " PHRASETO_TSQUERY('english', 'test')) AS custom_rank"
213
+ parts << ' FROM "courses" WHERE "courses"."search_vector" @@'
214
+ parts << " PHRASETO_TSQUERY('english', 'test')"
215
+ expect(result.to_sql).to eql(parts)
216
+ end
217
+
218
+ it 'can use regular query mode' do
219
+ result = Course.full_text_search('test', phrase: false)
220
+ parts = 'SELECT "courses".* FROM "courses"'
221
+ parts << ' WHERE "courses"."search_vector" @@'
222
+ parts << " TO_TSQUERY('english', 'test')"
223
+ expect(result.to_sql).to eql(parts)
224
+ end
225
+
226
+ it 'can use a attribute as the language' do
227
+ result = Course.full_text_search('test', language: :lang)
228
+ parts = 'SELECT "courses".* FROM "courses"'
229
+ parts << ' WHERE "courses"."search_vector" @@'
230
+ parts << %{ PHRASETO_TSQUERY("courses"."lang", 'test')}
231
+ expect(result.to_sql).to eql(parts)
232
+ end
233
+
234
+ it 'can call a method to pull the language' do
235
+ Course.define_singleton_method(:search_language) { 'portuguese' }
236
+ result = Course.full_text_search('test', language: :search_language)
237
+ parts = 'SELECT "courses".* FROM "courses"'
238
+ parts << ' WHERE "courses"."search_vector" @@'
239
+ parts << " PHRASETO_TSQUERY('portuguese', 'test')"
240
+ expect(result.to_sql).to eql(parts)
241
+ Course.singleton_class.undef_method(:search_language)
242
+ end
243
+
244
+ it 'properly binds all provided values' do
245
+ query = Course.full_text_search('test')
246
+ sql, binds = get_query_with_binds { query.load }
247
+ expect(sql).to include("PHRASETO_TSQUERY($1, $2)")
248
+ expect(binds.first.value).to eq('english')
249
+ expect(binds.second.value).to eq('test')
250
+ end
251
+ end
252
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe 'Function' do
4
+ let(:helper) { Torque::PostgreSQL::FN }
5
+ let(:conn) { ActiveRecord::Base.connection }
6
+ let(:visitor) { ::Arel::Visitors::PostgreSQL.new(conn) }
7
+ let(:collector) { ::Arel::Collectors::SQLString }
8
+
9
+ context 'on helper' do
10
+ it 'helps creating a bind' do
11
+ type = ::ActiveRecord::Type::String.new
12
+ expect(helper.bind(:foo, 'test', type)).to be_a(::Arel::Nodes::BindParam)
13
+ end
14
+
15
+ it 'helps creating a bind for a model attribute' do
16
+ expect(helper.bind_for(Video, :title, 'test')).to be_a(::Arel::Nodes::BindParam)
17
+ end
18
+
19
+ it 'helps creating a bind for an arel attribute' do
20
+ attr = Video.arel_table['title']
21
+ expect(helper.bind_with(attr, 'test')).to be_a(::Arel::Nodes::BindParam)
22
+ end
23
+
24
+ it 'helps concatenating arguments' do
25
+ values = %w[a b c].map(&::Arel.method(:sql))
26
+
27
+ # Unable to just call .sql with a simple thing
28
+ visited = visitor.accept(helper.concat(values[0]), collector.new)
29
+ expect(visited.value).to eq("a")
30
+
31
+ # 2+ we can call .sql directly
32
+ expect(helper.concat(values[0], values[1]).to_sql).to eq("a || b")
33
+ expect(helper.concat(values[0], values[1], values[2]).to_sql).to eq("a || b || c")
34
+ end
35
+
36
+ it 'helps building any other function' do
37
+ values = %w[a b c].map(&::Arel.method(:sql))
38
+ expect(helper).to respond_to(:coalesce)
39
+ expect(helper.coalesce(values[0], values[1]).to_sql).to eq("COALESCE(a, b)")
40
+ end
41
+ end
42
+ end
@@ -11,7 +11,7 @@ RSpec.describe 'HasMany' do
11
11
 
12
12
  context 'on original' do
13
13
  let(:other) { Text }
14
- let(:key) { Torque::PostgreSQL::AR720 ? :texts : 'texts' }
14
+ let(:key) { :texts }
15
15
 
16
16
  before { User.has_many :texts }
17
17
  subject { User.create(name: 'User 1') }
@@ -247,7 +247,7 @@ RSpec.describe 'HasMany' do
247
247
 
248
248
  context 'on array' do
249
249
  let(:other) { Video }
250
- let(:key) { Torque::PostgreSQL::AR720 ? :videos : 'videos' }
250
+ let(:key) { :videos }
251
251
 
252
252
  before { Tag.has_many :videos, array: true }
253
253
  subject { Tag.create(name: 'A') }
@@ -264,15 +264,21 @@ RSpec.describe 'HasMany' do
264
264
  end
265
265
 
266
266
  it 'loads associated records' do
267
- expect(subject.videos.to_sql).to match(Regexp.new(<<-SQL.squish))
268
- SELECT "videos"\\.\\* FROM "videos"
269
- WHERE \\(?"videos"\\."tag_ids" && ARRAY\\[#{subject.id}\\]::bigint\\[\\]\\)?
267
+ expect(subject.videos.to_sql).to eq(<<~SQL.squish)
268
+ SELECT "videos".* FROM "videos" WHERE #{subject.id} = ANY("videos"."tag_ids")
270
269
  SQL
271
270
 
272
271
  expect(subject.videos.load).to be_a(ActiveRecord::Associations::CollectionProxy)
273
272
  expect(subject.videos.to_a).to be_eql([])
274
273
  end
275
274
 
275
+ it 'uses binds instead of the literal value' do
276
+ query = subject.videos
277
+ sql, binds = get_query_with_binds { query.load }
278
+ expect(sql).to include('WHERE $1 = ANY("videos"."tag_ids")')
279
+ expect(binds.first.value).to eq(subject.id)
280
+ end
281
+
276
282
  it 'can be marked as loaded' do
277
283
  expect(subject.videos.loaded?).to be_eql(false)
278
284
  expect(subject.videos).to respond_to(:load_target)
@@ -474,15 +480,22 @@ RSpec.describe 'HasMany' do
474
480
  subject { player.create }
475
481
 
476
482
  it 'loads associated records' do
477
- expect(subject.games.to_sql).to match(Regexp.new(<<-SQL.squish))
478
- SELECT "games"\\.\\* FROM "games"
479
- WHERE \\(?"games"\\."player_ids" && ARRAY\\['#{subject.id}'\\]::uuid\\[\\]\\)?
483
+ expect(subject.games.to_sql).to eq(<<~SQL.squish)
484
+ SELECT "games".* FROM "games"
485
+ WHERE '#{subject.id}' = ANY("games"."player_ids")
480
486
  SQL
481
487
 
482
488
  expect(subject.games.load).to be_a(ActiveRecord::Associations::CollectionProxy)
483
489
  expect(subject.games.to_a).to be_eql([])
484
490
  end
485
491
 
492
+ it 'uses binds instead of the literal value' do
493
+ query = subject.games
494
+ sql, binds = get_query_with_binds { query.load }
495
+ expect(sql).to include('WHERE $1 = ANY("games"."player_ids")')
496
+ expect(binds.first.value).to eq(subject.id)
497
+ end
498
+
486
499
  it 'can preload records' do
487
500
  5.times { game.create(player_ids: [subject.id]) }
488
501
  entries = player.all.includes(:games).load
@@ -3,13 +3,7 @@ require 'spec_helper'
3
3
  RSpec.describe 'Interval' do
4
4
  let(:table_definition) { ActiveRecord::ConnectionAdapters::PostgreSQL::TableDefinition }
5
5
  let(:connection) { ActiveRecord::Base.connection }
6
- let(:source) do
7
- if Torque::PostgreSQL::AR720
8
- ActiveRecord::Base.connection_pool
9
- else
10
- ActiveRecord::Base.connection
11
- end
12
- end
6
+ let(:source) { ActiveRecord::Base.connection_pool }
13
7
 
14
8
  context 'on settings' do
15
9
  it 'must be set to ISO 8601' do