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
@@ -786,6 +786,18 @@ RSpec.describe 'Struct' do
786
786
  it 'leaves nil alone' do
787
787
  expect(normalized_klass.new.email).to be_nil
788
788
  end
789
+
790
+ it 'keeps a normalized struct property as a document inside another' do
791
+ klass = Class.new(Torque::PostgreSQL::Attributes::Struct) do
792
+ attribute :bio, Torque::PostgreSQL::Adapter::OID::Struct.new(Profile::Bio)
793
+ normalizes :bio, with: ->(value) { value }
794
+ end
795
+
796
+ type = Torque::PostgreSQL::Adapter::OID::Struct.new(klass)
797
+ value = klass.new(bio: { headline: 'Hi' })
798
+
799
+ expect(type.serialize(value)).to be_eql('{"bio":{"headline":"Hi"}}')
800
+ end
789
801
  end
790
802
 
791
803
  context 'on store accessor' do
@@ -884,6 +896,11 @@ RSpec.describe 'Struct' do
884
896
  expect(Profile.where(settings: { notifications: true }).pluck(:name)).to be_eql(%w[a])
885
897
  end
886
898
 
899
+ it 'casts the property without a permanent connection' do
900
+ expect(ActiveRecord::Base).not_to receive(:connection)
901
+ expect(Profile.where(settings: { notifications: true }).to_sql).to include('::boolean = TRUE')
902
+ end
903
+
887
904
  it 'hands each property back to the predicate builder' do
888
905
  expect(Profile.where(settings: { theme: %w[light dark] }).to_sql)
889
906
  .to include(%{("profiles"."settings" #>> ARRAY['theme']) IN ('light', 'dark')})
@@ -917,6 +934,18 @@ RSpec.describe 'Struct' do
917
934
  expect(model.where(settings: { address: { city: 'SP' } }).pluck(:name)).to be_eql(%w[n])
918
935
  end
919
936
 
937
+ it 'reaches inside a property that is not a document by path' do
938
+ expect(Profile.where(settings: { tags: { 0 => 'test' } }).to_sql)
939
+ .to include(%{("profiles"."settings" #>> ARRAY['tags', '0']) = 'test'})
940
+
941
+ expect(Profile.where(settings: { tags: { first: { name: 'x' } } }).to_sql)
942
+ .to include(%{("profiles"."settings" #>> ARRAY['tags', 'first', 'name']) = 'x'})
943
+
944
+ Profile.create!(name: 'n', settings: { tags: %w[test other] })
945
+ expect(Profile.where(settings: { tags: { 0 => 'test' } }).pluck(:name)).to be_eql(%w[n])
946
+ expect(Profile.where(settings: { tags: { 1 => 'test' } }).pluck(:name)).to be_empty
947
+ end
948
+
920
949
  it 'refuses a json column' do
921
950
  expect { Profile.where(bio: { headline: 'x' }).to_sql }
922
951
  .to raise_error(ArgumentError, /json columns cannot be queried/)
@@ -928,6 +957,139 @@ RSpec.describe 'Struct' do
928
957
  end
929
958
  end
930
959
 
960
+ context 'on arel properties' do
961
+ it 'builds the path to a property' do
962
+ node = Profile.arel_property_of(:settings, :theme)
963
+
964
+ expect(node).to be_a(Torque::PostgreSQL::Arel::Nodes::Property)
965
+ expect(node.path).to be_eql(%w[theme])
966
+ expect(node.cast).to be_nil
967
+ expect(Profile.where(node.eq('dark')).to_sql)
968
+ .to include(%{("profiles"."settings" #>> ARRAY['theme']) = 'dark'})
969
+ end
970
+
971
+ it 'casts the property to what the class declares for it' do
972
+ node = Profile.arel_property_of(:settings, :notifications)
973
+
974
+ expect(node.cast).to be_eql('boolean')
975
+ expect(Profile.order(node.desc).to_sql)
976
+ .to include(%{ORDER BY ("profiles"."settings" #>> ARRAY['notifications'])::boolean DESC})
977
+ end
978
+
979
+ it 'goes as deep as the path says' do
980
+ node = Profile.arel_property_of(:settings, :tags, 0)
981
+
982
+ expect(node.path).to be_eql(%w[tags 0])
983
+ expect(Profile.order(node.asc).to_sql)
984
+ .to include(%{ORDER BY ("profiles"."settings" #>> ARRAY['tags', '0']) ASC})
985
+ end
986
+
987
+ it 'reaches the properties of a nested document' do
988
+ inner = stub_const('ArelInner', Class.new(Torque::PostgreSQL::Attributes::Struct))
989
+ inner.attribute(:zip, :integer)
990
+ outer = stub_const('ArelOuter', Class.new(Torque::PostgreSQL::Attributes::Struct))
991
+ outer.attribute(:address, Torque::PostgreSQL::Adapter::OID::Struct.new(inner))
992
+
993
+ model = stub_const('ArelProfile', Class.new(ActiveRecord::Base))
994
+ model.table_name = 'profiles'
995
+ model.struct_for(:settings, outer)
996
+
997
+ node = model.arel_property_of(:settings, :address, :zip)
998
+ expect(node.cast).to be_eql('integer')
999
+ expect(model.order(node.asc).to_sql)
1000
+ .to include(%{ORDER BY ("profiles"."settings" #>> ARRAY['address', 'zip'])::integer ASC})
1001
+ end
1002
+
1003
+ it 'works on a column that is not backed by a class' do
1004
+ node = Video.arel_property_of(:metadata, :file, :duration)
1005
+
1006
+ expect(node.cast).to be_nil
1007
+ expect(Video.where(node.eq('10')).to_sql)
1008
+ .to include(%{("videos"."metadata" #>> ARRAY['file', 'duration']) = '10'})
1009
+ end
1010
+
1011
+ it 'refuses a json column' do
1012
+ expect { Profile.arel_property_of(:bio, :headline) }
1013
+ .to raise_error(ArgumentError, /json columns cannot be queried/)
1014
+ end
1015
+
1016
+ it 'refuses an undeclared property of a strict class' do
1017
+ expect { Profile.arel_property_of(:settings, :nope) }
1018
+ .to raise_error(ArgumentError, /not a declared property/)
1019
+ end
1020
+
1021
+ it 'keeps the type of the property when the column is normalized' do
1022
+ klass = Class.new(Profile) do
1023
+ normalizes :settings, with: ->(value) { value }
1024
+ end
1025
+
1026
+ node = klass.arel_property_of(:settings, :notifications)
1027
+ expect(node.to_sql).to be_eql(%{("profiles"."settings" #>> ARRAY['notifications'])::boolean})
1028
+
1029
+ expect { klass.arel_property_of(:settings, :nope) }
1030
+ .to raise_error(ArgumentError, /not a declared property/)
1031
+ end
1032
+ end
1033
+
1034
+ context 'on ordering and grouping' do
1035
+ let!(:dark) { Profile.create!(name: 'a', settings: { theme: 'dark', notifications: true }) }
1036
+ let!(:light) { Profile.create!(name: 'b', settings: { theme: 'light', notifications: false }) }
1037
+
1038
+ it 'orders by a property' do
1039
+ expect(Profile.order(settings: { theme: :asc }).to_sql)
1040
+ .to include(%{ORDER BY ("profiles"."settings" #>> ARRAY['theme']) ASC})
1041
+
1042
+ expect(Profile.order(settings: { theme: :asc }).pluck(:name)).to be_eql(%w[a b])
1043
+ expect(Profile.order(settings: { theme: :desc }).pluck(:name)).to be_eql(%w[b a])
1044
+ end
1045
+
1046
+ it 'casts the property to what the class declares for it' do
1047
+ expect(Profile.order(settings: { notifications: :desc }).to_sql)
1048
+ .to include(%{ORDER BY ("profiles"."settings" #>> ARRAY['notifications'])::boolean DESC})
1049
+
1050
+ expect(Profile.order(settings: { notifications: :desc }).pluck(:name)).to be_eql(%w[a b])
1051
+ end
1052
+
1053
+ it 'orders by the whole document' do
1054
+ expect(Profile.order(:settings).to_sql).to include(%{ORDER BY "profiles"."settings" ASC})
1055
+ end
1056
+
1057
+ it 'groups by a property' do
1058
+ expect(Profile.group(settings: :theme).to_sql)
1059
+ .to include(%{GROUP BY ("profiles"."settings" #>> ARRAY['theme'])})
1060
+
1061
+ expect(Profile.group(settings: :theme).count).to be_eql('dark' => 1, 'light' => 1)
1062
+ end
1063
+
1064
+ it 'filters groups with having' do
1065
+ expect(Profile.group(:settings).having(settings: { theme: 'dark' }).to_sql)
1066
+ .to include(%{HAVING (("profiles"."settings" #>> ARRAY['theme']) = 'dark')})
1067
+
1068
+ expect(Profile.group(:settings).having(settings: { theme: 'dark' }).count.size).to be_eql(1)
1069
+ expect(Profile.group(settings: :theme).having(settings: { theme: 'dark' }).count)
1070
+ .to be_eql('dark' => 1)
1071
+ end
1072
+
1073
+ it 'plucks a property' do
1074
+ expect(Profile.order(:name).pluck(settings: :theme)).to be_eql(%w[dark light])
1075
+ expect(Profile.order(:name).pluck('settings.theme')).to be_eql(%w[dark light])
1076
+ end
1077
+
1078
+ it 'refuses a json column' do
1079
+ expect { Profile.order(bio: { headline: :asc }).to_sql }
1080
+ .to raise_error(ArgumentError, /json columns cannot be queried/)
1081
+ end
1082
+
1083
+ it 'refuses an undeclared property of a strict class' do
1084
+ expect { Profile.order(settings: { nope: :asc }).to_sql }
1085
+ .to raise_error(ArgumentError, /not a declared property/)
1086
+ end
1087
+
1088
+ it 'leaves associations alone' do
1089
+ expect(Post.order(author: { name: :asc }).to_sql).to include(%{ORDER BY "author"."name" ASC})
1090
+ end
1091
+ end
1092
+
931
1093
  context 'on encryption options' do
932
1094
  before do
933
1095
  ActiveRecord::Encryption.configure(
@@ -965,4 +1127,44 @@ RSpec.describe 'Struct' do
965
1127
  expect(one).not_to be_eql(two)
966
1128
  end
967
1129
  end
1130
+
1131
+ context 'on filtered parameters' do
1132
+ before do
1133
+ ActiveRecord::Encryption.configure(
1134
+ primary_key: 'test master key',
1135
+ deterministic_key: 'test deterministic key',
1136
+ key_derivation_salt: 'testing salt',
1137
+ )
1138
+ end
1139
+
1140
+ let(:app) { Struct.new(:config).new(Struct.new(:filter_parameters).new([])) }
1141
+
1142
+ around do |example|
1143
+ listeners = ActiveRecord::Encryption.encrypted_attribute_declaration_listeners
1144
+ ActiveRecord::Encryption.encrypted_attribute_declaration_listeners = nil
1145
+ ActiveRecord::Encryption::AutoFilteredParameters.new(app).enable
1146
+ example.run
1147
+ ensure
1148
+ ActiveRecord::Encryption.encrypted_attribute_declaration_listeners = listeners
1149
+ end
1150
+
1151
+ let(:struct_klass) do
1152
+ Class.new(Torque::PostgreSQL::Attributes::Struct) do
1153
+ attribute :label, :string
1154
+ attribute :token, :string
1155
+
1156
+ encrypts :token
1157
+ end
1158
+ end
1159
+
1160
+ it 'records the encrypted attribute on the filter list' do
1161
+ expect(struct_klass.filter_attributes).to include(:token)
1162
+ end
1163
+
1164
+ it 'hides the encrypted attribute when inspecting' do
1165
+ inspected = struct_klass.new(label: 'a', token: 'secret').inspect
1166
+ expect(inspected).to include('a')
1167
+ expect(inspected).not_to include('secret')
1168
+ end
1169
+ end
968
1170
  end
@@ -1419,4 +1419,14 @@ RSpec.describe 'TableInheritance' do
1419
1419
  expect(queries).to be_empty
1420
1420
  end
1421
1421
  end
1422
+
1423
+ context 'on connection' do
1424
+ it 'checks the physical inheritance without a permanent connection' do
1425
+ expect(ActiveRecord::Base).not_to receive(:connection)
1426
+
1427
+ ActivityPost.remove_instance_variable(:@physically_inherited) \
1428
+ if ActivityPost.instance_variable_defined?(:@physically_inherited)
1429
+ expect(ActivityPost.physically_inherited?).to be_truthy
1430
+ end
1431
+ end
1422
1432
  end
@@ -8,6 +8,7 @@ RSpec.describe 'VersionedCommands' do
8
8
  expect(connection).not_to respond_to(:create_function)
9
9
  expect(connection).not_to respond_to(:create_type)
10
10
  expect(connection).not_to respond_to(:create_view)
11
+ expect(connection).not_to respond_to(:create_trigger)
11
12
  end
12
13
 
13
14
  it 'does not have the methods available in a migration' do
@@ -15,6 +16,7 @@ RSpec.describe 'VersionedCommands' do
15
16
  expect(instance).not_to respond_to(:create_function)
16
17
  expect(instance).not_to respond_to(:create_type)
17
18
  expect(instance).not_to respond_to(:create_view)
19
+ expect(instance).not_to respond_to(:create_trigger)
18
20
  end
19
21
 
20
22
  it 'does have the methods in schema definition' do
@@ -22,6 +24,7 @@ RSpec.describe 'VersionedCommands' do
22
24
  expect(instance).to respond_to(:create_function)
23
25
  expect(instance).to respond_to(:create_type)
24
26
  expect(instance).to respond_to(:create_view)
27
+ expect(instance).to respond_to(:create_trigger)
25
28
  end
26
29
 
27
30
  context 'on context' do
@@ -39,6 +42,7 @@ RSpec.describe 'VersionedCommands' do
39
42
  expect(result[4]).to eq('20250101000005_create_view_all_users_v1.sql')
40
43
  expect(result[5]).to eq('20250101000006_create_type_user_id_v1.sql')
41
44
  expect(result[6]).to eq('20250101000007_remove_function_count_users_v2.sql')
45
+ expect(result[7]).to eq('20250101000008_create_trigger_audit_users_v1.sql')
42
46
  end
43
47
 
44
48
  it 'correctly report the status of all migrations' do
@@ -50,6 +54,7 @@ RSpec.describe 'VersionedCommands' do
50
54
  expect(result[4]).to eq(['down', '20250101000005', 'Create View all_users (v1)'])
51
55
  expect(result[5]).to eq(['down', '20250101000006', 'Create Type user_id (v1)'])
52
56
  expect(result[6]).to eq(['down', '20250101000007', 'Remove Function count_users (v2)'])
57
+ expect(result[7]).to eq(['down', '20250101000008', 'Create Trigger audit_users (v1)'])
53
58
  end
54
59
 
55
60
  it 'reports for invalid names' do
@@ -280,6 +285,89 @@ RSpec.describe 'VersionedCommands' do
280
285
  expect { base.validate!(:view, content, 'internal_test') }.not_to raise_error
281
286
  end
282
287
  end
288
+
289
+ context 'on trigger' do
290
+ it 'requires a proper definition' do
291
+ content = <<~SQL
292
+ CREATE OR REPLACE FUNCTION test() RETURNS trigger;
293
+ SQL
294
+
295
+ expect do
296
+ base.validate!(:trigger, content, 'test')
297
+ end.to raise_error(ArgumentError, /Missing or invalid trigger/)
298
+ end
299
+
300
+ it 'prevents multiple trigger definitions' do
301
+ content = <<~SQL
302
+ CREATE OR REPLACE TRIGGER test BEFORE INSERT ON users FOR EACH ROW EXECUTE FUNCTION fn();
303
+ CREATE OR REPLACE TRIGGER other_test BEFORE INSERT ON users FOR EACH ROW EXECUTE FUNCTION fn();
304
+ SQL
305
+
306
+ expect do
307
+ base.validate!(:trigger, content, 'test')
308
+ end.to raise_error(ArgumentError, /More than one trigger/)
309
+ end
310
+
311
+ it 'requires OR REPLACE clause' do
312
+ content = <<~SQL
313
+ CREATE TRIGGER test BEFORE INSERT ON users FOR EACH ROW EXECUTE FUNCTION fn();
314
+ SQL
315
+
316
+ expect do
317
+ base.validate!(:trigger, content, 'test')
318
+ end.to raise_error(ArgumentError, /'OR REPLACE' is required/)
319
+ end
320
+
321
+ it 'requires matching name' do
322
+ content = <<~SQL
323
+ CREATE OR REPLACE TRIGGER other_test BEFORE INSERT ON users FOR EACH ROW EXECUTE FUNCTION fn();
324
+ SQL
325
+
326
+ expect do
327
+ base.validate!(:trigger, content, 'test')
328
+ end.to raise_error(ArgumentError, /must match file name/)
329
+ end
330
+
331
+ it 'works when setup correctly' do
332
+ content = <<~SQL
333
+ CREATE OR REPLACE TRIGGER TEST BEFORE INSERT ON users FOR EACH ROW EXECUTE FUNCTION fn();
334
+ SQL
335
+
336
+ expect { base.validate!(:trigger, content, 'test') }.not_to raise_error
337
+ end
338
+
339
+ it 'supports a bundled function' do
340
+ content = <<~SQL
341
+ CREATE OR REPLACE FUNCTION fn() RETURNS trigger AS $$ BEGIN RETURN NEW; END; $$ LANGUAGE plpgsql;
342
+ CREATE OR REPLACE TRIGGER test BEFORE INSERT ON users FOR EACH ROW EXECUTE FUNCTION fn();
343
+ SQL
344
+
345
+ expect { base.validate!(:trigger, content, 'test') }.not_to raise_error
346
+ end
347
+
348
+ it 'prevents multiple bundled functions' do
349
+ content = <<~SQL
350
+ CREATE OR REPLACE FUNCTION fn() RETURNS trigger;
351
+ CREATE OR REPLACE FUNCTION other_fn() RETURNS trigger;
352
+ CREATE OR REPLACE TRIGGER test BEFORE INSERT ON users FOR EACH ROW EXECUTE FUNCTION fn();
353
+ SQL
354
+
355
+ expect do
356
+ base.validate!(:trigger, content, 'test')
357
+ end.to raise_error(ArgumentError, /one function/)
358
+ end
359
+
360
+ it 'requires OR REPLACE on the bundled function' do
361
+ content = <<~SQL
362
+ CREATE FUNCTION fn() RETURNS trigger;
363
+ CREATE OR REPLACE TRIGGER test BEFORE INSERT ON users FOR EACH ROW EXECUTE FUNCTION fn();
364
+ SQL
365
+
366
+ expect do
367
+ base.validate!(:trigger, content, 'test')
368
+ end.to raise_error(ArgumentError, /'OR REPLACE' is required/)
369
+ end
370
+ end
283
371
  end
284
372
 
285
373
  context 'on running' do
@@ -363,6 +451,34 @@ RSpec.describe 'VersionedCommands' do
363
451
  expect(connection).to receive(:execute).with('DROP VIEW test;')
364
452
  command.migrate(:down)
365
453
  end
454
+
455
+ it 'properly drops triggers' do
456
+ command.type = 'trigger'
457
+
458
+ sql.replace(<<~SQL)
459
+ CREATE OR REPLACE TRIGGER test
460
+ BEFORE INSERT ON users
461
+ FOR EACH ROW EXECUTE FUNCTION fn();
462
+ SQL
463
+
464
+ expect(connection).to receive(:execute).with('DROP TRIGGER test ON users;')
465
+ command.migrate(:down)
466
+ end
467
+
468
+ it 'properly drops triggers with bundled functions' do
469
+ command.type = 'trigger'
470
+
471
+ sql.replace(<<~SQL)
472
+ CREATE OR REPLACE FUNCTION fn() RETURNS trigger AS $$ BEGIN RETURN NEW; END; $$ LANGUAGE plpgsql;
473
+ CREATE OR REPLACE TRIGGER test
474
+ BEFORE INSERT ON internal.users
475
+ FOR EACH ROW EXECUTE FUNCTION fn();
476
+ SQL
477
+
478
+ expect(connection).to receive(:execute).with('DROP TRIGGER test ON internal.users;')
479
+ expect(connection).to receive(:execute).with('DROP FUNCTION fn();')
480
+ command.migrate(:down)
481
+ end
366
482
  end
367
483
 
368
484
  context 'on migrator' do
@@ -449,6 +565,8 @@ RSpec.describe 'VersionedCommands' do
449
565
  end
450
566
 
451
567
  before do
568
+ connection.instance_variable_set(:@schemas_whitelist, nil)
569
+
452
570
  allow(commands_table).to receive(:new).and_return(schema_table)
453
571
  allow(schema_table).to receive(:versions_of).and_return([])
454
572
  allow(schema_table).to receive(:table_name).and_return('versioned_commands_tbl')
@@ -459,6 +577,7 @@ RSpec.describe 'VersionedCommands' do
459
577
  expect(dump_result).not_to include('# These are types managed by versioned commands')
460
578
  expect(dump_result).not_to include('# These are functions managed by versioned commands')
461
579
  expect(dump_result).not_to include('# These are views managed by versioned commands')
580
+ expect(dump_result).not_to include('# These are triggers managed by versioned commands')
462
581
  end
463
582
 
464
583
  it 'includes all types' do
@@ -509,5 +628,31 @@ RSpec.describe 'VersionedCommands' do
509
628
  expect(dump_result).to include('create_view "internal_other", version: 2')
510
629
  expect(dump_result).not_to include('create_view "removed", version: 1')
511
630
  end
631
+
632
+ it 'includes all triggers' do
633
+ body = 'RETURNS trigger AS $$ BEGIN RETURN NEW; END; $$ LANGUAGE plpgsql'
634
+ connection.execute('CREATE TABLE trigger_users (id integer);')
635
+ connection.execute('CREATE TABLE internal.trigger_users (id integer);')
636
+ connection.execute("CREATE FUNCTION trigger_fn() #{body};")
637
+ connection.execute(<<~SQL)
638
+ CREATE TRIGGER test BEFORE INSERT ON trigger_users
639
+ FOR EACH ROW EXECUTE FUNCTION trigger_fn();
640
+ SQL
641
+ connection.execute(<<~SQL)
642
+ CREATE TRIGGER other BEFORE INSERT ON internal.trigger_users
643
+ FOR EACH ROW EXECUTE FUNCTION trigger_fn();
644
+ SQL
645
+
646
+ allow(schema_table).to receive(:versions_of).with('trigger').and_return([
647
+ ['test', 1],
648
+ ['internal_other', 2],
649
+ ['remove', 1],
650
+ ])
651
+
652
+ expect(dump_result).to include('# These are triggers managed by versioned commands')
653
+ expect(dump_result).to include('create_trigger "test", version: 1')
654
+ expect(dump_result).to include('create_trigger "internal_other", version: 2')
655
+ expect(dump_result).not_to include('create_trigger "removed", version: 1')
656
+ end
512
657
  end
513
658
  end