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
|