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.
- checksums.yaml +4 -4
- data/lib/generators/torque/function_generator.rb +13 -0
- data/lib/generators/torque/templates/function.sql.erb +4 -0
- data/lib/generators/torque/templates/type.sql.erb +2 -0
- data/lib/generators/torque/templates/view.sql.erb +3 -0
- data/lib/generators/torque/type_generator.rb +13 -0
- data/lib/generators/torque/view_generator.rb +16 -0
- data/lib/torque/postgresql/adapter/database_statements.rb +111 -94
- data/lib/torque/postgresql/adapter/oid/array.rb +17 -0
- data/lib/torque/postgresql/adapter/oid/line.rb +2 -6
- data/lib/torque/postgresql/adapter/oid/range.rb +4 -4
- data/lib/torque/postgresql/adapter/oid.rb +1 -23
- data/lib/torque/postgresql/adapter/quoting.rb +13 -7
- data/lib/torque/postgresql/adapter/schema_creation.rb +7 -28
- data/lib/torque/postgresql/adapter/schema_definitions.rb +58 -0
- data/lib/torque/postgresql/adapter/schema_dumper.rb +136 -34
- data/lib/torque/postgresql/adapter/schema_overrides.rb +45 -0
- data/lib/torque/postgresql/adapter/schema_statements.rb +109 -49
- data/lib/torque/postgresql/arel/infix_operation.rb +15 -28
- data/lib/torque/postgresql/arel/nodes.rb +16 -2
- data/lib/torque/postgresql/arel/operations.rb +7 -1
- data/lib/torque/postgresql/arel/visitors.rb +7 -9
- data/lib/torque/postgresql/associations/association_scope.rb +23 -31
- data/lib/torque/postgresql/associations/belongs_to_many_association.rb +25 -0
- data/lib/torque/postgresql/associations/builder/belongs_to_many.rb +16 -0
- data/lib/torque/postgresql/attributes/builder/enum.rb +12 -9
- data/lib/torque/postgresql/attributes/builder/full_text_search.rb +109 -0
- data/lib/torque/postgresql/attributes/builder/period.rb +21 -21
- data/lib/torque/postgresql/attributes/builder.rb +49 -11
- data/lib/torque/postgresql/attributes/enum.rb +7 -7
- data/lib/torque/postgresql/attributes/enum_set.rb +7 -7
- data/lib/torque/postgresql/attributes/full_text_search.rb +19 -0
- data/lib/torque/postgresql/attributes/period.rb +2 -2
- data/lib/torque/postgresql/attributes.rb +0 -4
- data/lib/torque/postgresql/auxiliary_statement/recursive.rb +3 -3
- data/lib/torque/postgresql/base.rb +5 -11
- data/lib/torque/postgresql/collector.rb +1 -1
- data/lib/torque/postgresql/config.rb +129 -5
- data/lib/torque/postgresql/function.rb +94 -0
- data/lib/torque/postgresql/inheritance.rb +52 -36
- data/lib/torque/postgresql/predicate_builder/arel_attribute_handler.rb +33 -0
- data/lib/torque/postgresql/predicate_builder/array_handler.rb +47 -0
- data/lib/torque/postgresql/predicate_builder/enumerator_lazy_handler.rb +37 -0
- data/lib/torque/postgresql/predicate_builder/regexp_handler.rb +21 -0
- data/lib/torque/postgresql/predicate_builder.rb +35 -0
- data/lib/torque/postgresql/railtie.rb +137 -30
- data/lib/torque/postgresql/reflection/abstract_reflection.rb +12 -44
- data/lib/torque/postgresql/reflection/belongs_to_many_reflection.rb +4 -0
- data/lib/torque/postgresql/reflection/has_many_reflection.rb +4 -0
- data/lib/torque/postgresql/reflection/runtime_reflection.rb +1 -1
- data/lib/torque/postgresql/relation/auxiliary_statement.rb +7 -2
- data/lib/torque/postgresql/relation/buckets.rb +124 -0
- data/lib/torque/postgresql/relation/distinct_on.rb +7 -2
- data/lib/torque/postgresql/relation/inheritance.rb +22 -15
- data/lib/torque/postgresql/relation/join_series.rb +112 -0
- data/lib/torque/postgresql/relation/merger.rb +17 -3
- data/lib/torque/postgresql/relation.rb +24 -38
- data/lib/torque/postgresql/schema_cache.rb +6 -12
- data/lib/torque/postgresql/version.rb +1 -1
- data/lib/torque/postgresql/versioned_commands/command_migration.rb +146 -0
- data/lib/torque/postgresql/versioned_commands/generator.rb +57 -0
- data/lib/torque/postgresql/versioned_commands/migration_context.rb +83 -0
- data/lib/torque/postgresql/versioned_commands/migrator.rb +39 -0
- data/lib/torque/postgresql/versioned_commands/schema_table.rb +101 -0
- data/lib/torque/postgresql/versioned_commands.rb +161 -0
- data/lib/torque/postgresql.rb +2 -1
- data/spec/fixtures/migrations/20250101000001_create_users.rb +0 -0
- data/spec/fixtures/migrations/20250101000002_create_function_count_users_v1.sql +0 -0
- data/spec/fixtures/migrations/20250101000003_create_internal_users.rb +0 -0
- data/spec/fixtures/migrations/20250101000004_update_function_count_users_v2.sql +0 -0
- data/spec/fixtures/migrations/20250101000005_create_view_all_users_v1.sql +0 -0
- data/spec/fixtures/migrations/20250101000006_create_type_user_id_v1.sql +0 -0
- data/spec/fixtures/migrations/20250101000007_remove_function_count_users_v2.sql +0 -0
- data/spec/initialize.rb +67 -0
- data/spec/mocks/cache_query.rb +21 -21
- data/spec/mocks/create_table.rb +6 -26
- data/spec/schema.rb +17 -12
- data/spec/spec_helper.rb +11 -2
- data/spec/tests/arel_spec.rb +32 -7
- data/spec/tests/auxiliary_statement_spec.rb +3 -3
- data/spec/tests/belongs_to_many_spec.rb +72 -5
- data/spec/tests/enum_set_spec.rb +12 -11
- data/spec/tests/enum_spec.rb +4 -2
- data/spec/tests/full_text_seach_test.rb +280 -0
- data/spec/tests/function_spec.rb +42 -0
- data/spec/tests/has_many_spec.rb +21 -8
- data/spec/tests/interval_spec.rb +1 -7
- data/spec/tests/period_spec.rb +61 -61
- data/spec/tests/predicate_builder_spec.rb +132 -0
- data/spec/tests/relation_spec.rb +229 -0
- data/spec/tests/schema_spec.rb +6 -9
- data/spec/tests/table_inheritance_spec.rb +25 -26
- data/spec/tests/versioned_commands_spec.rb +513 -0
- 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"
|
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"
|
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 }
|
data/spec/tests/enum_set_spec.rb
CHANGED
@@ -43,13 +43,7 @@ RSpec.describe 'Enum' do
|
|
43
43
|
end
|
44
44
|
|
45
45
|
context 'on schema' do
|
46
|
-
let(:source)
|
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
|
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
|
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
|
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
|
data/spec/tests/enum_spec.rb
CHANGED
@@ -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
|
data/spec/tests/has_many_spec.rb
CHANGED
@@ -11,7 +11,7 @@ RSpec.describe 'HasMany' do
|
|
11
11
|
|
12
12
|
context 'on original' do
|
13
13
|
let(:other) { Text }
|
14
|
-
let(:key) {
|
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) {
|
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
|
268
|
-
SELECT "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
|
478
|
-
SELECT "games"
|
479
|
-
WHERE
|
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
|
data/spec/tests/interval_spec.rb
CHANGED
@@ -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)
|
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
|