torque-postgresql 4.0.1 → 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 +52 -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 +98 -7
- data/lib/torque/postgresql/adapter/inheritance_statements.rb +297 -0
- data/lib/torque/postgresql/adapter/oid/array.rb +13 -0
- data/lib/torque/postgresql/adapter/oid/box.rb +1 -1
- data/lib/torque/postgresql/adapter/oid/circle.rb +1 -1
- data/lib/torque/postgresql/adapter/oid/composite.rb +117 -0
- 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/line.rb +1 -1
- data/lib/torque/postgresql/adapter/oid/lquery.rb +70 -0
- data/lib/torque/postgresql/adapter/oid/ltree.rb +111 -0
- data/lib/torque/postgresql/adapter/oid/segment.rb +1 -1
- data/lib/torque/postgresql/adapter/oid/struct.rb +150 -0
- data/lib/torque/postgresql/adapter/oid/struct_list.rb +52 -0
- data/lib/torque/postgresql/adapter/oid/struct_set.rb +38 -0
- data/lib/torque/postgresql/adapter/oid.rb +13 -0
- data/lib/torque/postgresql/adapter/quoting.rb +11 -2
- data/lib/torque/postgresql/adapter/schema_definitions.rb +51 -0
- data/lib/torque/postgresql/adapter/schema_dumper.rb +27 -0
- data/lib/torque/postgresql/adapter/schema_statements.rb +207 -0
- data/lib/torque/postgresql/adapter.rb +2 -5
- data/lib/torque/postgresql/arel/nodes.rb +114 -1
- data/lib/torque/postgresql/arel/visitors.rb +65 -8
- data/lib/torque/postgresql/associations/join_dependency.rb +55 -0
- data/lib/torque/postgresql/associations.rb +3 -0
- data/lib/torque/postgresql/attributes/base.rb +97 -0
- data/lib/torque/postgresql/attributes/composite.rb +98 -0
- data/lib/torque/postgresql/attributes/enum.rb +13 -7
- data/lib/torque/postgresql/attributes/lquery.rb +120 -0
- data/lib/torque/postgresql/attributes/ltree.rb +121 -0
- data/lib/torque/postgresql/attributes/simple_enum.rb +72 -0
- data/lib/torque/postgresql/attributes/struct.rb +137 -0
- data/lib/torque/postgresql/base.rb +26 -15
- data/lib/torque/postgresql/config.rb +100 -15
- data/lib/torque/postgresql/geometry_builder.rb +4 -10
- data/lib/torque/postgresql/inheritance/expander.rb +66 -0
- data/lib/torque/postgresql/inheritance/record.rb +52 -0
- data/lib/torque/postgresql/inheritance.rb +141 -68
- data/lib/torque/postgresql/migration/command_recorder.rb +76 -0
- 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 +122 -0
- data/lib/torque/postgresql/predicate_builder/ltree_handler.rb +112 -0
- data/lib/torque/postgresql/predicate_builder/struct_handler.rb +88 -0
- data/lib/torque/postgresql/predicate_builder.rb +53 -0
- data/lib/torque/postgresql/predicate_table.rb +61 -0
- data/lib/torque/postgresql/railtie.rb +52 -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 +145 -28
- data/lib/torque/postgresql/relation/join_series.rb +1 -0
- data/lib/torque/postgresql/relation/merger.rb +30 -5
- data/lib/torque/postgresql/relation/segments.rb +140 -0
- data/lib/torque/postgresql/relation.rb +32 -12
- data/lib/torque/postgresql/schema_cache.rb +1 -0
- data/lib/torque/postgresql/validations.rb +29 -0
- data/lib/torque/postgresql/version.rb +1 -1
- data/lib/torque/postgresql/versioned_commands/command_migration.rb +24 -1
- data/lib/torque/postgresql/versioned_commands.rb +39 -0
- data/lib/torque/postgresql.rb +4 -0
- 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 +29 -2
- data/spec/mocks/cache_query.rb +13 -1
- data/spec/models/author.rb +1 -1
- data/spec/models/comment.rb +2 -0
- data/spec/models/place.rb +2 -0
- data/spec/models/profile.rb +31 -0
- data/spec/rbs_helper.rb +8 -0
- data/spec/schema.rb +35 -4
- data/spec/spec_helper.rb +1 -0
- data/spec/tests/arel_spec.rb +3 -3
- data/spec/tests/auxiliary_statement_spec.rb +12 -0
- data/spec/tests/buckets_spec.rb +122 -0
- data/spec/tests/composite_spec.rb +867 -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 +886 -0
- 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 +1170 -0
- data/spec/tests/table_inheritance_spec.rb +1037 -56
- data/spec/tests/versioned_commands_spec.rb +145 -0
- metadata +198 -2
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'inheritance/record'
|
|
4
|
+
require_relative 'inheritance/expander'
|
|
5
|
+
|
|
3
6
|
module Torque
|
|
4
7
|
module PostgreSQL
|
|
5
8
|
InheritanceError = Class.new(ArgumentError)
|
|
@@ -7,25 +10,12 @@ module Torque
|
|
|
7
10
|
module Inheritance
|
|
8
11
|
extend ActiveSupport::Concern
|
|
9
12
|
|
|
10
|
-
# Cast the given object to its correct class
|
|
11
|
-
def cast_record
|
|
12
|
-
record_class_value = send(self.class._record_class_attribute)
|
|
13
|
-
|
|
14
|
-
return self unless self.class.table_name != record_class_value
|
|
15
|
-
klass = self.class.casted_dependents[record_class_value]
|
|
16
|
-
self.class.raise_unable_to_cast(record_class_value) if klass.nil?
|
|
17
|
-
|
|
18
|
-
# The record need to be re-queried to have its attributes loaded
|
|
19
|
-
# :TODO: Improve this by only loading the necessary extra columns
|
|
20
|
-
klass.find(self.id)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
13
|
class_methods do
|
|
24
|
-
delegate :
|
|
14
|
+
delegate :_record_class_attribute, :_record_class_column_name, to: ActiveRecord::Relation
|
|
25
15
|
|
|
26
16
|
# Get a full list of all attributes from a model and all its dependents
|
|
27
17
|
def inheritance_merged_attributes
|
|
28
|
-
|
|
18
|
+
inheritance_cache(:@inheritance_merged_attributes) do
|
|
29
19
|
children = casted_dependents.values.flat_map(&:attribute_names)
|
|
30
20
|
attribute_names.to_set.merge(children).to_a.freeze
|
|
31
21
|
end
|
|
@@ -34,7 +24,7 @@ module Torque
|
|
|
34
24
|
# Get the list of attributes that can be merged while querying because
|
|
35
25
|
# they all have the same type
|
|
36
26
|
def inheritance_mergeable_attributes
|
|
37
|
-
|
|
27
|
+
inheritance_cache(:@inheritance_mergeable_attributes) do
|
|
38
28
|
base = inheritance_merged_attributes - attribute_names
|
|
39
29
|
types = base.zip(base.size.times.map { [] }).to_h
|
|
40
30
|
|
|
@@ -54,11 +44,11 @@ module Torque
|
|
|
54
44
|
|
|
55
45
|
# Check if the model's table depends on any inheritance
|
|
56
46
|
def physically_inherited?
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
47
|
+
inheritance_cache(:@physically_inherited) do
|
|
48
|
+
connection_pool.schema_cache.dependencies(
|
|
49
|
+
defined?(@table_name) ? @table_name : decorated_table_name,
|
|
50
|
+
).present?
|
|
51
|
+
end
|
|
62
52
|
rescue ActiveRecord::ConnectionNotEstablished
|
|
63
53
|
false
|
|
64
54
|
end
|
|
@@ -66,20 +56,63 @@ module Torque
|
|
|
66
56
|
# Get the list of all tables directly or indirectly dependent of the
|
|
67
57
|
# current one
|
|
68
58
|
def inheritance_dependents
|
|
69
|
-
|
|
59
|
+
connection_pool.schema_cache.associations(table_name) || []
|
|
70
60
|
end
|
|
71
61
|
|
|
72
62
|
# Check whether the model's table has directly or indirectly dependents
|
|
73
63
|
def physically_inheritances?
|
|
74
|
-
inheritance_dependents.present?
|
|
64
|
+
inheritance_cache(:@physically_inheritances) { inheritance_dependents.present? }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The prefix that identifies the columns of this table when they are
|
|
68
|
+
# loaded next to the columns of a sibling table
|
|
69
|
+
def inheritance_column_prefix
|
|
70
|
+
inheritance_cache(:@inheritance_column_prefix) { "#{table_name}__".freeze }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The topmost class of this class' physical inheritance family, which
|
|
74
|
+
# is what casted_dependents needs to be called on to see every
|
|
75
|
+
# dependent in the family rather than just this class' own descendants
|
|
76
|
+
def physically_inheritance_root
|
|
77
|
+
physically_inherited? ? superclass.physically_inheritance_root : self
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The columns that belong to other dependents of this class' physical
|
|
81
|
+
# inheritance family, which must never land on one of its casted
|
|
82
|
+
# records because they belong to a sibling, not to this class
|
|
83
|
+
def inheritance_foreign_attribute_names
|
|
84
|
+
inheritance_cache(:@inheritance_foreign_attribute_names) do
|
|
85
|
+
(physically_inheritance_root.inheritance_merged_attributes - attribute_names).to_set.freeze
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The exact "table_name__" prefixes build_inheritances emits for a
|
|
90
|
+
# non-mergeable column of any dependent in this class' physical
|
|
91
|
+
# inheritance family, which is what a joined sibling column looks like
|
|
92
|
+
def inheritance_dependent_prefixes
|
|
93
|
+
inheritance_cache(:@inheritance_dependent_prefixes) do
|
|
94
|
+
physically_inheritance_root.casted_dependents.keys.map { |name| "#{name}__" }.to_set.freeze
|
|
95
|
+
end
|
|
75
96
|
end
|
|
76
97
|
|
|
77
98
|
# Get the list of all ActiveRecord classes directly or indirectly
|
|
78
99
|
# associated by inheritance
|
|
79
100
|
def casted_dependents
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
101
|
+
inheritance_cache(:@casted_dependents) do
|
|
102
|
+
inheritance_dependents.map do |table_name|
|
|
103
|
+
[table_name, connection_pool.schema_cache.lookup_model(table_name)]
|
|
104
|
+
end.to_h.freeze
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Get the dependents that actually add columns, which are the only ones
|
|
109
|
+
# that produce incomplete records when loaded from this table
|
|
110
|
+
def inheritance_expandable_dependents
|
|
111
|
+
inheritance_cache(:@inheritance_expandable_dependents) do
|
|
112
|
+
casted_dependents.select do |_table_name, klass|
|
|
113
|
+
(klass.attribute_names - attribute_names).any?
|
|
114
|
+
end.freeze
|
|
115
|
+
end
|
|
83
116
|
end
|
|
84
117
|
|
|
85
118
|
# Manually set the model name associated with tables name in order to
|
|
@@ -88,8 +121,8 @@ module Torque
|
|
|
88
121
|
table = super
|
|
89
122
|
|
|
90
123
|
adapter = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
|
|
91
|
-
if Torque::PostgreSQL.config.eager_load &&
|
|
92
|
-
|
|
124
|
+
if Torque::PostgreSQL.config.eager_load && connection_pool.db_config.adapter_class <= adapter
|
|
125
|
+
connection_pool.schema_cache.add_model_name(table, self)
|
|
93
126
|
end
|
|
94
127
|
|
|
95
128
|
table
|
|
@@ -125,73 +158,113 @@ module Torque
|
|
|
125
158
|
physically_inherited? ? decorated_table_name : super
|
|
126
159
|
end
|
|
127
160
|
|
|
128
|
-
# Raises an error message saying that the
|
|
129
|
-
#
|
|
161
|
+
# Raises an error message saying that the given record class could not
|
|
162
|
+
# be instantiated since the model was not identified
|
|
130
163
|
def raise_unable_to_cast(record_class_value)
|
|
131
164
|
raise InheritanceError.new(<<~MSG.squish)
|
|
132
|
-
|
|
165
|
+
A record could not be instantiated as '#{record_class_value}'.
|
|
133
166
|
If this table name doesn't represent a guessable model,
|
|
134
167
|
please use 'Torque::PostgreSQL.conf.irregular_models =
|
|
135
168
|
{ '#{record_class_value}' => 'ModelName' }'.
|
|
136
169
|
MSG
|
|
137
170
|
end
|
|
138
171
|
|
|
172
|
+
protected
|
|
173
|
+
|
|
174
|
+
# Inheritance metadata is derived from the schema, so it must be
|
|
175
|
+
# dropped whenever ActiveRecord drops its own schema-derived caches
|
|
176
|
+
def reload_schema_from_cache(recursive = true)
|
|
177
|
+
@physically_inherited = nil
|
|
178
|
+
@physically_inheritances = nil
|
|
179
|
+
@casted_dependents = nil
|
|
180
|
+
@inheritance_merged_attributes = nil
|
|
181
|
+
@inheritance_mergeable_attributes = nil
|
|
182
|
+
@inheritance_expandable_dependents = nil
|
|
183
|
+
@inheritance_column_prefix = nil
|
|
184
|
+
@inheritance_foreign_attribute_names = nil
|
|
185
|
+
@inheritance_dependent_prefixes = nil
|
|
186
|
+
super
|
|
187
|
+
end
|
|
188
|
+
|
|
139
189
|
private
|
|
140
190
|
|
|
141
|
-
#
|
|
142
|
-
#
|
|
191
|
+
# Memoize using double-checked locking against ActiveRecord's own
|
|
192
|
+
# monitor for loading the schema
|
|
193
|
+
def inheritance_cache(name)
|
|
194
|
+
current = instance_variable_get(name)
|
|
195
|
+
return current unless current.nil?
|
|
196
|
+
|
|
197
|
+
# why: @load_schema_monitor is still nil while AR's inherited reaches base_class
|
|
198
|
+
(@load_schema_monitor ||= Monitor.new).synchronize do
|
|
199
|
+
current = instance_variable_get(name)
|
|
200
|
+
return current unless current.nil?
|
|
201
|
+
|
|
202
|
+
instance_variable_set(name, yield)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Rows coming from a table with dependents carry the table they were
|
|
207
|
+
# stored in, which is what decides the class to instantiate
|
|
143
208
|
def instantiate_instance_of(klass, attributes, types = {}, &block)
|
|
144
209
|
return super unless klass.physically_inheritances?
|
|
145
210
|
|
|
146
|
-
|
|
147
|
-
|
|
211
|
+
marker = _record_class_column_name
|
|
212
|
+
record_class = attributes[marker]
|
|
213
|
+
if record_class.blank? || record_class == klass.table_name
|
|
214
|
+
values, types = sanitize_attributes(klass, attributes, types)
|
|
215
|
+
return super(klass, values, types, &block)
|
|
216
|
+
end
|
|
148
217
|
|
|
149
|
-
|
|
150
|
-
|
|
218
|
+
real_class = klass.casted_dependents[record_class]
|
|
219
|
+
klass.raise_unable_to_cast(record_class) if real_class.nil?
|
|
220
|
+
|
|
221
|
+
values, types = sanitize_attributes(real_class, attributes, types)
|
|
222
|
+
record = super(real_class, values, types, &block)
|
|
223
|
+
|
|
224
|
+
# why: expand_records may already join in real_class's own columns
|
|
225
|
+
if klass.inheritance_expandable_dependents.key?(record_class)
|
|
226
|
+
incomplete = real_class.attribute_names.any? { |name| !values.key?(name) }
|
|
227
|
+
record.send(:mark_as_partial_record!) if incomplete
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
record
|
|
151
231
|
end
|
|
152
232
|
|
|
153
|
-
#
|
|
154
|
-
#
|
|
233
|
+
# Drop what is known to be foreign: the internal record class column
|
|
234
|
+
# and the columns prefixed with another dependent table of the same
|
|
235
|
+
# family, unwrapping our own prefix and letting everything else
|
|
236
|
+
# through, including a user alias that happens to contain '__'
|
|
155
237
|
def sanitize_attributes(real_class, attributes, types)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
238
|
+
prefix = real_class.inheritance_column_prefix
|
|
239
|
+
sibling_prefixes = real_class.inheritance_dependent_prefixes
|
|
240
|
+
foreign = real_class.inheritance_foreign_attribute_names
|
|
241
|
+
skip = _record_class_column_name
|
|
159
242
|
|
|
160
|
-
|
|
243
|
+
new_values = {}
|
|
161
244
|
new_types = {}
|
|
162
245
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
indexes = indexes.each_with_object({}) do |(column, index), new_indexes|
|
|
166
|
-
attribute, prefix = column.split('__', 2).reverse
|
|
167
|
-
current_index = index - dropped
|
|
168
|
-
|
|
169
|
-
if prefix != table_name && skip.include?(attribute)
|
|
170
|
-
row.delete_at(current_index)
|
|
171
|
-
dropped += 1
|
|
172
|
-
else
|
|
173
|
-
new_types.merge!(types.slice(attribute))
|
|
174
|
-
new_types[current_index] = types[index]
|
|
175
|
-
new_indexes[attribute] = current_index
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
[ActiveRecord::Result::IndexedRow.new(indexes, row), new_types]
|
|
180
|
-
end
|
|
246
|
+
attributes.to_hash.each do |column, value|
|
|
247
|
+
next if column == skip || foreign.include?(column)
|
|
181
248
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
249
|
+
name =
|
|
250
|
+
if column.start_with?(prefix)
|
|
251
|
+
column.delete_prefix(prefix)
|
|
252
|
+
elsif sibling_prefixes.any? { |sibling_prefix| column.start_with?(sibling_prefix) }
|
|
253
|
+
next
|
|
254
|
+
else
|
|
255
|
+
column
|
|
256
|
+
end
|
|
186
257
|
|
|
187
|
-
|
|
188
|
-
|
|
258
|
+
new_values[name] = value
|
|
259
|
+
new_types[name] = types[column] if types.key?(column)
|
|
260
|
+
end
|
|
189
261
|
|
|
190
|
-
|
|
262
|
+
[new_values, new_types]
|
|
191
263
|
end
|
|
192
264
|
end
|
|
193
265
|
end
|
|
194
266
|
|
|
195
267
|
ActiveRecord::Base.include Inheritance
|
|
268
|
+
ActiveRecord::Base.include Inheritance::Record
|
|
196
269
|
end
|
|
197
270
|
end
|
|
@@ -15,6 +15,82 @@ module Torque
|
|
|
15
15
|
[:rename_type, args.reverse]
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
# Records the creation of a composite type
|
|
19
|
+
def create_composite_type(*args, &block)
|
|
20
|
+
record(:create_composite_type, args, &block)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Inverts the creation of a composite type
|
|
24
|
+
def invert_create_composite_type(args)
|
|
25
|
+
options = args.second.try(:slice, :schema) || {}
|
|
26
|
+
[:drop_type, [args.first, options]]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Records changes to a composite type, which cannot be inverted for the
|
|
30
|
+
# same reason as +change_table+
|
|
31
|
+
def change_composite_type(*args, &block)
|
|
32
|
+
record(:change_composite_type, args, &block)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Records a column being added to a composite type
|
|
36
|
+
def add_composite_column(*args, &block)
|
|
37
|
+
record(:add_composite_column, args, &block)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Inverts a column being added to a composite type
|
|
41
|
+
def invert_add_composite_column(args)
|
|
42
|
+
[:remove_composite_column, args]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Records a column being removed from a composite type
|
|
46
|
+
def remove_composite_column(*args, &block)
|
|
47
|
+
record(:remove_composite_column, args, &block)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Inverts a column being removed from a composite type, which is only
|
|
51
|
+
# possible when its type was informed
|
|
52
|
+
def invert_remove_composite_column(args)
|
|
53
|
+
raise ActiveRecord::IrreversibleMigration, <<~MSG.squish if args.third.nil?
|
|
54
|
+
remove_composite_column is only reversible if given a type.
|
|
55
|
+
MSG
|
|
56
|
+
|
|
57
|
+
[:add_composite_column, args]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Records the type of a column being changed, which cannot be inverted
|
|
61
|
+
# because the previous type is unknown
|
|
62
|
+
def change_composite_column(*args, &block)
|
|
63
|
+
record(:change_composite_column, args, &block)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Records a column of a composite type being renamed
|
|
67
|
+
def rename_composite_column(*args, &block)
|
|
68
|
+
record(:rename_composite_column, args, &block)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Inverts a column of a composite type being renamed
|
|
72
|
+
def invert_rename_composite_column(args)
|
|
73
|
+
[:rename_composite_column, [args.first, args.third, args.second, *args[3..]]]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Records the spread of features into inherited tables. While reverting,
|
|
77
|
+
# the pruning form goes to the front of the list, which is played back
|
|
78
|
+
# reversed, so the children are only pruned once everything else the
|
|
79
|
+
# migration did to their parents has been undone
|
|
80
|
+
def sync_inheritance_features(*args, &block)
|
|
81
|
+
return record(:sync_inheritance_features, args, &block) unless reverting
|
|
82
|
+
|
|
83
|
+
commands.unshift(inverse_of(:sync_inheritance_features, args, &block))
|
|
84
|
+
end
|
|
85
|
+
ruby2_keywords(:sync_inheritance_features)
|
|
86
|
+
|
|
87
|
+
# Inverts the spread of features by running it again while pruning, so
|
|
88
|
+
# the children converge back to whatever the parent looks like
|
|
89
|
+
def invert_sync_inheritance_features(args)
|
|
90
|
+
options = args.extract_options!.merge(prune: true)
|
|
91
|
+
[:sync_inheritance_features, [*args, Hash.ruby2_keywords_hash(options)]]
|
|
92
|
+
end
|
|
93
|
+
|
|
18
94
|
# Records the creation of a schema
|
|
19
95
|
def create_schema(*args, &block)
|
|
20
96
|
record(:create_schema, args, &block)
|
|
@@ -12,13 +12,17 @@ module Torque
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def call_for_array(attribute, value)
|
|
15
|
-
if
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
return attribute.eq(nil) if value.nil?
|
|
16
|
+
|
|
17
|
+
value = value.to_a if value.is_a?(::Set)
|
|
18
|
+
return call_with_value(attribute, value) unless value.is_a?(::Array)
|
|
19
|
+
return call_with_empty(attribute) if value.empty?
|
|
20
|
+
|
|
21
|
+
nils, values = value.map { |entry| id_of(entry) }.partition(&:nil?)
|
|
22
|
+
return attribute.eq(nil) if values.empty?
|
|
23
|
+
|
|
24
|
+
condition = call_with_array(attribute, values)
|
|
25
|
+
nils.empty? ? condition : condition.or(attribute.eq(nil))
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
private
|
|
@@ -35,8 +39,12 @@ module Torque
|
|
|
35
39
|
FN.cardinality(attribute).eq(0)
|
|
36
40
|
end
|
|
37
41
|
|
|
42
|
+
def id_of(entry)
|
|
43
|
+
entry.is_a?(::ActiveRecord::Base) ? entry.id : entry
|
|
44
|
+
end
|
|
45
|
+
|
|
38
46
|
def array_attribute?(attribute)
|
|
39
|
-
attribute.type_caster.is_a?(ARRAY_OID)
|
|
47
|
+
Adapter::OID.unwrap(attribute.type_caster).is_a?(ARRAY_OID)
|
|
40
48
|
end
|
|
41
49
|
end
|
|
42
50
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Torque
|
|
4
|
+
module PostgreSQL
|
|
5
|
+
module PredicateBuilder
|
|
6
|
+
# Turns a hash of columns into a proper set of conditions over a composite
|
|
7
|
+
# column, handing each of them back to the predicate builder so that the
|
|
8
|
+
# whole +where+ vocabulary works inside a composite
|
|
9
|
+
class CompositeHandler
|
|
10
|
+
class << self
|
|
11
|
+
# Values that this handler knows how to deal with. Without a value,
|
|
12
|
+
# only a single composite has parts that can be reached on their own
|
|
13
|
+
def candidate?(type, value = nil)
|
|
14
|
+
return false unless Adapter::OID::Composite.from(type)
|
|
15
|
+
return !type.is_a?(ARRAY_OID) if value.nil?
|
|
16
|
+
|
|
17
|
+
case value
|
|
18
|
+
when ::Hash, Attributes::Composite then true
|
|
19
|
+
when ::Array then value.compact.any? { |entry| candidate?(type, entry) }
|
|
20
|
+
else false
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Whether the value describes columns, instead of whole records
|
|
25
|
+
def columns?(value)
|
|
26
|
+
value.is_a?(::Hash) || (value.is_a?(::Array) && value.all?(::Hash))
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(predicate_builder)
|
|
31
|
+
@predicate_builder = predicate_builder
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def call(attribute, type, value)
|
|
35
|
+
@composite = Adapter::OID::Composite.from(type)
|
|
36
|
+
array = type.is_a?(ARRAY_OID)
|
|
37
|
+
record = !self.class.columns?(value)
|
|
38
|
+
|
|
39
|
+
return call_for_record(attribute, value, array) if record
|
|
40
|
+
return call_for_array(attribute, value) if array
|
|
41
|
+
|
|
42
|
+
conditions_for(attribute, value)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The columns of the composite value as the columns of a table, so that
|
|
46
|
+
# any of them can be resolved by name
|
|
47
|
+
def table_for(source, type)
|
|
48
|
+
@composite = Adapter::OID::Composite.from(type)
|
|
49
|
+
PredicateTable.new(self, source, Arel::Nodes::Column)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The type of a single column of the composite type
|
|
53
|
+
def type_of(name)
|
|
54
|
+
columns = @composite.columns
|
|
55
|
+
name = name.to_s
|
|
56
|
+
|
|
57
|
+
raise ArgumentError, <<~MSG.squish unless columns.key?(name)
|
|
58
|
+
Unable to build a condition for "#{name}" because it is not a
|
|
59
|
+
column of the "#{@composite.name}" composite type.
|
|
60
|
+
MSG
|
|
61
|
+
|
|
62
|
+
[columns[name]]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
attr_reader :predicate_builder, :composite
|
|
68
|
+
|
|
69
|
+
# Whole records cannot be sent as anonymous binds, because the
|
|
70
|
+
# comparison operators resolve them to the +record+ pseudo type, so
|
|
71
|
+
# they are always casted to the type they belong to
|
|
72
|
+
def call_for_record(attribute, value, array)
|
|
73
|
+
name = attribute.name
|
|
74
|
+
cast = composite.name
|
|
75
|
+
|
|
76
|
+
if !array && value.is_a?(::Array)
|
|
77
|
+
records = value.map { |entry| FN.bind(name, entry, composite).pg_cast(cast) }
|
|
78
|
+
return attribute.in(records)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
return attribute.eq(FN.bind(name, value, composite).pg_cast(cast)) unless array
|
|
82
|
+
|
|
83
|
+
if value.is_a?(::Array)
|
|
84
|
+
list = FN.bind(name, value, ARRAY_OID.new(composite))
|
|
85
|
+
return attribute.overlaps(list.pg_cast(cast, true))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
entry = FN.bind(name, value, composite).pg_cast(cast)
|
|
89
|
+
FN.infix(:"=", entry, FN.any(attribute))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Entries of an array of composite values can only be matched one by
|
|
93
|
+
# one, so the conditions are checked against each unnested entry
|
|
94
|
+
def call_for_array(attribute, value)
|
|
95
|
+
source = Arel::Nodes::Ref.new(composite.name)
|
|
96
|
+
entries = ::Arel::Nodes::TableAlias.new(FN.unnest(attribute), composite.name)
|
|
97
|
+
|
|
98
|
+
manager = ::Arel::SelectManager.new
|
|
99
|
+
manager.from(entries)
|
|
100
|
+
manager.project(::Arel.sql('1'))
|
|
101
|
+
manager.where(conditions_for(source, value))
|
|
102
|
+
manager.exists
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# A list of values means that any of them is a match
|
|
106
|
+
def conditions_for(source, value)
|
|
107
|
+
return group_for(source, value) unless value.is_a?(::Array)
|
|
108
|
+
|
|
109
|
+
groups = value.map { |entry| group_for(source, entry) }
|
|
110
|
+
groups.reduce(:or)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def group_for(source, entry)
|
|
114
|
+
entry = entry.to_h unless entry.is_a?(::Hash)
|
|
115
|
+
table = table_for(source, composite)
|
|
116
|
+
nodes = predicate_builder.with(table).build_from_hash(entry.stringify_keys)
|
|
117
|
+
::Arel::Nodes::Grouping.new(nodes.reduce(:and))
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Torque
|
|
4
|
+
module PostgreSQL
|
|
5
|
+
module PredicateBuilder
|
|
6
|
+
# Serves every column of the ltree extension. A value of the same kind as
|
|
7
|
+
# the column is an equality, a value of the opposite kind is a match, and
|
|
8
|
+
# an Array is a list of values as soon as its first entry is a whole one
|
|
9
|
+
class LtreeHandler
|
|
10
|
+
class << self
|
|
11
|
+
def candidate?(type)
|
|
12
|
+
column_of(type).is_a?(Adapter::OID::Ltree)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def column_of(type)
|
|
16
|
+
type.is_a?(ARRAY_OID) ? type.subtype : type
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(predicate_builder)
|
|
21
|
+
@predicate_builder = predicate_builder
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(attribute, type, value)
|
|
25
|
+
@attribute = attribute
|
|
26
|
+
@column = self.class.column_of(type)
|
|
27
|
+
@array = type.is_a?(ARRAY_OID)
|
|
28
|
+
@list = list?(value)
|
|
29
|
+
|
|
30
|
+
values = list ? value : [value]
|
|
31
|
+
pattern = values.any? { |entry| lquery.cast(entry)&.pattern? }
|
|
32
|
+
pattern == lquery_column? ? equality(values) : match(values)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
attr_reader :attribute, :column, :array, :list
|
|
38
|
+
|
|
39
|
+
def list?(value)
|
|
40
|
+
return false unless value.is_a?(::Array)
|
|
41
|
+
|
|
42
|
+
first = value.first
|
|
43
|
+
value.empty? || first.is_a?(::Array) || first.is_a?(LTree) || first.is_a?(LQuery)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def lquery_column?
|
|
47
|
+
column.is_a?(Adapter::OID::Lquery)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def lquery
|
|
51
|
+
Adapter::OID::Lquery.new
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def ltree
|
|
55
|
+
Adapter::OID::Ltree.new
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Bound as the value the type produces, since Rails deep dups a raw
|
|
59
|
+
# value before compiling the query, and a dup of a record has no id
|
|
60
|
+
def bind(value, type)
|
|
61
|
+
FN.bind(attribute.name, type.cast(value), type)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# PostgreSQL defines no equality operator for lquery, so a pattern
|
|
65
|
+
# column is compared by its text form
|
|
66
|
+
def equality(values)
|
|
67
|
+
target = lquery_column? ? attribute.pg_cast('text', array) : attribute
|
|
68
|
+
return single_equality(target, bind(values.first, column)) unless list
|
|
69
|
+
return empty_equality(target) if values.empty?
|
|
70
|
+
return target.overlaps(bind(values, ARRAY_OID.new(column))) if array
|
|
71
|
+
|
|
72
|
+
binds = values.map { |entry| bind(entry, column) }
|
|
73
|
+
target.in(binds)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def single_equality(target, bind)
|
|
77
|
+
return target.eq(bind) unless array
|
|
78
|
+
|
|
79
|
+
FN.infix(:"=", bind, FN.any(target))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def empty_equality(target)
|
|
83
|
+
return target.in([]) unless array
|
|
84
|
+
|
|
85
|
+
FN.cardinality(attribute).eq(0)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def match(values)
|
|
89
|
+
other = lquery_column? ? ltree : lquery
|
|
90
|
+
cast = other.type.to_s
|
|
91
|
+
return single_match(bind(values.first, other).pg_cast(cast)) unless list
|
|
92
|
+
|
|
93
|
+
list_match(bind(values, ARRAY_OID.new(other)).pg_cast(cast, true))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# PostgreSQL defines ~ for every pair except lquery[] against ltree
|
|
97
|
+
def single_match(bind)
|
|
98
|
+
return attribute.matches_lquery(bind) unless array && lquery_column?
|
|
99
|
+
|
|
100
|
+
FN.infix(:"~", bind, FN.any(attribute))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# PostgreSQL defines ? for every pair except lquery against ltree[]
|
|
104
|
+
def list_match(binds)
|
|
105
|
+
return attribute.matches_any_lquery(binds) if array || !lquery_column?
|
|
106
|
+
|
|
107
|
+
FN.infix(:"~", attribute, FN.any(binds))
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|