torque-postgresql 3.4.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/torque/function_generator.rb +13 -0
  3. data/lib/generators/torque/templates/function.sql.erb +4 -0
  4. data/lib/generators/torque/templates/type.sql.erb +2 -0
  5. data/lib/generators/torque/templates/view.sql.erb +3 -0
  6. data/lib/generators/torque/type_generator.rb +13 -0
  7. data/lib/generators/torque/view_generator.rb +16 -0
  8. data/lib/torque/postgresql/adapter/database_statements.rb +111 -94
  9. data/lib/torque/postgresql/adapter/oid/array.rb +17 -0
  10. data/lib/torque/postgresql/adapter/oid/line.rb +2 -6
  11. data/lib/torque/postgresql/adapter/oid/range.rb +4 -4
  12. data/lib/torque/postgresql/adapter/oid.rb +1 -23
  13. data/lib/torque/postgresql/adapter/quoting.rb +13 -7
  14. data/lib/torque/postgresql/adapter/schema_creation.rb +7 -28
  15. data/lib/torque/postgresql/adapter/schema_definitions.rb +58 -0
  16. data/lib/torque/postgresql/adapter/schema_dumper.rb +136 -34
  17. data/lib/torque/postgresql/adapter/schema_overrides.rb +45 -0
  18. data/lib/torque/postgresql/adapter/schema_statements.rb +109 -49
  19. data/lib/torque/postgresql/arel/infix_operation.rb +15 -28
  20. data/lib/torque/postgresql/arel/nodes.rb +16 -2
  21. data/lib/torque/postgresql/arel/operations.rb +7 -1
  22. data/lib/torque/postgresql/arel/visitors.rb +7 -9
  23. data/lib/torque/postgresql/associations/association_scope.rb +23 -31
  24. data/lib/torque/postgresql/associations/belongs_to_many_association.rb +25 -0
  25. data/lib/torque/postgresql/associations/builder/belongs_to_many.rb +16 -0
  26. data/lib/torque/postgresql/attributes/builder/enum.rb +12 -9
  27. data/lib/torque/postgresql/attributes/builder/full_text_search.rb +109 -0
  28. data/lib/torque/postgresql/attributes/builder/period.rb +21 -21
  29. data/lib/torque/postgresql/attributes/builder.rb +49 -11
  30. data/lib/torque/postgresql/attributes/enum.rb +7 -7
  31. data/lib/torque/postgresql/attributes/enum_set.rb +7 -7
  32. data/lib/torque/postgresql/attributes/full_text_search.rb +19 -0
  33. data/lib/torque/postgresql/attributes/period.rb +2 -2
  34. data/lib/torque/postgresql/attributes.rb +0 -4
  35. data/lib/torque/postgresql/auxiliary_statement/recursive.rb +3 -3
  36. data/lib/torque/postgresql/base.rb +5 -11
  37. data/lib/torque/postgresql/collector.rb +1 -1
  38. data/lib/torque/postgresql/config.rb +129 -5
  39. data/lib/torque/postgresql/function.rb +94 -0
  40. data/lib/torque/postgresql/inheritance.rb +52 -36
  41. data/lib/torque/postgresql/predicate_builder/arel_attribute_handler.rb +33 -0
  42. data/lib/torque/postgresql/predicate_builder/array_handler.rb +47 -0
  43. data/lib/torque/postgresql/predicate_builder/enumerator_lazy_handler.rb +37 -0
  44. data/lib/torque/postgresql/predicate_builder/regexp_handler.rb +21 -0
  45. data/lib/torque/postgresql/predicate_builder.rb +35 -0
  46. data/lib/torque/postgresql/railtie.rb +137 -30
  47. data/lib/torque/postgresql/reflection/abstract_reflection.rb +12 -44
  48. data/lib/torque/postgresql/reflection/belongs_to_many_reflection.rb +4 -0
  49. data/lib/torque/postgresql/reflection/has_many_reflection.rb +4 -0
  50. data/lib/torque/postgresql/reflection/runtime_reflection.rb +1 -1
  51. data/lib/torque/postgresql/relation/auxiliary_statement.rb +7 -2
  52. data/lib/torque/postgresql/relation/buckets.rb +124 -0
  53. data/lib/torque/postgresql/relation/distinct_on.rb +7 -2
  54. data/lib/torque/postgresql/relation/inheritance.rb +22 -15
  55. data/lib/torque/postgresql/relation/join_series.rb +112 -0
  56. data/lib/torque/postgresql/relation/merger.rb +17 -3
  57. data/lib/torque/postgresql/relation.rb +24 -38
  58. data/lib/torque/postgresql/schema_cache.rb +6 -12
  59. data/lib/torque/postgresql/version.rb +1 -1
  60. data/lib/torque/postgresql/versioned_commands/command_migration.rb +146 -0
  61. data/lib/torque/postgresql/versioned_commands/generator.rb +57 -0
  62. data/lib/torque/postgresql/versioned_commands/migration_context.rb +83 -0
  63. data/lib/torque/postgresql/versioned_commands/migrator.rb +39 -0
  64. data/lib/torque/postgresql/versioned_commands/schema_table.rb +101 -0
  65. data/lib/torque/postgresql/versioned_commands.rb +161 -0
  66. data/lib/torque/postgresql.rb +2 -1
  67. data/spec/fixtures/migrations/20250101000001_create_users.rb +0 -0
  68. data/spec/fixtures/migrations/20250101000002_create_function_count_users_v1.sql +0 -0
  69. data/spec/fixtures/migrations/20250101000003_create_internal_users.rb +0 -0
  70. data/spec/fixtures/migrations/20250101000004_update_function_count_users_v2.sql +0 -0
  71. data/spec/fixtures/migrations/20250101000005_create_view_all_users_v1.sql +0 -0
  72. data/spec/fixtures/migrations/20250101000006_create_type_user_id_v1.sql +0 -0
  73. data/spec/fixtures/migrations/20250101000007_remove_function_count_users_v2.sql +0 -0
  74. data/spec/initialize.rb +67 -0
  75. data/spec/mocks/cache_query.rb +21 -21
  76. data/spec/mocks/create_table.rb +6 -26
  77. data/spec/schema.rb +17 -12
  78. data/spec/spec_helper.rb +11 -2
  79. data/spec/tests/arel_spec.rb +32 -7
  80. data/spec/tests/auxiliary_statement_spec.rb +3 -3
  81. data/spec/tests/belongs_to_many_spec.rb +72 -5
  82. data/spec/tests/enum_set_spec.rb +12 -11
  83. data/spec/tests/enum_spec.rb +4 -2
  84. data/spec/tests/full_text_seach_test.rb +280 -0
  85. data/spec/tests/function_spec.rb +42 -0
  86. data/spec/tests/has_many_spec.rb +21 -8
  87. data/spec/tests/interval_spec.rb +1 -7
  88. data/spec/tests/period_spec.rb +61 -61
  89. data/spec/tests/predicate_builder_spec.rb +132 -0
  90. data/spec/tests/relation_spec.rb +229 -0
  91. data/spec/tests/schema_spec.rb +6 -9
  92. data/spec/tests/table_inheritance_spec.rb +25 -26
  93. data/spec/tests/versioned_commands_spec.rb +513 -0
  94. metadata +64 -39
@@ -3,9 +3,9 @@ require 'spec_helper'
3
3
  RSpec.describe 'BelongsToMany' do
4
4
  context 'on model' do
5
5
  let(:model) { Video }
6
+ let(:key) { :tests }
6
7
  let(:builder) { Torque::PostgreSQL::Associations::Builder::BelongsToMany }
7
8
  let(:reflection) { Torque::PostgreSQL::Reflection::BelongsToManyReflection }
8
- let(:key) { Torque::PostgreSQL::AR720 ? :tests : 'tests' }
9
9
 
10
10
  after { model._reflections = {} }
11
11
 
@@ -34,8 +34,8 @@ RSpec.describe 'BelongsToMany' do
34
34
 
35
35
  context 'on association' do
36
36
  let(:other) { Tag }
37
+ let(:key) { :tags }
37
38
  let(:initial) { FactoryBot.create(:tag) }
38
- let(:key) { Torque::PostgreSQL::AR720 ? :tags : 'tags' }
39
39
 
40
40
  before { Video.belongs_to_many(:tags) }
41
41
  subject { Video.create(title: 'A') }
@@ -58,7 +58,7 @@ RSpec.describe 'BelongsToMany' do
58
58
  it 'loads associated records' do
59
59
  subject.update(tag_ids: [initial.id])
60
60
  expect(subject.tags.to_sql).to be_eql(<<-SQL.squish)
61
- SELECT "tags".* FROM "tags" WHERE "tags"."id" IN (#{initial.id})
61
+ SELECT "tags".* FROM "tags" WHERE "tags"."id" = #{initial.id}
62
62
  SQL
63
63
 
64
64
  expect(subject.tags.load).to be_a(ActiveRecord::Associations::CollectionProxy)
@@ -370,6 +370,18 @@ RSpec.describe 'BelongsToMany' do
370
370
  expect { query.load }.not_to raise_error
371
371
  end
372
372
 
373
+ context 'when handling binds' do
374
+ let(:tag_ids) { FactoryBot.create_list(:tag, 5).map(&:id) }
375
+ let!(:record) { Video.new(tag_ids: tag_ids) }
376
+
377
+ it 'uses rails default with in and several binds' do
378
+ sql, binds = get_query_with_binds { record.tags.load }
379
+
380
+ expect(sql).to include(' WHERE "tags"."id" IN ($1, $2, $3, $4, $5)')
381
+ expect(binds.size).to be_eql(5)
382
+ end
383
+ end
384
+
373
385
  context 'when the attribute has a default value' do
374
386
  subject { FactoryBot.create(:item) }
375
387
 
@@ -424,16 +436,28 @@ RSpec.describe 'BelongsToMany' do
424
436
 
425
437
  subject { game.create }
426
438
 
427
- it 'loads associated records' do
439
+ it 'loads one associated records' do
428
440
  subject.update(player_ids: [other.id])
429
441
  expect(subject.players.to_sql).to be_eql(<<-SQL.squish)
430
- SELECT "players".* FROM "players" WHERE "players"."id" IN ('#{other.id}')
442
+ SELECT "players".* FROM "players" WHERE "players"."id" = '#{other.id}'
431
443
  SQL
432
444
 
433
445
  expect(subject.players.load).to be_a(ActiveRecord::Associations::CollectionProxy)
434
446
  expect(subject.players.to_a).to be_eql([other])
435
447
  end
436
448
 
449
+ it 'loads several associated records' do
450
+ entries = [other, player.create]
451
+ subject.update(player_ids: entries.map(&:id))
452
+ expect(subject.players.to_sql).to be_eql(<<-SQL.squish)
453
+ SELECT "players".* FROM "players"
454
+ WHERE "players"."id" IN ('#{entries[0].id}', '#{entries[1].id}')
455
+ SQL
456
+
457
+ expect(subject.players.load).to be_a(ActiveRecord::Associations::CollectionProxy)
458
+ expect(subject.players.to_a).to be_eql(entries)
459
+ end
460
+
437
461
  it 'can preload records' do
438
462
  records = 5.times.map { player.create }
439
463
  subject.players.concat(records)
@@ -452,6 +476,49 @@ RSpec.describe 'BelongsToMany' do
452
476
  end
453
477
  end
454
478
 
479
+ context 'using callbacks' do
480
+ let(:tags) { FactoryBot.create_list(:tag, 3) }
481
+ let(:collectors) { Hash.new { |h, k| h[k] = [] } }
482
+
483
+ subject { Video.create(title: 'A') }
484
+
485
+ after do
486
+ Video.reset_callbacks(:save)
487
+ Video._reflections = {}
488
+ end
489
+
490
+ before do
491
+ subject.update_attribute(:tag_ids, tags.first(2).pluck(:id))
492
+ Video.belongs_to_many(:tags,
493
+ before_add: ->(_, tag) { collectors[:before_add] << tag },
494
+ after_add: ->(_, tag) { collectors[:after_add] << tag },
495
+ before_remove: ->(_, tag) { collectors[:before_remove] << tag },
496
+ after_remove: ->(_, tag) { collectors[:after_remove] << tag },
497
+ )
498
+ end
499
+
500
+ it 'works with id changes' do
501
+ subject.tag_ids = tags.drop(1).pluck(:id)
502
+ subject.save!
503
+
504
+ expect(collectors[:before_add]).to be_eql([tags.last])
505
+ expect(collectors[:after_add]).to be_eql([tags.last])
506
+
507
+ expect(collectors[:before_remove]).to be_eql([tags.first])
508
+ expect(collectors[:after_remove]).to be_eql([tags.first])
509
+ end
510
+
511
+ it 'works with record changes' do
512
+ subject.tags = tags.drop(1)
513
+
514
+ expect(collectors[:before_add]).to be_eql([tags.last])
515
+ expect(collectors[:after_add]).to be_eql([tags.last])
516
+
517
+ expect(collectors[:before_remove]).to be_eql([tags.first])
518
+ expect(collectors[:after_remove]).to be_eql([tags.first])
519
+ end
520
+ end
521
+
455
522
  context 'using custom keys' do
456
523
  let(:connection) { ActiveRecord::Base.connection }
457
524
  let(:post) { Post }
@@ -43,13 +43,7 @@ RSpec.describe 'Enum' do
43
43
  end
44
44
 
45
45
  context 'on schema' do
46
- let(:source) do
47
- if Torque::PostgreSQL::AR720
48
- ActiveRecord::Base.connection_pool
49
- else
50
- ActiveRecord::Base.connection
51
- end
52
- end
46
+ let(:source) { ActiveRecord::Base.connection_pool }
53
47
 
54
48
  let(:dump_result) do
55
49
  ActiveRecord::SchemaDumper.dump(source, (dump_result = StringIO.new))
@@ -275,10 +269,11 @@ RSpec.describe 'Enum' do
275
269
  end
276
270
 
277
271
  context 'on model' do
272
+ let(:instance) { Course.new }
273
+
278
274
  before(:each) { decorate(Course, :types) }
279
275
 
280
276
  subject { Course }
281
- let(:instance) { Course.new }
282
277
 
283
278
  it 'has all enum set methods' do
284
279
  expect(subject).to respond_to(:types)
@@ -301,17 +296,23 @@ RSpec.describe 'Enum' do
301
296
 
302
297
  it 'scope the model correctly' do
303
298
  query = subject.a.to_sql
304
- expect(query).to match(/"courses"."types" @> ARRAY\['A'\]::types\[\]/)
299
+ expect(query).to include(%{WHERE "courses"."types" @> '{A}'::types[]})
305
300
  end
306
301
 
307
302
  it 'has a match all scope' do
308
303
  query = subject.has_types('B', 'A').to_sql
309
- expect(query).to match(/"courses"."types" @> ARRAY\['B', 'A'\]::types\[\]/)
304
+ expect(query).to include(%{WHERE "courses"."types" @> '{B,A}'::types[]})
310
305
  end
311
306
 
312
307
  it 'has a match any scope' do
313
308
  query = subject.has_any_types('B', 'A').to_sql
314
- expect(query).to match(/"courses"."types" && ARRAY\['B', 'A'\]::types\[\]/)
309
+ expect(query).to include(%{WHERE "courses"."types" && '{B,A}'::types[]})
310
+ end
311
+
312
+ it 'uses bind param instead of raw value' do
313
+ sql, binds = get_query_with_binds { subject.has_any_types('B', 'A').load }
314
+ expect(sql).to include('WHERE "courses"."types" && $1::types[]')
315
+ expect(binds.first.value).to eq(%w[B A])
315
316
  end
316
317
  end
317
318
  end
@@ -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,280 @@
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 default query mode' do
219
+ result = Course.full_text_search('test', mode: :default)
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 plain query mode' do
227
+ result = Course.full_text_search('test', mode: :plain)
228
+ parts = 'SELECT "courses".* FROM "courses"'
229
+ parts << ' WHERE "courses"."search_vector" @@'
230
+ parts << " PLAINTO_TSQUERY('english', 'test')"
231
+ expect(result.to_sql).to eql(parts)
232
+ end
233
+
234
+ it 'can use web query mode' do
235
+ result = Course.full_text_search('test', mode: :web)
236
+ parts = 'SELECT "courses".* FROM "courses"'
237
+ parts << ' WHERE "courses"."search_vector" @@'
238
+ parts << " WEBSEARCH_TO_TSQUERY('english', 'test')"
239
+ expect(result.to_sql).to eql(parts)
240
+ end
241
+
242
+ it 'can use a attribute as the language' do
243
+ result = Course.full_text_search('test', language: :lang)
244
+ parts = 'SELECT "courses".* FROM "courses"'
245
+ parts << ' WHERE "courses"."search_vector" @@'
246
+ parts << %{ PHRASETO_TSQUERY("courses"."lang", 'test')}
247
+ expect(result.to_sql).to eql(parts)
248
+ end
249
+
250
+ it 'can call a method to pull the language' do
251
+ Course.define_singleton_method(:search_language) { 'portuguese' }
252
+ result = Course.full_text_search('test', language: :search_language)
253
+ parts = 'SELECT "courses".* FROM "courses"'
254
+ parts << ' WHERE "courses"."search_vector" @@'
255
+ parts << " PHRASETO_TSQUERY('portuguese', 'test')"
256
+ expect(result.to_sql).to eql(parts)
257
+ Course.singleton_class.undef_method(:search_language)
258
+ end
259
+
260
+ it 'properly binds all provided values' do
261
+ query = Course.full_text_search('test')
262
+ sql, binds = get_query_with_binds { query.load }
263
+ expect(sql).to include("PHRASETO_TSQUERY($1, $2)")
264
+ expect(binds.first.value).to eq('english')
265
+ expect(binds.second.value).to eq('test')
266
+ end
267
+
268
+ it 'raises an error when the language is not found' do
269
+ expect do
270
+ Course.full_text_search('test', language: '')
271
+ end.to raise_error(ArgumentError, /Unable to determine language/)
272
+ end
273
+
274
+ it 'raises an error when the mode is invalid' do
275
+ expect do
276
+ Course.full_text_search('test', mode: :invalid)
277
+ end.to raise_error(ArgumentError, /Invalid mode :invalid for full text search/)
278
+ end
279
+ end
280
+ 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