torque-postgresql 4.1.0 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Rakefile +28 -0
- data/lib/generators/torque/skill_generator.rb +17 -0
- data/lib/generators/torque/templates/trigger.sql.erb +3 -0
- data/lib/generators/torque/trigger_generator.rb +13 -0
- data/lib/torque/postgresql/adapter/database_statements.rb +9 -0
- data/lib/torque/postgresql/adapter/oid/array.rb +0 -4
- data/lib/torque/postgresql/adapter/oid/composite.rb +2 -1
- data/lib/torque/postgresql/adapter/oid/enum.rb +1 -1
- data/lib/torque/postgresql/adapter/oid/enum_set.rb +2 -2
- data/lib/torque/postgresql/adapter/oid/interval.rb +1 -1
- data/lib/torque/postgresql/adapter/oid/lquery.rb +45 -28
- data/lib/torque/postgresql/adapter/oid/ltree.rb +67 -9
- data/lib/torque/postgresql/adapter/oid/struct.rb +1 -1
- data/lib/torque/postgresql/adapter/oid.rb +13 -0
- data/lib/torque/postgresql/adapter/schema_definitions.rb +5 -0
- data/lib/torque/postgresql/adapter/schema_dumper.rb +1 -0
- data/lib/torque/postgresql/arel/nodes.rb +51 -0
- data/lib/torque/postgresql/arel/visitors.rb +46 -0
- data/lib/torque/postgresql/attributes/base.rb +10 -7
- data/lib/torque/postgresql/attributes/composite.rb +1 -5
- data/lib/torque/postgresql/attributes/enum.rb +1 -6
- data/lib/torque/postgresql/attributes/lquery.rb +67 -127
- data/lib/torque/postgresql/attributes/ltree.rb +35 -83
- data/lib/torque/postgresql/base.rb +9 -1
- data/lib/torque/postgresql/config.rb +21 -3
- data/lib/torque/postgresql/geometry_builder.rb +4 -10
- data/lib/torque/postgresql/inheritance.rb +5 -5
- data/lib/torque/postgresql/predicate_builder/arel_attribute_handler.rb +1 -1
- data/lib/torque/postgresql/predicate_builder/array_handler.rb +16 -8
- data/lib/torque/postgresql/predicate_builder/composite_handler.rb +18 -5
- data/lib/torque/postgresql/predicate_builder/ltree_handler.rb +86 -18
- data/lib/torque/postgresql/predicate_builder/struct_handler.rb +29 -9
- data/lib/torque/postgresql/predicate_builder.rb +38 -11
- data/lib/torque/postgresql/railtie.rb +17 -0
- data/lib/torque/postgresql/relation/auxiliary_statement.rb +7 -3
- data/lib/torque/postgresql/relation/buckets.rb +2 -1
- data/lib/torque/postgresql/relation/cross_join.rb +193 -0
- data/lib/torque/postgresql/relation/inheritance.rb +1 -2
- data/lib/torque/postgresql/relation/join_series.rb +1 -0
- data/lib/torque/postgresql/relation/merger.rb +9 -0
- data/lib/torque/postgresql/relation/segments.rb +140 -0
- data/lib/torque/postgresql/relation.rb +23 -4
- data/lib/torque/postgresql/version.rb +1 -1
- data/lib/torque/postgresql/versioned_commands/command_migration.rb +23 -0
- data/lib/torque/postgresql/versioned_commands.rb +39 -0
- data/lib/torque/postgresql.rb +0 -2
- data/sig/generators/torque/function_generator.rbs +9 -0
- data/sig/generators/torque/skill_generator.rbs +7 -0
- data/sig/generators/torque/trigger_generator.rbs +9 -0
- data/sig/generators/torque/type_generator.rbs +9 -0
- data/sig/generators/torque/view_generator.rbs +9 -0
- data/sig/manifest.yaml +7 -0
- data/sig/torque/postgresql/adapter/database_statements.rbs +42 -0
- data/sig/torque/postgresql/adapter/inheritance_statements.rbs +36 -0
- data/sig/torque/postgresql/adapter/oid/array.rbs +20 -0
- data/sig/torque/postgresql/adapter/oid/box.rbs +28 -0
- data/sig/torque/postgresql/adapter/oid/circle.rbs +33 -0
- data/sig/torque/postgresql/adapter/oid/composite.rbs +35 -0
- data/sig/torque/postgresql/adapter/oid/enum.rbs +28 -0
- data/sig/torque/postgresql/adapter/oid/enum_set.rbs +19 -0
- data/sig/torque/postgresql/adapter/oid/interval.rbs +20 -0
- data/sig/torque/postgresql/adapter/oid/line.rbs +33 -0
- data/sig/torque/postgresql/adapter/oid/lquery.rbs +21 -0
- data/sig/torque/postgresql/adapter/oid/ltree.rbs +32 -0
- data/sig/torque/postgresql/adapter/oid/range.rbs +28 -0
- data/sig/torque/postgresql/adapter/oid/segment.rbs +38 -0
- data/sig/torque/postgresql/adapter/oid/struct.rbs +41 -0
- data/sig/torque/postgresql/adapter/oid/struct_list.rbs +20 -0
- data/sig/torque/postgresql/adapter/oid/struct_set.rbs +18 -0
- data/sig/torque/postgresql/adapter/oid.rbs +9 -0
- data/sig/torque/postgresql/adapter/quoting.rbs +20 -0
- data/sig/torque/postgresql/adapter/schema_creation.rbs +13 -0
- data/sig/torque/postgresql/adapter/schema_definitions.rbs +55 -0
- data/sig/torque/postgresql/adapter/schema_dumper.rbs +37 -0
- data/sig/torque/postgresql/adapter/schema_overrides.rbs +46 -0
- data/sig/torque/postgresql/adapter/schema_statements.rbs +50 -0
- data/sig/torque/postgresql/adapter.rbs +24 -0
- data/sig/torque/postgresql/arel/infix_operation.rbs +79 -0
- data/sig/torque/postgresql/arel/join_source.rbs +19 -0
- data/sig/torque/postgresql/arel/nodes.rbs +83 -0
- data/sig/torque/postgresql/arel/operations.rbs +28 -0
- data/sig/torque/postgresql/arel/select_manager.rbs +15 -0
- data/sig/torque/postgresql/arel/visitors.rbs +30 -0
- data/sig/torque/postgresql/arel.rbs +6 -0
- data/sig/torque/postgresql/associations/association_scope.rbs +28 -0
- data/sig/torque/postgresql/associations/belongs_to_many_association.rbs +51 -0
- data/sig/torque/postgresql/associations/builder/belongs_to_many.rbs +24 -0
- data/sig/torque/postgresql/associations/builder/has_many.rbs +15 -0
- data/sig/torque/postgresql/associations/builder.rbs +8 -0
- data/sig/torque/postgresql/associations/foreign_association.rbs +16 -0
- data/sig/torque/postgresql/associations/join_dependency.rbs +16 -0
- data/sig/torque/postgresql/associations/preloader/association.rbs +32 -0
- data/sig/torque/postgresql/associations/preloader/loader_query.rbs +20 -0
- data/sig/torque/postgresql/associations/preloader.rbs +8 -0
- data/sig/torque/postgresql/associations.rbs +17 -0
- data/sig/torque/postgresql/attributes/base.rbs +37 -0
- data/sig/torque/postgresql/attributes/builder/enum.rbs +38 -0
- data/sig/torque/postgresql/attributes/builder/full_text_search.rbs +27 -0
- data/sig/torque/postgresql/attributes/builder/period.rbs +106 -0
- data/sig/torque/postgresql/attributes/builder.rbs +19 -0
- data/sig/torque/postgresql/attributes/composite.rbs +28 -0
- data/sig/torque/postgresql/attributes/enum.rbs +69 -0
- data/sig/torque/postgresql/attributes/enum_set.rbs +75 -0
- data/sig/torque/postgresql/attributes/full_text_search.rbs +13 -0
- data/sig/torque/postgresql/attributes/lazy.rbs +17 -0
- data/sig/torque/postgresql/attributes/lquery.rbs +48 -0
- data/sig/torque/postgresql/attributes/ltree.rbs +46 -0
- data/sig/torque/postgresql/attributes/period.rbs +13 -0
- data/sig/torque/postgresql/attributes/simple_enum.rbs +39 -0
- data/sig/torque/postgresql/attributes/struct.rbs +38 -0
- data/sig/torque/postgresql/attributes.rbs +6 -0
- data/sig/torque/postgresql/autosave_association.rbs +16 -0
- data/sig/torque/postgresql/auxiliary_statement/recursive.rbs +22 -0
- data/sig/torque/postgresql/auxiliary_statement/settings.rbs +63 -0
- data/sig/torque/postgresql/auxiliary_statement.rbs +57 -0
- data/sig/torque/postgresql/base.rbs +41 -0
- data/sig/torque/postgresql/collector.rbs +7 -0
- data/sig/torque/postgresql/config.rbs +9 -0
- data/sig/torque/postgresql/function.rbs +21 -0
- data/sig/torque/postgresql/geometry_builder.rbs +20 -0
- data/sig/torque/postgresql/i18n.rbs +15 -0
- data/sig/torque/postgresql/inheritance/expander.rbs +23 -0
- data/sig/torque/postgresql/inheritance/record.rbs +20 -0
- data/sig/torque/postgresql/inheritance.rbs +47 -0
- data/sig/torque/postgresql/insert_all.rbs +22 -0
- data/sig/torque/postgresql/migration/command_recorder.rbs +30 -0
- data/sig/torque/postgresql/migration.rbs +6 -0
- data/sig/torque/postgresql/predicate_builder/arel_attribute_handler.rbs +15 -0
- data/sig/torque/postgresql/predicate_builder/array_handler.rbs +26 -0
- data/sig/torque/postgresql/predicate_builder/composite_handler.rbs +26 -0
- data/sig/torque/postgresql/predicate_builder/enumerator_lazy_handler.rbs +18 -0
- data/sig/torque/postgresql/predicate_builder/ltree_handler.rbs +36 -0
- data/sig/torque/postgresql/predicate_builder/regexp_handler.rbs +16 -0
- data/sig/torque/postgresql/predicate_builder/struct_handler.rbs +24 -0
- data/sig/torque/postgresql/predicate_builder.rbs +23 -0
- data/sig/torque/postgresql/predicate_table.rbs +24 -0
- data/sig/torque/postgresql/railtie.rbs +10 -0
- data/sig/torque/postgresql/reflection/abstract_reflection.rbs +22 -0
- data/sig/torque/postgresql/reflection/association_reflection.rbs +18 -0
- data/sig/torque/postgresql/reflection/belongs_to_many_reflection.rbs +30 -0
- data/sig/torque/postgresql/reflection/has_many_reflection.rbs +14 -0
- data/sig/torque/postgresql/reflection/runtime_reflection.rbs +18 -0
- data/sig/torque/postgresql/reflection/through_reflection.rbs +14 -0
- data/sig/torque/postgresql/reflection.rbs +11 -0
- data/sig/torque/postgresql/relation/auxiliary_statement.rbs +29 -0
- data/sig/torque/postgresql/relation/buckets.rbs +35 -0
- data/sig/torque/postgresql/relation/cross_join.rbs +63 -0
- data/sig/torque/postgresql/relation/distinct_on.rbs +16 -0
- data/sig/torque/postgresql/relation/inheritance.rbs +48 -0
- data/sig/torque/postgresql/relation/join_series.rbs +28 -0
- data/sig/torque/postgresql/relation/merger.rbs +23 -0
- data/sig/torque/postgresql/relation/segments.rbs +27 -0
- data/sig/torque/postgresql/relation.rbs +48 -0
- data/sig/torque/postgresql/schema_cache/bound_schema_reflection.rbs +14 -0
- data/sig/torque/postgresql/schema_cache/inheritance.rbs +12 -0
- data/sig/torque/postgresql/schema_cache/schema_reflection.rbs +14 -0
- data/sig/torque/postgresql/schema_cache.rbs +43 -0
- data/sig/torque/postgresql/table_name.rbs +20 -0
- data/sig/torque/postgresql/validations.rbs +13 -0
- data/sig/torque/postgresql/version.rbs +5 -0
- data/sig/torque/postgresql/versioned_commands/command_migration.rbs +54 -0
- data/sig/torque/postgresql/versioned_commands/generator.rbs +17 -0
- data/sig/torque/postgresql/versioned_commands/migration_context.rbs +30 -0
- data/sig/torque/postgresql/versioned_commands/migrator.rbs +19 -0
- data/sig/torque/postgresql/versioned_commands/schema_table.rbs +23 -0
- data/sig/torque/postgresql/versioned_commands.rbs +25 -0
- data/sig/torque/postgresql.rbs +26 -0
- data/skills/torque-postgresql/SKILL.md +70 -0
- data/spec/fixtures/migrations/20250101000008_create_trigger_audit_users_v1.sql +3 -0
- data/spec/initialize.rb +9 -0
- data/spec/mocks/cache_query.rb +1 -1
- data/spec/rbs_helper.rb +8 -0
- data/spec/schema.rb +2 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/tests/arel_spec.rb +1 -3
- data/spec/tests/auxiliary_statement_spec.rb +12 -0
- data/spec/tests/buckets_spec.rb +122 -0
- data/spec/tests/composite_spec.rb +67 -0
- data/spec/tests/cross_join_spec.rb +185 -0
- data/spec/tests/distinct_on_spec.rb +5 -0
- data/spec/tests/enum_set_spec.rb +4 -0
- data/spec/tests/enum_spec.rb +20 -4
- data/spec/tests/geometric_builder_spec.rb +16 -5
- data/spec/tests/has_many_spec.rb +2 -1
- data/spec/tests/interval_spec.rb +6 -0
- data/spec/tests/ltree_spec.rb +398 -64
- data/spec/tests/predicate_builder_spec.rb +120 -0
- data/spec/tests/relation_spec.rb +25 -69
- data/spec/tests/segments_spec.rb +264 -0
- data/spec/tests/struct_spec.rb +202 -0
- data/spec/tests/table_inheritance_spec.rb +10 -0
- data/spec/tests/versioned_commands_spec.rb +145 -0
- metadata +153 -2
data/spec/tests/arel_spec.rb
CHANGED
|
@@ -56,9 +56,7 @@ RSpec.describe 'Arel' do
|
|
|
56
56
|
it 'does not break the change column default value method' do
|
|
57
57
|
connection.add_column(:authors, :enabled, :boolean)
|
|
58
58
|
expect { connection.change_column_default(:authors, :enabled, { from: nil, to: true }) }.not_to raise_error
|
|
59
|
-
|
|
60
|
-
expected = Torque::PostgreSQL::AR810 ? true : 'true'
|
|
61
|
-
expect(Author.columns_hash['enabled'].default).to eq(expected)
|
|
59
|
+
expect(Author.new.enabled).to be(true)
|
|
62
60
|
end
|
|
63
61
|
|
|
64
62
|
it 'does not break jsonb' do
|
|
@@ -101,6 +101,18 @@ RSpec.describe 'AuxiliaryStatement' do
|
|
|
101
101
|
expect(query.send(:bound_attributes).map(&:value_before_type_cast)).to eql([1, 2])
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
+
it 'collects the bound attributes without a permanent connection' do
|
|
105
|
+
klass.send(:auxiliary_statement, :comments) do |cte|
|
|
106
|
+
cte.query Comment.where(id: 1)
|
|
107
|
+
cte.attributes content: :comment_content
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
expect(ActiveRecord::Base).not_to receive(:connection)
|
|
111
|
+
|
|
112
|
+
query = subject.where(id: 2).with(:comments)
|
|
113
|
+
expect(query.send(:bound_attributes).map(&:value_before_type_cast)).to eql([1, 2])
|
|
114
|
+
end
|
|
115
|
+
|
|
104
116
|
it 'accepts string as attributes' do
|
|
105
117
|
klass.send(:auxiliary_statement, :comments) do |cte|
|
|
106
118
|
cte.query Comment.all
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe 'Buckets', type: :helper do
|
|
4
|
+
context 'on relation' do
|
|
5
|
+
let(:source) { User.all }
|
|
6
|
+
|
|
7
|
+
it 'produces the right query' do
|
|
8
|
+
query = source.buckets(:age, 0..50, count: 5)
|
|
9
|
+
sql, binds = get_query_with_binds { query.load }
|
|
10
|
+
|
|
11
|
+
expect(sql).to include(<<~SQL.squish)
|
|
12
|
+
WIDTH_BUCKET("users"."age", $1::numeric, $2::numeric, $3::integer) AS bucket
|
|
13
|
+
SQL
|
|
14
|
+
expect(binds.map(&:value)).to eq([0, 50, 5])
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'accepts a part of a column' do
|
|
18
|
+
Profile.create!(name: 'a', settings: { theme: 'dark' })
|
|
19
|
+
Profile.create!(name: 'b', settings: { theme: 'light' })
|
|
20
|
+
|
|
21
|
+
query = Profile.buckets({ settings: :theme }, %w[dark light])
|
|
22
|
+
expect(query.to_sql).to include(<<~SQL.squish)
|
|
23
|
+
WIDTH_BUCKET(("profiles"."settings" #>> ARRAY['theme']), ARRAY['dark', 'light']) AS bucket
|
|
24
|
+
SQL
|
|
25
|
+
|
|
26
|
+
query = Profile.buckets({ settings: :theme }, %w[dark light])
|
|
27
|
+
expect(query.count).to be_eql('dark' => 1, 'light' => 1)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'can query records by buckets' do
|
|
31
|
+
list = [create(:user, age: 5), create(:user, age: 5), create(:user, age: 15)]
|
|
32
|
+
query = source.buckets(:age, 0..50, count: 5).records
|
|
33
|
+
|
|
34
|
+
expect(query).to be_a(Hash)
|
|
35
|
+
expect(query.keys).to match_array([0...10, 10...20])
|
|
36
|
+
expect(query[0...10]).to match_array([list[0], list[1]])
|
|
37
|
+
expect(query[10...20]).to match_array([list[2]])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'can query buckets of roles' do
|
|
41
|
+
list = [create(:user, role: :visitor)]
|
|
42
|
+
list << create(:user, role: :assistant)
|
|
43
|
+
list << create(:user, role: :manager)
|
|
44
|
+
query = source.buckets(:role, %w[assistant manager], cast: :roles).records
|
|
45
|
+
|
|
46
|
+
expect(query).to be_a(Hash)
|
|
47
|
+
expect(query.keys).to match_array([nil, 'assistant', 'manager'])
|
|
48
|
+
expect(query[nil]).to eq([list[0]])
|
|
49
|
+
expect(query['assistant']).to eq([list[1]])
|
|
50
|
+
expect(query['manager']).to eq([list[2]])
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'works with calculations' do
|
|
54
|
+
list = [create(:user, age: 5), create(:user, age: 5), create(:user, age: 15)]
|
|
55
|
+
list << create(:user, age: nil)
|
|
56
|
+
query = source.buckets(:age, 0..50, count: 5).count
|
|
57
|
+
|
|
58
|
+
expect(query).to be_a(Hash)
|
|
59
|
+
expect(query.keys).to match_array([nil, 0...10, 10...20])
|
|
60
|
+
expect(query[nil]).to eq(1)
|
|
61
|
+
expect(query[0...10]).to eq(2)
|
|
62
|
+
expect(query[10...20]).to eq(1)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'works with other types of calculations' do
|
|
66
|
+
list = [create(:user, age: 5), create(:user, age: 5), create(:user, age: 15)]
|
|
67
|
+
query = source.buckets(:age, 0..50, count: 5).sum(:age)
|
|
68
|
+
|
|
69
|
+
expect(query).to be_a(Hash)
|
|
70
|
+
expect(query.keys).to match_array([0...10, 10...20])
|
|
71
|
+
expect(query[0...10]).to eq(10)
|
|
72
|
+
expect(query[10...20]).to eq(15)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'work with joins and merge' do
|
|
76
|
+
list = [create(:user, age: 5), create(:user, age: 5), create(:user, age: 15)]
|
|
77
|
+
records = [create(:comment, user: list[0], content: 'Hello')]
|
|
78
|
+
records << create(:comment, user: list[1], content: 'World')
|
|
79
|
+
records << create(:comment, user: list[2], content: 'Test')
|
|
80
|
+
|
|
81
|
+
query = Comment.joins(:user).merge(source.buckets(:age, 0..50, count: 5)).records
|
|
82
|
+
|
|
83
|
+
expect(query).to be_a(Hash)
|
|
84
|
+
expect(query.keys).to match_array([0...10, 10...20])
|
|
85
|
+
expect(query[0...10]).to match_array([records[0], records[1]])
|
|
86
|
+
expect(query[10...20]).to match_array([records[2]])
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
context 'with dates' do
|
|
90
|
+
let(:keys) { 3.times.map { |add| Date.new(2010 + add) } }
|
|
91
|
+
let(:query) { source.buckets(:created_at, keys, cast: :date) }
|
|
92
|
+
|
|
93
|
+
let!(:list) do
|
|
94
|
+
list = [create(:user, created_at: Date.new(2009, 6, 1))]
|
|
95
|
+
list << create(:user, created_at: Date.new(2010, 6, 1))
|
|
96
|
+
list << create(:user, created_at: Date.new(2011, 6, 1))
|
|
97
|
+
list << create(:user, created_at: Date.new(2011, 12, 31))
|
|
98
|
+
list << create(:user, created_at: Date.new(2015, 6, 1))
|
|
99
|
+
list
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'produces the right query' do
|
|
103
|
+
expect(query.to_sql).to include(<<~SQL.squish)
|
|
104
|
+
WIDTH_BUCKET("users"."created_at", ARRAY['2010-01-01', '2011-01-01', '2012-01-01']::date[]) AS bucket
|
|
105
|
+
SQL
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'outputs the right results' do
|
|
109
|
+
result = query.records
|
|
110
|
+
expect(result.keys).to match_array([nil, *keys])
|
|
111
|
+
expect(result[nil]).to eq([list[0]])
|
|
112
|
+
expect(result[keys[0]]).to eq([list[1]])
|
|
113
|
+
expect(result[keys[1]]).to match_array([list[2], list[3]])
|
|
114
|
+
expect(result[keys[2]]).to eq([list[4]])
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'works with count just fine' do
|
|
118
|
+
expect(query.count).to be_eql(nil => 1, keys[0] => 1, keys[1] => 2, keys[2] => 1)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -692,6 +692,22 @@ RSpec.describe 'Composite' do
|
|
|
692
692
|
expect(Place.where(offices: many.offices).first).to be_eql(many)
|
|
693
693
|
end
|
|
694
694
|
|
|
695
|
+
it 'lists whole values on a plain column' do
|
|
696
|
+
other = Composite::Address.new(street: 'Side', number: 1)
|
|
697
|
+
|
|
698
|
+
expect(Place.where(home: [address, other]).to_sql)
|
|
699
|
+
.to include(%{"places"."home" IN ('("Main",,"9",)'::address, '("Side",,"1",)'::address)})
|
|
700
|
+
expect(Place.where(home: [address, other]).first).to be_eql(place)
|
|
701
|
+
expect(Place.where(home: [other]).first).to be_nil
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
it 'loads the columns without a permanent connection' do
|
|
705
|
+
expect(ActiveRecord::Base).not_to receive(:connection)
|
|
706
|
+
|
|
707
|
+
Composite::Address.reset_columns!
|
|
708
|
+
expect(Composite::Address.new(street: 'Main').street).to be_eql('Main')
|
|
709
|
+
end
|
|
710
|
+
|
|
695
711
|
it 'checks each entry of a list of hashes' do
|
|
696
712
|
expect(Place.where(offices: [{ street: 'A' }, { street: 'B' }]).to_sql)
|
|
697
713
|
.to include('EXISTS', 'OR')
|
|
@@ -709,6 +725,46 @@ RSpec.describe 'Composite' do
|
|
|
709
725
|
end
|
|
710
726
|
end
|
|
711
727
|
|
|
728
|
+
context 'on ordering and grouping' do
|
|
729
|
+
let!(:first) { Place.create!(name: 'a', home: { street: 'Main', number: 9 }) }
|
|
730
|
+
let!(:second) { Place.create!(name: 'b', home: { street: 'Side', number: 1 }) }
|
|
731
|
+
|
|
732
|
+
it 'orders by a column' do
|
|
733
|
+
expect(Place.order(home: { street: :desc }).to_sql)
|
|
734
|
+
.to include(%{ORDER BY ("places"."home")."street" DESC})
|
|
735
|
+
|
|
736
|
+
expect(Place.order(home: { street: :desc }).pluck(:name)).to be_eql(%w[b a])
|
|
737
|
+
expect(Place.order(home: { number: :asc }).pluck(:name)).to be_eql(%w[b a])
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
it 'orders by the whole value' do
|
|
741
|
+
expect(Place.order(:home).to_sql).to include(%{ORDER BY "places"."home" ASC})
|
|
742
|
+
expect(Place.order(:home).pluck(:name)).to be_eql(%w[a b])
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
it 'groups by a column' do
|
|
746
|
+
expect(Place.group(home: :street).to_sql).to include(%{GROUP BY ("places"."home")."street"})
|
|
747
|
+
expect(Place.group(home: :street).count).to be_eql('Main' => 1, 'Side' => 1)
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
it 'filters groups with having' do
|
|
751
|
+
expect(Place.group(:home).having(home: { street: 'Main' }).to_sql)
|
|
752
|
+
.to include(%{HAVING (("places"."home")."street" = 'Main')})
|
|
753
|
+
|
|
754
|
+
expect(Place.group(home: :street).having(home: { street: 'Main' }).count)
|
|
755
|
+
.to be_eql('Main' => 1)
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
it 'plucks a column' do
|
|
759
|
+
expect(Place.order(:name).pluck(home: :number)).to be_eql([9, 1])
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
it 'raises when a key is not a column of the composite type' do
|
|
763
|
+
expect { Place.order(home: { nope: :asc }).to_sql }
|
|
764
|
+
.to raise_error(ArgumentError, /not a column of the "address"/)
|
|
765
|
+
end
|
|
766
|
+
end
|
|
767
|
+
|
|
712
768
|
context 'on validation' do
|
|
713
769
|
let(:validator) { Torque::PostgreSQL::Validations::NestedValidator }
|
|
714
770
|
|
|
@@ -736,6 +792,17 @@ RSpec.describe 'Composite' do
|
|
|
736
792
|
expect(Place.new(name: 'Fine', home: { street: 'Main' })).to be_valid
|
|
737
793
|
end
|
|
738
794
|
|
|
795
|
+
it 'validates the attributes that are also normalized' do
|
|
796
|
+
klass = Class.new(Place) do
|
|
797
|
+
normalizes :home, with: ->(value) { value }
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
place = klass.new(name: 'Broken', home: invalid_address)
|
|
801
|
+
|
|
802
|
+
expect(place).to be_invalid
|
|
803
|
+
expect(place.errors.attribute_names).to include(:home)
|
|
804
|
+
end
|
|
805
|
+
|
|
739
806
|
it 'only validates the attributes backed by a composite type' do
|
|
740
807
|
Place.new
|
|
741
808
|
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe 'CrossJoin' do
|
|
4
|
+
let(:table) { Profile.arel_table }
|
|
5
|
+
let(:snippets) { Torque::PostgreSQL::FN.unnest(table[:snippets]) }
|
|
6
|
+
|
|
7
|
+
context 'on model' do
|
|
8
|
+
it 'has its method' do
|
|
9
|
+
expect(Profile).to respond_to(:cross_join)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'returns a relation when using the method' do
|
|
13
|
+
expect(Profile.cross_join(snippets, as: :doc)).to be_a(ActiveRecord::Relation)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
context 'on relation' do
|
|
18
|
+
subject { Profile.unscoped }
|
|
19
|
+
|
|
20
|
+
it 'has its method' do
|
|
21
|
+
expect(subject).to respond_to(:cross_join)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'joins an arel function without naming its columns' do
|
|
25
|
+
expect(subject.cross_join(snippets, as: :doc).to_sql).to \
|
|
26
|
+
eql('SELECT "profiles".* FROM "profiles" CROSS JOIN UNNEST("profiles"."snippets") "doc"')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'names the columns when they are given' do
|
|
30
|
+
expect(subject.cross_join(snippets, as: :doc, columns: %i[value]).to_sql).to \
|
|
31
|
+
eql('SELECT "profiles".* FROM "profiles" CROSS JOIN UNNEST("profiles"."snippets") "doc"("value")')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'does not write LATERAL for a function' do
|
|
35
|
+
expect(subject.cross_join(snippets, as: :doc).to_sql).not_to include('LATERAL')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'does not write ROWS FROM without an ordinality' do
|
|
39
|
+
expect(subject.cross_join(snippets, as: :doc).to_sql).not_to include('ROWS FROM')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'raises for a source it cannot join' do
|
|
43
|
+
expect { subject.cross_join('whatever', as: :doc) }.to \
|
|
44
|
+
raise_error(ArgumentError, /Unable to join/)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
context 'modes' do
|
|
49
|
+
subject { Profile.unscoped }
|
|
50
|
+
|
|
51
|
+
it 'writes a left join against a true condition' do
|
|
52
|
+
expect(subject.cross_join(snippets, as: :doc, mode: :left).to_sql).to \
|
|
53
|
+
eql('SELECT "profiles".* FROM "profiles" LEFT OUTER JOIN UNNEST("profiles"."snippets") "doc" ON TRUE')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'raises for a mode postgres cannot combine with a lateral reference' do
|
|
57
|
+
expect { subject.cross_join(snippets, as: :doc, mode: :right) }.to \
|
|
58
|
+
raise_error(ArgumentError, /not implemented as a join type/)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
context 'rows' do
|
|
63
|
+
subject { Profile.unscoped }
|
|
64
|
+
|
|
65
|
+
it 'turns a list of documents into one array per column' do
|
|
66
|
+
result = subject.cross_join([{ id: 1, label: 'a' }, { id: 2, label: 'b' }], as: :v)
|
|
67
|
+
expect(result.to_sql).to \
|
|
68
|
+
eql('SELECT "profiles".* FROM "profiles" CROSS JOIN UNNEST(ARRAY[1, 2], ARRAY[\'a\', \'b\']) "v"("id", "label")')
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'accepts plain rows when the columns are named' do
|
|
72
|
+
result = subject.cross_join([[1, 'a']], as: :v, columns: %i[id label])
|
|
73
|
+
expect(result.to_sql).to \
|
|
74
|
+
eql('SELECT "profiles".* FROM "profiles" CROSS JOIN UNNEST(ARRAY[1], ARRAY[\'a\']) "v"("id", "label")')
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'reads the documents by name, in any order' do
|
|
78
|
+
result = subject.cross_join([{ id: 1, label: 'a' }, { label: 'b', id: 2 }], as: :v)
|
|
79
|
+
expect(result.to_sql).to \
|
|
80
|
+
eql('SELECT "profiles".* FROM "profiles" CROSS JOIN UNNEST(ARRAY[1, 2], ARRAY[\'a\', \'b\']) "v"("id", "label")')
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'does not care whether the keys are strings or symbols' do
|
|
84
|
+
result = subject.cross_join([{ 'id' => 1 }], as: :v, columns: %i[id])
|
|
85
|
+
expect(result.to_sql).to include('UNNEST(ARRAY[1]) "v"("id")')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'raises when the columns cannot be named' do
|
|
89
|
+
expect { subject.cross_join([[1, 'a']], as: :v) }.to \
|
|
90
|
+
raise_error(ArgumentError, /Unable to name the columns/)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'raises for an empty list' do
|
|
94
|
+
expect { subject.cross_join([], as: :v) }.to \
|
|
95
|
+
raise_error(ArgumentError, /empty list/)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'multiplies the rows of the query' do
|
|
99
|
+
Profile.create!(name: 'one')
|
|
100
|
+
Profile.create!(name: 'two')
|
|
101
|
+
|
|
102
|
+
expect(subject.cross_join([{ n: 1 }, { n: 2 }, { n: 3 }], as: :v).count).to be_eql(6)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it 'keeps the values typed' do
|
|
106
|
+
Profile.create!(name: 'one')
|
|
107
|
+
result = subject.cross_join([{ n: 10 }], as: :v).pluck(Arel.sql('"v"."n"'))
|
|
108
|
+
|
|
109
|
+
expect(result).to be_eql([10])
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
context 'a relation' do
|
|
114
|
+
subject { Profile.unscoped }
|
|
115
|
+
|
|
116
|
+
it 'is written as a lateral subquery' do
|
|
117
|
+
other = Profile.select(:name).limit(1)
|
|
118
|
+
expect(subject.cross_join(other, as: :other, columns: %i[name]).to_sql).to \
|
|
119
|
+
include('CROSS JOIN LATERAL (SELECT "profiles"."name" FROM "profiles" LIMIT 1) "other"("name")')
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it 'keeps the bindings of both queries apart' do
|
|
123
|
+
Profile.create!(name: 'aaa')
|
|
124
|
+
Profile.create!(name: 'bbb')
|
|
125
|
+
|
|
126
|
+
other = Profile.where(name: 'aaa').select(:name).limit(1)
|
|
127
|
+
result = subject.where(name: 'bbb').cross_join(other, as: :other, columns: %i[name])
|
|
128
|
+
|
|
129
|
+
expect(result.pluck(:name, Arel.sql('"other"."name"'))).to be_eql([%w[bbb aaa]])
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
context 'with_ordinality' do
|
|
134
|
+
subject { Profile.unscoped }
|
|
135
|
+
|
|
136
|
+
it 'uses the configured name' do
|
|
137
|
+
expect(subject.cross_join(snippets, as: :doc, columns: %i[value], with_ordinality: true).to_sql).to \
|
|
138
|
+
eql('SELECT "profiles".* FROM "profiles" CROSS JOIN UNNEST("profiles"."snippets")' \
|
|
139
|
+
' WITH ORDINALITY "doc"("value", "ordinality")')
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it 'accepts a name of its own' do
|
|
143
|
+
expect(subject.cross_join(snippets, as: :doc, columns: %i[value], with_ordinality: :idx).to_sql).to \
|
|
144
|
+
eql('SELECT "profiles".* FROM "profiles" CROSS JOIN UNNEST("profiles"."snippets")' \
|
|
145
|
+
' WITH ORDINALITY "doc"("value", "idx")')
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'leaves the list out when the columns are unknown' do
|
|
149
|
+
result = subject.cross_join(snippets, as: :doc, with_ordinality: true).to_sql
|
|
150
|
+
expect(result).to end_with('WITH ORDINALITY "doc"')
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it 'refuses a name of its own when the columns are unknown' do
|
|
154
|
+
expect { subject.cross_join(snippets, as: :doc, with_ordinality: :idx) }.to \
|
|
155
|
+
raise_error(ArgumentError, /columns of the source are unknown/)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it 'counts from one, in the order of the list' do
|
|
159
|
+
Profile.create!(name: 'one')
|
|
160
|
+
result = subject.cross_join([{ n: 'a' }, { n: 'b' }, { n: 'c' }], as: :v, with_ordinality: true)
|
|
161
|
+
|
|
162
|
+
expect(result.pluck(Arel.sql('"v"."ordinality"'))).to be_eql([1, 2, 3])
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
context 'scoping' do
|
|
167
|
+
subject { Profile.unscoped.cross_join(snippets, as: :doc) }
|
|
168
|
+
|
|
169
|
+
it 'is removed by unscope' do
|
|
170
|
+
expect(subject.unscope(:cross_joins).to_sql).to eql('SELECT "profiles".* FROM "profiles"')
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it 'is removed by except' do
|
|
174
|
+
expect(subject.except(:cross_joins).to_sql).to eql('SELECT "profiles".* FROM "profiles"')
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it 'is carried over by merge' do
|
|
178
|
+
expect(Profile.all.merge(subject).to_sql).to include('CROSS JOIN UNNEST')
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
it 'is not removed by except(:joins)' do
|
|
182
|
+
expect(subject.except(:joins).to_sql).to include('CROSS JOIN UNNEST')
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
@@ -39,6 +39,11 @@ RSpec.describe 'DistinctOn' do
|
|
|
39
39
|
eql('SELECT DISTINCT ON ( "authors"."name", "authors"."age" ) "posts".* FROM "posts"')
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
it 'is able to do with a part of a column' do
|
|
43
|
+
expect(Profile.distinct_on(settings: :theme).to_sql).to \
|
|
44
|
+
eql(%{SELECT DISTINCT ON ( ("profiles"."settings" #>> ARRAY['theme']) ) "profiles".* FROM "profiles"})
|
|
45
|
+
end
|
|
46
|
+
|
|
42
47
|
it 'raises with invalid relation' do
|
|
43
48
|
expect { subject.distinct_on(supervisors: :name).to_sql }.to \
|
|
44
49
|
raise_error(ArgumentError, /Relation for/)
|
data/spec/tests/enum_set_spec.rb
CHANGED
|
@@ -227,6 +227,10 @@ RSpec.describe 'Enum' do
|
|
|
227
227
|
expect(subject.serialize(enum.B | enum.C)).to be_eql('{B,C}')
|
|
228
228
|
expect(subject.serialize(3)).to be_eql('{A,B}')
|
|
229
229
|
end
|
|
230
|
+
|
|
231
|
+
it 'keeps an invalid value for the database to reject' do
|
|
232
|
+
expect(subject.serialize(%w[A E])).to be_eql('{A,E}')
|
|
233
|
+
end
|
|
230
234
|
end
|
|
231
235
|
|
|
232
236
|
context 'on cast' do
|
data/spec/tests/enum_spec.rb
CHANGED
|
@@ -98,6 +98,13 @@ RSpec.describe 'Enum' do
|
|
|
98
98
|
expect(subject.values).to be_eql(values)
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
+
it 'loads the values without a permanent connection' do
|
|
102
|
+
expect(ActiveRecord::Base).not_to receive(:connection)
|
|
103
|
+
|
|
104
|
+
subject.remove_instance_variable(:@values) if subject.instance_variable_defined?(:@values)
|
|
105
|
+
expect(subject.values).to be_eql(values)
|
|
106
|
+
end
|
|
107
|
+
|
|
101
108
|
it 'can return a sample value' do
|
|
102
109
|
expect(Enum).to respond_to(:sample)
|
|
103
110
|
expect(Enum::ContentStatus).to respond_to(:sample)
|
|
@@ -291,8 +298,13 @@ RSpec.describe 'Enum' do
|
|
|
291
298
|
context 'on serialize' do
|
|
292
299
|
it 'returns nil' do
|
|
293
300
|
expect(subject.serialize(nil)).to be_nil
|
|
294
|
-
|
|
295
|
-
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
it 'keeps an invalid value for the database to reject' do
|
|
304
|
+
expect(subject.serialize('test')).to be_eql('test')
|
|
305
|
+
expect(subject.serialize(15)).to be_eql('15')
|
|
306
|
+
expect { User.create!(name: 'Rick', role: 'bogus') }
|
|
307
|
+
.to raise_error(ActiveRecord::StatementInvalid, /invalid input value for enum/)
|
|
296
308
|
end
|
|
297
309
|
|
|
298
310
|
it 'returns as string' do
|
|
@@ -306,12 +318,16 @@ RSpec.describe 'Enum' do
|
|
|
306
318
|
expect(subject.cast(nil)).to be_nil
|
|
307
319
|
end
|
|
308
320
|
|
|
309
|
-
it 'accepts
|
|
321
|
+
it 'accepts blank values as nil' do
|
|
310
322
|
expect(subject.cast(false)).to be_nil
|
|
311
|
-
expect(subject.cast(true)).to be_nil
|
|
312
323
|
expect(subject.cast([])).to be_nil
|
|
313
324
|
end
|
|
314
325
|
|
|
326
|
+
it 'keeps invalid values for the database to reject' do
|
|
327
|
+
expect(subject.cast(true)).to be(true)
|
|
328
|
+
expect(subject.cast('bogus')).to be_eql('bogus')
|
|
329
|
+
end
|
|
330
|
+
|
|
315
331
|
it 'accepts string' do
|
|
316
332
|
value = subject.cast('created')
|
|
317
333
|
expect(value).to be_a(enum)
|
|
@@ -73,13 +73,23 @@ RSpec.describe 'Geometries' do
|
|
|
73
73
|
expect(config_class).to receive(:new).with(1.0, 2.0, 3.0, 4.0).and_return(1)
|
|
74
74
|
expect(instance.cast('1.0, 2.0, 3.0, 4.0')).to be_eql(1)
|
|
75
75
|
|
|
76
|
-
expect
|
|
76
|
+
expect(config_class).to receive(:new).with(6.0).and_return(9)
|
|
77
|
+
expect(instance.cast(['6 6 6'])).to be_eql(9)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'does not change the given value' do
|
|
81
|
+
value = '(1, 2, 3, 4)'
|
|
82
|
+
expect(config_class).to receive(:new).with(1, 2, 3, 4).twice.and_return(1)
|
|
83
|
+
expect(instance.cast(value)).to be_eql(1)
|
|
84
|
+
expect(value).to be_eql('(1, 2, 3, 4)')
|
|
85
|
+
expect(instance.cast(value.freeze)).to be_eql(1)
|
|
77
86
|
end
|
|
78
87
|
|
|
79
88
|
it 'accepts hash values' do
|
|
80
89
|
expect(instance.cast({})).to be_nil
|
|
81
90
|
|
|
82
|
-
expect
|
|
91
|
+
expect(config_class).to receive(:new).with(1, 2).and_return(9)
|
|
92
|
+
expect(instance.cast({ 'a' => 1, 'b' => 2 })).to be_eql(9)
|
|
83
93
|
|
|
84
94
|
expect(config_class).to receive(:new).with(1, 2, 3, 4).and_return(4)
|
|
85
95
|
expect(instance.cast({ 'a' => 1, 'b' => 2 , 'c' => 3, 'd' => 4})).to be_eql(4)
|
|
@@ -103,7 +113,8 @@ RSpec.describe 'Geometries' do
|
|
|
103
113
|
|
|
104
114
|
expect(instance.cast([])).to be_nil
|
|
105
115
|
|
|
106
|
-
expect
|
|
116
|
+
expect(config_class).to receive(:new).with(6, 5, 4).and_return(9)
|
|
117
|
+
expect(instance.cast([6, 5, 4])).to be_eql(9)
|
|
107
118
|
end
|
|
108
119
|
end
|
|
109
120
|
|
|
@@ -121,13 +132,13 @@ RSpec.describe 'Geometries' do
|
|
|
121
132
|
end
|
|
122
133
|
|
|
123
134
|
it 'accepts hash value' do
|
|
124
|
-
expect
|
|
135
|
+
expect(instance.serialize({a: 1, b: 2, c: 3})).to be_eql('(1, 2, <3, {}>)')
|
|
125
136
|
expect(instance.serialize({a: 1, b: 2, c: 3, d: 4})).to be_eql('(1, 2, <3, {4}>)')
|
|
126
137
|
expect(instance.serialize({a: 1, b: 2, c: 3, d: 4, e: 5, f: 6})).to be_eql('(1, 2, <3, {4}>)')
|
|
127
138
|
end
|
|
128
139
|
|
|
129
140
|
it 'accepts array value' do
|
|
130
|
-
expect
|
|
141
|
+
expect(instance.serialize([6, 5, 4])).to be_eql('(6, 5, <4, {}>)')
|
|
131
142
|
expect(instance.serialize([1, 2, 3, 4])).to be_eql('(1, 2, <3, {4}>)')
|
|
132
143
|
expect(instance.serialize([5, 4, 3, 2, 1, 0])).to be_eql('(5, 4, <3, {2}>)')
|
|
133
144
|
end
|
data/spec/tests/has_many_spec.rb
CHANGED
|
@@ -204,7 +204,8 @@ RSpec.describe 'HasMany' do
|
|
|
204
204
|
end
|
|
205
205
|
|
|
206
206
|
context 'with query constraint' do
|
|
207
|
-
|
|
207
|
+
# why: Rails 8.1.4+ skips preloading for owners whose composite key has a nil part
|
|
208
|
+
let(:activity) { Activity.create!(author: FactoryBot.create(:author)) }
|
|
208
209
|
|
|
209
210
|
before do
|
|
210
211
|
skip('Only Rails 7.1 onwards') unless Post.respond_to?(:query_constraints)
|
data/spec/tests/interval_spec.rb
CHANGED
|
@@ -98,6 +98,12 @@ RSpec.describe 'Interval' do
|
|
|
98
98
|
expect(value).to eq(reference)
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
+
it 'does not change the given array' do
|
|
102
|
+
value = [1, nil, 2]
|
|
103
|
+
subject.cast(value)
|
|
104
|
+
expect(value).to be_eql([1, nil, 2])
|
|
105
|
+
end
|
|
106
|
+
|
|
101
107
|
it 'accepts array with Y-M-D H:M:S format' do
|
|
102
108
|
value = subject.cast([1, 2, 3, 4, 5, 6])
|
|
103
109
|
expect(value).to be_a(ActiveSupport::Duration)
|