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/struct_spec.rb
CHANGED
|
@@ -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
|