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.
Files changed (194) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +28 -0
  3. data/lib/generators/torque/skill_generator.rb +17 -0
  4. data/lib/generators/torque/templates/trigger.sql.erb +3 -0
  5. data/lib/generators/torque/trigger_generator.rb +13 -0
  6. data/lib/torque/postgresql/adapter/database_statements.rb +9 -0
  7. data/lib/torque/postgresql/adapter/oid/array.rb +0 -4
  8. data/lib/torque/postgresql/adapter/oid/composite.rb +2 -1
  9. data/lib/torque/postgresql/adapter/oid/enum.rb +1 -1
  10. data/lib/torque/postgresql/adapter/oid/enum_set.rb +2 -2
  11. data/lib/torque/postgresql/adapter/oid/interval.rb +1 -1
  12. data/lib/torque/postgresql/adapter/oid/lquery.rb +45 -28
  13. data/lib/torque/postgresql/adapter/oid/ltree.rb +67 -9
  14. data/lib/torque/postgresql/adapter/oid/struct.rb +1 -1
  15. data/lib/torque/postgresql/adapter/oid.rb +13 -0
  16. data/lib/torque/postgresql/adapter/schema_definitions.rb +5 -0
  17. data/lib/torque/postgresql/adapter/schema_dumper.rb +1 -0
  18. data/lib/torque/postgresql/arel/nodes.rb +51 -0
  19. data/lib/torque/postgresql/arel/visitors.rb +46 -0
  20. data/lib/torque/postgresql/attributes/base.rb +10 -7
  21. data/lib/torque/postgresql/attributes/composite.rb +1 -5
  22. data/lib/torque/postgresql/attributes/enum.rb +1 -6
  23. data/lib/torque/postgresql/attributes/lquery.rb +67 -127
  24. data/lib/torque/postgresql/attributes/ltree.rb +35 -83
  25. data/lib/torque/postgresql/base.rb +9 -1
  26. data/lib/torque/postgresql/config.rb +21 -3
  27. data/lib/torque/postgresql/geometry_builder.rb +4 -10
  28. data/lib/torque/postgresql/inheritance.rb +5 -5
  29. data/lib/torque/postgresql/predicate_builder/arel_attribute_handler.rb +1 -1
  30. data/lib/torque/postgresql/predicate_builder/array_handler.rb +16 -8
  31. data/lib/torque/postgresql/predicate_builder/composite_handler.rb +18 -5
  32. data/lib/torque/postgresql/predicate_builder/ltree_handler.rb +86 -18
  33. data/lib/torque/postgresql/predicate_builder/struct_handler.rb +29 -9
  34. data/lib/torque/postgresql/predicate_builder.rb +38 -11
  35. data/lib/torque/postgresql/railtie.rb +17 -0
  36. data/lib/torque/postgresql/relation/auxiliary_statement.rb +7 -3
  37. data/lib/torque/postgresql/relation/buckets.rb +2 -1
  38. data/lib/torque/postgresql/relation/cross_join.rb +193 -0
  39. data/lib/torque/postgresql/relation/inheritance.rb +1 -2
  40. data/lib/torque/postgresql/relation/join_series.rb +1 -0
  41. data/lib/torque/postgresql/relation/merger.rb +9 -0
  42. data/lib/torque/postgresql/relation/segments.rb +140 -0
  43. data/lib/torque/postgresql/relation.rb +23 -4
  44. data/lib/torque/postgresql/version.rb +1 -1
  45. data/lib/torque/postgresql/versioned_commands/command_migration.rb +23 -0
  46. data/lib/torque/postgresql/versioned_commands.rb +39 -0
  47. data/lib/torque/postgresql.rb +0 -2
  48. data/sig/generators/torque/function_generator.rbs +9 -0
  49. data/sig/generators/torque/skill_generator.rbs +7 -0
  50. data/sig/generators/torque/trigger_generator.rbs +9 -0
  51. data/sig/generators/torque/type_generator.rbs +9 -0
  52. data/sig/generators/torque/view_generator.rbs +9 -0
  53. data/sig/manifest.yaml +7 -0
  54. data/sig/torque/postgresql/adapter/database_statements.rbs +42 -0
  55. data/sig/torque/postgresql/adapter/inheritance_statements.rbs +36 -0
  56. data/sig/torque/postgresql/adapter/oid/array.rbs +20 -0
  57. data/sig/torque/postgresql/adapter/oid/box.rbs +28 -0
  58. data/sig/torque/postgresql/adapter/oid/circle.rbs +33 -0
  59. data/sig/torque/postgresql/adapter/oid/composite.rbs +35 -0
  60. data/sig/torque/postgresql/adapter/oid/enum.rbs +28 -0
  61. data/sig/torque/postgresql/adapter/oid/enum_set.rbs +19 -0
  62. data/sig/torque/postgresql/adapter/oid/interval.rbs +20 -0
  63. data/sig/torque/postgresql/adapter/oid/line.rbs +33 -0
  64. data/sig/torque/postgresql/adapter/oid/lquery.rbs +21 -0
  65. data/sig/torque/postgresql/adapter/oid/ltree.rbs +32 -0
  66. data/sig/torque/postgresql/adapter/oid/range.rbs +28 -0
  67. data/sig/torque/postgresql/adapter/oid/segment.rbs +38 -0
  68. data/sig/torque/postgresql/adapter/oid/struct.rbs +41 -0
  69. data/sig/torque/postgresql/adapter/oid/struct_list.rbs +20 -0
  70. data/sig/torque/postgresql/adapter/oid/struct_set.rbs +18 -0
  71. data/sig/torque/postgresql/adapter/oid.rbs +9 -0
  72. data/sig/torque/postgresql/adapter/quoting.rbs +20 -0
  73. data/sig/torque/postgresql/adapter/schema_creation.rbs +13 -0
  74. data/sig/torque/postgresql/adapter/schema_definitions.rbs +55 -0
  75. data/sig/torque/postgresql/adapter/schema_dumper.rbs +37 -0
  76. data/sig/torque/postgresql/adapter/schema_overrides.rbs +46 -0
  77. data/sig/torque/postgresql/adapter/schema_statements.rbs +50 -0
  78. data/sig/torque/postgresql/adapter.rbs +24 -0
  79. data/sig/torque/postgresql/arel/infix_operation.rbs +79 -0
  80. data/sig/torque/postgresql/arel/join_source.rbs +19 -0
  81. data/sig/torque/postgresql/arel/nodes.rbs +83 -0
  82. data/sig/torque/postgresql/arel/operations.rbs +28 -0
  83. data/sig/torque/postgresql/arel/select_manager.rbs +15 -0
  84. data/sig/torque/postgresql/arel/visitors.rbs +30 -0
  85. data/sig/torque/postgresql/arel.rbs +6 -0
  86. data/sig/torque/postgresql/associations/association_scope.rbs +28 -0
  87. data/sig/torque/postgresql/associations/belongs_to_many_association.rbs +51 -0
  88. data/sig/torque/postgresql/associations/builder/belongs_to_many.rbs +24 -0
  89. data/sig/torque/postgresql/associations/builder/has_many.rbs +15 -0
  90. data/sig/torque/postgresql/associations/builder.rbs +8 -0
  91. data/sig/torque/postgresql/associations/foreign_association.rbs +16 -0
  92. data/sig/torque/postgresql/associations/join_dependency.rbs +16 -0
  93. data/sig/torque/postgresql/associations/preloader/association.rbs +32 -0
  94. data/sig/torque/postgresql/associations/preloader/loader_query.rbs +20 -0
  95. data/sig/torque/postgresql/associations/preloader.rbs +8 -0
  96. data/sig/torque/postgresql/associations.rbs +17 -0
  97. data/sig/torque/postgresql/attributes/base.rbs +37 -0
  98. data/sig/torque/postgresql/attributes/builder/enum.rbs +38 -0
  99. data/sig/torque/postgresql/attributes/builder/full_text_search.rbs +27 -0
  100. data/sig/torque/postgresql/attributes/builder/period.rbs +106 -0
  101. data/sig/torque/postgresql/attributes/builder.rbs +19 -0
  102. data/sig/torque/postgresql/attributes/composite.rbs +28 -0
  103. data/sig/torque/postgresql/attributes/enum.rbs +69 -0
  104. data/sig/torque/postgresql/attributes/enum_set.rbs +75 -0
  105. data/sig/torque/postgresql/attributes/full_text_search.rbs +13 -0
  106. data/sig/torque/postgresql/attributes/lazy.rbs +17 -0
  107. data/sig/torque/postgresql/attributes/lquery.rbs +48 -0
  108. data/sig/torque/postgresql/attributes/ltree.rbs +46 -0
  109. data/sig/torque/postgresql/attributes/period.rbs +13 -0
  110. data/sig/torque/postgresql/attributes/simple_enum.rbs +39 -0
  111. data/sig/torque/postgresql/attributes/struct.rbs +38 -0
  112. data/sig/torque/postgresql/attributes.rbs +6 -0
  113. data/sig/torque/postgresql/autosave_association.rbs +16 -0
  114. data/sig/torque/postgresql/auxiliary_statement/recursive.rbs +22 -0
  115. data/sig/torque/postgresql/auxiliary_statement/settings.rbs +63 -0
  116. data/sig/torque/postgresql/auxiliary_statement.rbs +57 -0
  117. data/sig/torque/postgresql/base.rbs +41 -0
  118. data/sig/torque/postgresql/collector.rbs +7 -0
  119. data/sig/torque/postgresql/config.rbs +9 -0
  120. data/sig/torque/postgresql/function.rbs +21 -0
  121. data/sig/torque/postgresql/geometry_builder.rbs +20 -0
  122. data/sig/torque/postgresql/i18n.rbs +15 -0
  123. data/sig/torque/postgresql/inheritance/expander.rbs +23 -0
  124. data/sig/torque/postgresql/inheritance/record.rbs +20 -0
  125. data/sig/torque/postgresql/inheritance.rbs +47 -0
  126. data/sig/torque/postgresql/insert_all.rbs +22 -0
  127. data/sig/torque/postgresql/migration/command_recorder.rbs +30 -0
  128. data/sig/torque/postgresql/migration.rbs +6 -0
  129. data/sig/torque/postgresql/predicate_builder/arel_attribute_handler.rbs +15 -0
  130. data/sig/torque/postgresql/predicate_builder/array_handler.rbs +26 -0
  131. data/sig/torque/postgresql/predicate_builder/composite_handler.rbs +26 -0
  132. data/sig/torque/postgresql/predicate_builder/enumerator_lazy_handler.rbs +18 -0
  133. data/sig/torque/postgresql/predicate_builder/ltree_handler.rbs +36 -0
  134. data/sig/torque/postgresql/predicate_builder/regexp_handler.rbs +16 -0
  135. data/sig/torque/postgresql/predicate_builder/struct_handler.rbs +24 -0
  136. data/sig/torque/postgresql/predicate_builder.rbs +23 -0
  137. data/sig/torque/postgresql/predicate_table.rbs +24 -0
  138. data/sig/torque/postgresql/railtie.rbs +10 -0
  139. data/sig/torque/postgresql/reflection/abstract_reflection.rbs +22 -0
  140. data/sig/torque/postgresql/reflection/association_reflection.rbs +18 -0
  141. data/sig/torque/postgresql/reflection/belongs_to_many_reflection.rbs +30 -0
  142. data/sig/torque/postgresql/reflection/has_many_reflection.rbs +14 -0
  143. data/sig/torque/postgresql/reflection/runtime_reflection.rbs +18 -0
  144. data/sig/torque/postgresql/reflection/through_reflection.rbs +14 -0
  145. data/sig/torque/postgresql/reflection.rbs +11 -0
  146. data/sig/torque/postgresql/relation/auxiliary_statement.rbs +29 -0
  147. data/sig/torque/postgresql/relation/buckets.rbs +35 -0
  148. data/sig/torque/postgresql/relation/cross_join.rbs +63 -0
  149. data/sig/torque/postgresql/relation/distinct_on.rbs +16 -0
  150. data/sig/torque/postgresql/relation/inheritance.rbs +48 -0
  151. data/sig/torque/postgresql/relation/join_series.rbs +28 -0
  152. data/sig/torque/postgresql/relation/merger.rbs +23 -0
  153. data/sig/torque/postgresql/relation/segments.rbs +27 -0
  154. data/sig/torque/postgresql/relation.rbs +48 -0
  155. data/sig/torque/postgresql/schema_cache/bound_schema_reflection.rbs +14 -0
  156. data/sig/torque/postgresql/schema_cache/inheritance.rbs +12 -0
  157. data/sig/torque/postgresql/schema_cache/schema_reflection.rbs +14 -0
  158. data/sig/torque/postgresql/schema_cache.rbs +43 -0
  159. data/sig/torque/postgresql/table_name.rbs +20 -0
  160. data/sig/torque/postgresql/validations.rbs +13 -0
  161. data/sig/torque/postgresql/version.rbs +5 -0
  162. data/sig/torque/postgresql/versioned_commands/command_migration.rbs +54 -0
  163. data/sig/torque/postgresql/versioned_commands/generator.rbs +17 -0
  164. data/sig/torque/postgresql/versioned_commands/migration_context.rbs +30 -0
  165. data/sig/torque/postgresql/versioned_commands/migrator.rbs +19 -0
  166. data/sig/torque/postgresql/versioned_commands/schema_table.rbs +23 -0
  167. data/sig/torque/postgresql/versioned_commands.rbs +25 -0
  168. data/sig/torque/postgresql.rbs +26 -0
  169. data/skills/torque-postgresql/SKILL.md +70 -0
  170. data/spec/fixtures/migrations/20250101000008_create_trigger_audit_users_v1.sql +3 -0
  171. data/spec/initialize.rb +9 -0
  172. data/spec/mocks/cache_query.rb +1 -1
  173. data/spec/rbs_helper.rb +8 -0
  174. data/spec/schema.rb +2 -0
  175. data/spec/spec_helper.rb +1 -0
  176. data/spec/tests/arel_spec.rb +1 -3
  177. data/spec/tests/auxiliary_statement_spec.rb +12 -0
  178. data/spec/tests/buckets_spec.rb +122 -0
  179. data/spec/tests/composite_spec.rb +67 -0
  180. data/spec/tests/cross_join_spec.rb +185 -0
  181. data/spec/tests/distinct_on_spec.rb +5 -0
  182. data/spec/tests/enum_set_spec.rb +4 -0
  183. data/spec/tests/enum_spec.rb +20 -4
  184. data/spec/tests/geometric_builder_spec.rb +16 -5
  185. data/spec/tests/has_many_spec.rb +2 -1
  186. data/spec/tests/interval_spec.rb +6 -0
  187. data/spec/tests/ltree_spec.rb +398 -64
  188. data/spec/tests/predicate_builder_spec.rb +120 -0
  189. data/spec/tests/relation_spec.rb +25 -69
  190. data/spec/tests/segments_spec.rb +264 -0
  191. data/spec/tests/struct_spec.rb +202 -0
  192. data/spec/tests/table_inheritance_spec.rb +10 -0
  193. data/spec/tests/versioned_commands_spec.rb +145 -0
  194. metadata +153 -2
@@ -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
- # Rails 8.1 type casts column defaults, so it is no longer a string
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/)
@@ -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
@@ -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
- expect(subject.serialize('test')).to be_nil
295
- expect(subject.serialize(15)).to be_nil
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 invalid values as nil' do
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 { instance.cast(['6 6 6']) }.to raise_error(RuntimeError, 'Invalid format')
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 { instance.cast({ 'a' => 1, 'b' => 2 }) }.to raise_error(RuntimeError, 'Invalid format')
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 { instance.cast([6, 5, 4]) }.to raise_error(RuntimeError, 'Invalid format')
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 { instance.cast({a: 1, b: 2, c: 3}) }.to raise_error(RuntimeError, 'Invalid format')
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 { instance.serialize([6, 5, 4]) }.to raise_error(RuntimeError, 'Invalid format')
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
@@ -204,7 +204,8 @@ RSpec.describe 'HasMany' do
204
204
  end
205
205
 
206
206
  context 'with query constraint' do
207
- let(:activity) { Activity.create! }
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)
@@ -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)