torque-postgresql 4.0.1 → 4.1.0
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 +24 -0
- data/lib/torque/postgresql/adapter/database_statements.rb +89 -7
- data/lib/torque/postgresql/adapter/inheritance_statements.rb +297 -0
- data/lib/torque/postgresql/adapter/oid/array.rb +17 -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 +116 -0
- data/lib/torque/postgresql/adapter/oid/line.rb +1 -1
- data/lib/torque/postgresql/adapter/oid/lquery.rb +53 -0
- data/lib/torque/postgresql/adapter/oid/ltree.rb +53 -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/quoting.rb +11 -2
- data/lib/torque/postgresql/adapter/schema_definitions.rb +46 -0
- data/lib/torque/postgresql/adapter/schema_dumper.rb +26 -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 +63 -1
- data/lib/torque/postgresql/arel/visitors.rb +19 -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 +94 -0
- data/lib/torque/postgresql/attributes/composite.rb +102 -0
- data/lib/torque/postgresql/attributes/enum.rb +13 -2
- data/lib/torque/postgresql/attributes/lquery.rb +180 -0
- data/lib/torque/postgresql/attributes/ltree.rb +169 -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 +17 -14
- data/lib/torque/postgresql/config.rb +81 -14
- data/lib/torque/postgresql/inheritance/expander.rb +66 -0
- data/lib/torque/postgresql/inheritance/record.rb +52 -0
- data/lib/torque/postgresql/inheritance.rb +138 -65
- data/lib/torque/postgresql/migration/command_recorder.rb +76 -0
- data/lib/torque/postgresql/predicate_builder/composite_handler.rb +109 -0
- data/lib/torque/postgresql/predicate_builder/ltree_handler.rb +44 -0
- data/lib/torque/postgresql/predicate_builder/struct_handler.rb +68 -0
- data/lib/torque/postgresql/predicate_builder.rb +26 -0
- data/lib/torque/postgresql/predicate_table.rb +61 -0
- data/lib/torque/postgresql/railtie.rb +35 -0
- data/lib/torque/postgresql/relation/inheritance.rb +146 -28
- data/lib/torque/postgresql/relation/merger.rb +21 -5
- data/lib/torque/postgresql/relation.rb +12 -11
- 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 +1 -1
- data/lib/torque/postgresql.rb +6 -0
- data/spec/initialize.rb +20 -2
- data/spec/mocks/cache_query.rb +12 -0
- 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/schema.rb +33 -4
- data/spec/tests/arel_spec.rb +5 -3
- data/spec/tests/composite_spec.rb +800 -0
- data/spec/tests/ltree_spec.rb +552 -0
- data/spec/tests/struct_spec.rb +968 -0
- data/spec/tests/table_inheritance_spec.rb +1027 -56
- metadata +47 -2
|
@@ -0,0 +1,109 @@
|
|
|
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, checked before the
|
|
12
|
+
# attribute itself because it is way cheaper
|
|
13
|
+
def candidate?(value, type)
|
|
14
|
+
return false unless Adapter::OID::Composite.from(type)
|
|
15
|
+
|
|
16
|
+
case value
|
|
17
|
+
when ::Hash, Attributes::Composite then true
|
|
18
|
+
when ::Array then value.any? { |entry| candidate?(entry, type) }
|
|
19
|
+
else false
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Whether the value describes columns, instead of whole records
|
|
24
|
+
def columns?(value)
|
|
25
|
+
value.is_a?(::Hash) || (value.is_a?(::Array) && value.all?(::Hash))
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(predicate_builder)
|
|
30
|
+
@predicate_builder = predicate_builder
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def call(attribute, type, value)
|
|
34
|
+
@composite = Adapter::OID::Composite.from(type)
|
|
35
|
+
array = type.is_a?(ARRAY_OID)
|
|
36
|
+
record = !self.class.columns?(value)
|
|
37
|
+
|
|
38
|
+
return call_for_record(attribute, value, array) if record
|
|
39
|
+
return call_for_array(attribute, value) if array
|
|
40
|
+
|
|
41
|
+
conditions_for(attribute, value)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The type of a single column of the composite type
|
|
45
|
+
def type_of(name)
|
|
46
|
+
columns = @composite.columns
|
|
47
|
+
name = name.to_s
|
|
48
|
+
|
|
49
|
+
raise ArgumentError, <<~MSG.squish unless columns.key?(name)
|
|
50
|
+
Unable to build a condition for "#{name}" because it is not a
|
|
51
|
+
column of the "#{@composite.name}" composite type.
|
|
52
|
+
MSG
|
|
53
|
+
|
|
54
|
+
[columns[name]]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
attr_reader :predicate_builder, :composite
|
|
60
|
+
|
|
61
|
+
# Whole records cannot be sent as anonymous binds, because the
|
|
62
|
+
# comparison operators resolve them to the +record+ pseudo type, so
|
|
63
|
+
# they are always casted to the type they belong to
|
|
64
|
+
def call_for_record(attribute, value, array)
|
|
65
|
+
name = attribute.name
|
|
66
|
+
cast = composite.name
|
|
67
|
+
|
|
68
|
+
return attribute.eq(FN.bind(name, value, composite).pg_cast(cast)) unless array
|
|
69
|
+
|
|
70
|
+
if value.is_a?(::Array)
|
|
71
|
+
list = FN.bind(name, value, ARRAY_OID.new(composite))
|
|
72
|
+
return attribute.overlaps(list.pg_cast(cast, true))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
entry = FN.bind(name, value, composite).pg_cast(cast)
|
|
76
|
+
FN.infix(:"=", entry, FN.any(attribute))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Entries of an array of composite values can only be matched one by
|
|
80
|
+
# one, so the conditions are checked against each unnested entry
|
|
81
|
+
def call_for_array(attribute, value)
|
|
82
|
+
source = Arel::Nodes::Ref.new(composite.name)
|
|
83
|
+
entries = ::Arel::Nodes::TableAlias.new(FN.unnest(attribute), composite.name)
|
|
84
|
+
|
|
85
|
+
manager = ::Arel::SelectManager.new
|
|
86
|
+
manager.from(entries)
|
|
87
|
+
manager.project(::Arel.sql('1'))
|
|
88
|
+
manager.where(conditions_for(source, value))
|
|
89
|
+
manager.exists
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# A list of values means that any of them is a match
|
|
93
|
+
def conditions_for(source, value)
|
|
94
|
+
return group_for(source, value) unless value.is_a?(::Array)
|
|
95
|
+
|
|
96
|
+
groups = value.map { |entry| group_for(source, entry) }
|
|
97
|
+
groups.reduce(:or)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def group_for(source, entry)
|
|
101
|
+
entry = entry.to_h unless entry.is_a?(::Hash)
|
|
102
|
+
table = PredicateTable.new(self, source, Arel::Nodes::Column)
|
|
103
|
+
nodes = predicate_builder.with(table).build_from_hash(entry.stringify_keys)
|
|
104
|
+
::Arel::Nodes::Grouping.new(nodes.reduce(:and))
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Torque
|
|
4
|
+
module PostgreSQL
|
|
5
|
+
module PredicateBuilder
|
|
6
|
+
# A condition over an ltree column is rarely a plain equality, so the value
|
|
7
|
+
# decides which operator to use: a plain path is compared with +=+, while
|
|
8
|
+
# anything carrying an lquery marker is matched with +~+
|
|
9
|
+
class LtreeHandler
|
|
10
|
+
class << self
|
|
11
|
+
# An Array is always a single value here, either the labels of a path
|
|
12
|
+
# or the items of a pattern, so it never means a list of values the
|
|
13
|
+
# way the regular handlers would take it. Objects that describe their
|
|
14
|
+
# own path are claimed as well, otherwise a record would be reduced to
|
|
15
|
+
# its primary key before it had the chance to describe itself
|
|
16
|
+
def candidate?(value, type)
|
|
17
|
+
return false unless type.is_a?(Adapter::OID::Ltree)
|
|
18
|
+
|
|
19
|
+
value.is_a?(::Array) || value.is_a?(LQuery) ||
|
|
20
|
+
LTree.compatible?(value) || LQuery.marker?(value)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(predicate_builder)
|
|
25
|
+
@predicate_builder = predicate_builder
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def call(attribute, type, value)
|
|
29
|
+
return attribute.eq(FN.bind_with(attribute, type.cast(value))) \
|
|
30
|
+
unless LQuery.marker?(value)
|
|
31
|
+
|
|
32
|
+
attribute.matches_lquery(pattern_for(attribute, value))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def pattern_for(attribute, value)
|
|
38
|
+
type = Adapter::OID::Lquery.new
|
|
39
|
+
FN.bind(attribute.name, type.cast(value), type).pg_cast('lquery')
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Torque
|
|
4
|
+
module PostgreSQL
|
|
5
|
+
module PredicateBuilder
|
|
6
|
+
# Turns a hash of properties into a proper set of conditions over the
|
|
7
|
+
# document that holds them, handing each of them back to the predicate
|
|
8
|
+
# builder so that the whole +where+ vocabulary works inside a struct
|
|
9
|
+
class StructHandler
|
|
10
|
+
# Properties are extracted as text, so only the ones whose type has a
|
|
11
|
+
# counterpart on the database are casted before being compared
|
|
12
|
+
PLAIN_TYPES = %i[string text].freeze
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
# Only a hash describes properties, everything else is left alone so
|
|
16
|
+
# that whole documents are still compared as documents
|
|
17
|
+
def candidate?(value, type)
|
|
18
|
+
value.is_a?(::Hash) && type.is_a?(Adapter::OID::Struct) &&
|
|
19
|
+
!type.is_a?(Adapter::OID::StructList)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(predicate_builder)
|
|
24
|
+
@predicate_builder = predicate_builder
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def call(attribute, struct, value)
|
|
28
|
+
raise ArgumentError, <<~MSG.squish unless struct.type == :jsonb
|
|
29
|
+
Unable to build a condition over "#{attribute.name}" because json
|
|
30
|
+
columns cannot be queried by their properties. Use jsonb instead.
|
|
31
|
+
MSG
|
|
32
|
+
|
|
33
|
+
@struct = struct
|
|
34
|
+
table = PredicateTable.new(self, attribute, Arel::Nodes::Property)
|
|
35
|
+
nodes = predicate_builder.with(table).build_from_hash(value.stringify_keys)
|
|
36
|
+
|
|
37
|
+
::Arel::Nodes::Grouping.new(nodes.reduce(:and))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The type of a single property, plus what it has to be casted to once
|
|
41
|
+
# it is extracted from the document as text
|
|
42
|
+
def type_of(name)
|
|
43
|
+
klass = @struct.klass
|
|
44
|
+
name = name.to_s
|
|
45
|
+
|
|
46
|
+
raise ArgumentError, <<~MSG.squish if klass.strict? && !klass.attribute_names.include?(name)
|
|
47
|
+
Unable to build a condition for "#{name}" because it is not a
|
|
48
|
+
declared property of #{klass.name}.
|
|
49
|
+
MSG
|
|
50
|
+
|
|
51
|
+
type = klass.attribute_types[name]
|
|
52
|
+
[type, cast_for(type)]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
attr_reader :predicate_builder
|
|
58
|
+
|
|
59
|
+
def cast_for(type)
|
|
60
|
+
name = type&.type
|
|
61
|
+
return if name.nil? || PLAIN_TYPES.include?(name)
|
|
62
|
+
|
|
63
|
+
ActiveRecord::Base.connection.type_to_sql(name)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'predicate_builder/array_handler'
|
|
4
4
|
|
|
5
|
+
require_relative 'predicate_builder/composite_handler'
|
|
6
|
+
require_relative 'predicate_builder/ltree_handler'
|
|
7
|
+
require_relative 'predicate_builder/struct_handler'
|
|
5
8
|
require_relative 'predicate_builder/regexp_handler'
|
|
6
9
|
require_relative 'predicate_builder/arel_attribute_handler'
|
|
7
10
|
require_relative 'predicate_builder/enumerator_lazy_handler'
|
|
@@ -28,6 +31,29 @@ module Torque
|
|
|
28
31
|
register_handler(::Arel::Attributes::Attribute, ArelAttributeHandler.new(self))
|
|
29
32
|
end
|
|
30
33
|
end
|
|
34
|
+
|
|
35
|
+
# Values described as a hash of columns or properties are turned into
|
|
36
|
+
# conditions over the column that holds them. Whole values are left to
|
|
37
|
+
# the regular handlers, so that they are compared as a single value
|
|
38
|
+
def build(attribute, value, operator = nil)
|
|
39
|
+
type = table.type(attribute.name)
|
|
40
|
+
handler = handler_for_document(type, value)
|
|
41
|
+
return super if handler.nil?
|
|
42
|
+
|
|
43
|
+
handler.new(self).call(attribute, type, value)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def handler_for_document(type, value)
|
|
49
|
+
if PostgreSQL.config.composite.enabled && CompositeHandler.candidate?(value, type)
|
|
50
|
+
CompositeHandler
|
|
51
|
+
elsif PostgreSQL.config.struct.enabled && StructHandler.candidate?(value, type)
|
|
52
|
+
StructHandler
|
|
53
|
+
elsif PostgreSQL.config.ltree.enabled && LtreeHandler.candidate?(value, type)
|
|
54
|
+
LtreeHandler
|
|
55
|
+
end
|
|
56
|
+
end
|
|
31
57
|
end
|
|
32
58
|
|
|
33
59
|
::ActiveRecord::PredicateBuilder.prepend(PredicateBuilder)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Torque
|
|
4
|
+
module PostgreSQL
|
|
5
|
+
# Mimics ActiveRecord::TableMetadata so that the parts of a value that holds
|
|
6
|
+
# other values, like the columns of a composite type or the properties of a
|
|
7
|
+
# document, can be treated as the columns of the value they belong to. That
|
|
8
|
+
# is what allows each of them to be handed back to the predicate builder
|
|
9
|
+
#
|
|
10
|
+
# The handler is the one that knows how to resolve a name, and the node is
|
|
11
|
+
# the one that knows how to reach it
|
|
12
|
+
class PredicateTable
|
|
13
|
+
def initialize(handler, source, node)
|
|
14
|
+
@handler = handler
|
|
15
|
+
@source = source
|
|
16
|
+
@node = node
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def arel_table
|
|
20
|
+
self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def [](name)
|
|
24
|
+
@node.new(@source, name, *@handler.type_of(name))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def type(name)
|
|
28
|
+
@handler.type_of(name).first
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Anything that gets here is meant to be resolved as a part of the value,
|
|
32
|
+
# which is what +type_of+ is there to accept or reject
|
|
33
|
+
def has_column?(*)
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def primary_key
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Rails 8.1 renamed this, so both names answer for the supported versions
|
|
42
|
+
def associated_with(*)
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
alias associated_with? associated_with
|
|
47
|
+
|
|
48
|
+
def aggregated_with?(*)
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def polymorphic_association?
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def through_association?
|
|
57
|
+
false
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -85,6 +85,32 @@ module Torque
|
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
## Struct Enabled Setup
|
|
89
|
+
if (config = torque_config.struct).enabled
|
|
90
|
+
require_relative 'adapter/oid/struct'
|
|
91
|
+
require_relative 'adapter/oid/struct_list'
|
|
92
|
+
require_relative 'adapter/oid/struct_set'
|
|
93
|
+
|
|
94
|
+
require_relative 'attributes/struct'
|
|
95
|
+
|
|
96
|
+
Attributes::Struct.include_on(ActiveRecord::Base)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
## Composite Enabled Setup
|
|
100
|
+
if (config = torque_config.composite).enabled
|
|
101
|
+
require_relative 'adapter/oid/composite'
|
|
102
|
+
require_relative 'attributes/composite'
|
|
103
|
+
|
|
104
|
+
ar_type.register(:composite, Adapter::OID::Composite, adapter: :postgresql)
|
|
105
|
+
|
|
106
|
+
config.namespace ||= ::Object.const_set('Composite', Module.new)
|
|
107
|
+
|
|
108
|
+
# Define a method to find composite classes based on the namespace
|
|
109
|
+
config.namespace.define_singleton_method(:const_missing) do |name|
|
|
110
|
+
Attributes::Composite.lookup(name)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
88
114
|
## Geometry Enabled Setup
|
|
89
115
|
if (config = torque_config.geometry).enabled
|
|
90
116
|
require_relative 'adapter/oid/box'
|
|
@@ -110,6 +136,15 @@ module Torque
|
|
|
110
136
|
ar_type.register(:interval, Adapter::OID::Interval, adapter: :postgresql)
|
|
111
137
|
end
|
|
112
138
|
|
|
139
|
+
## LTree Enabled Setup
|
|
140
|
+
if (config = torque_config.ltree).enabled
|
|
141
|
+
require_relative 'adapter/oid/ltree'
|
|
142
|
+
require_relative 'adapter/oid/lquery'
|
|
143
|
+
|
|
144
|
+
ar_type.register(:ltree, Adapter::OID::Ltree, adapter: :postgresql)
|
|
145
|
+
ar_type.register(:lquery, Adapter::OID::Lquery, adapter: :postgresql)
|
|
146
|
+
end
|
|
147
|
+
|
|
113
148
|
## Full Text Search Enabled Setup
|
|
114
149
|
if (config = torque_config.full_text_search).enabled
|
|
115
150
|
require_relative 'attributes/full_text_search'
|
|
@@ -6,13 +6,33 @@ module Torque
|
|
|
6
6
|
module Inheritance
|
|
7
7
|
|
|
8
8
|
# :nodoc:
|
|
9
|
-
def
|
|
10
|
-
@values.fetch(:
|
|
9
|
+
def expand_records_values
|
|
10
|
+
@values.fetch(:expand_records, FROZEN_EMPTY_ARRAY)
|
|
11
11
|
end
|
|
12
12
|
# :nodoc:
|
|
13
|
-
def
|
|
13
|
+
def expand_records_values=(value)
|
|
14
14
|
assert_modifiable!
|
|
15
|
-
@values[:
|
|
15
|
+
@values[:expand_records] = value
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# :nodoc:
|
|
19
|
+
def expand_records_eager_load_value
|
|
20
|
+
@values.fetch(:expand_records_eager_load, nil)
|
|
21
|
+
end
|
|
22
|
+
# :nodoc:
|
|
23
|
+
def expand_records_eager_load_value=(value)
|
|
24
|
+
assert_modifiable!
|
|
25
|
+
@values[:expand_records_eager_load] = value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# :nodoc:
|
|
29
|
+
def expand_records_scoped_value
|
|
30
|
+
@values.fetch(:expand_records_scoped, nil) || {}
|
|
31
|
+
end
|
|
32
|
+
# :nodoc:
|
|
33
|
+
def expand_records_scoped_value=(value)
|
|
34
|
+
assert_modifiable!
|
|
35
|
+
@values[:expand_records_scoped] = value
|
|
16
36
|
end
|
|
17
37
|
|
|
18
38
|
# :nodoc:
|
|
@@ -25,6 +45,8 @@ module Torque
|
|
|
25
45
|
@values[:itself_only] = value
|
|
26
46
|
end
|
|
27
47
|
|
|
48
|
+
RECORD_CLASS_TOKEN = :_regclass
|
|
49
|
+
|
|
28
50
|
delegate :quote_table_name, :quote_column_name, to: :connection
|
|
29
51
|
|
|
30
52
|
# Specify that the results should come only from the table that the
|
|
@@ -38,46 +60,90 @@ module Torque
|
|
|
38
60
|
|
|
39
61
|
# Like #itself_only, but modifies relation in place.
|
|
40
62
|
def itself_only!(*)
|
|
63
|
+
raise_itself_only_conflict! if expand_records_values.present?
|
|
64
|
+
|
|
41
65
|
self.itself_only_value = true
|
|
42
66
|
self
|
|
43
67
|
end
|
|
44
68
|
|
|
45
|
-
#
|
|
46
|
-
#
|
|
69
|
+
# Load the columns that only exist on the inherited tables, so that
|
|
70
|
+
# records come out complete and writable. Defaults to every dependent
|
|
71
|
+
# that adds columns
|
|
72
|
+
#
|
|
73
|
+
# Activity.expand_records
|
|
74
|
+
# # Runs one additional query per inherited table
|
|
47
75
|
#
|
|
48
|
-
# Activity.
|
|
49
|
-
# #
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
spawn.cast_records!(*types, **options)
|
|
76
|
+
# Activity.expand_records(ActivityBook, eager_load: true)
|
|
77
|
+
# # Runs a single query using outer joins
|
|
78
|
+
def expand_records(*types, **options)
|
|
79
|
+
spawn.expand_records!(*types, **options)
|
|
53
80
|
end
|
|
54
81
|
|
|
55
|
-
# Like #
|
|
56
|
-
def
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
82
|
+
# Like #expand_records, but modifies relation in place
|
|
83
|
+
def expand_records!(*types, eager_load: false, filter: false)
|
|
84
|
+
raise_itself_only_conflict! if itself_only_value === true
|
|
85
|
+
|
|
86
|
+
types = types.presence || model.inheritance_expandable_dependents.values
|
|
87
|
+
|
|
88
|
+
where!(regclass.pg_cast(:varchar).in(types.map(&:table_name))) if filter
|
|
89
|
+
self.expand_records_values = types
|
|
90
|
+
self.expand_records_eager_load_value = eager_load
|
|
60
91
|
self
|
|
61
92
|
end
|
|
62
93
|
|
|
94
|
+
# Accept the record class marker as a column, which is the only way an
|
|
95
|
+
# explicit selection can still produce casted records
|
|
96
|
+
#
|
|
97
|
+
# Activity.select(:_regclass, :id, :title)
|
|
98
|
+
def select(*fields)
|
|
99
|
+
return super if fields.empty?
|
|
100
|
+
return super(*fields, build_record_class_marker) if fields.delete(RECORD_CLASS_TOKEN)
|
|
101
|
+
|
|
102
|
+
super
|
|
103
|
+
end
|
|
104
|
+
|
|
63
105
|
private
|
|
64
106
|
|
|
107
|
+
def raise_itself_only_conflict!
|
|
108
|
+
raise InheritanceError.new(<<~MSG.squish)
|
|
109
|
+
Reading from ONLY a table never returns records from its
|
|
110
|
+
inherited tables, so itself_only and expand_records cannot be
|
|
111
|
+
combined.
|
|
112
|
+
MSG
|
|
113
|
+
end
|
|
114
|
+
|
|
65
115
|
# Hook arel build to add any necessary table
|
|
66
116
|
def build_arel(*)
|
|
67
117
|
arel = super
|
|
68
118
|
arel.only if self.itself_only_value === true
|
|
69
|
-
|
|
119
|
+
|
|
120
|
+
arel.project(build_record_class_marker) if inheritance_discriminated?
|
|
121
|
+
build_inheritances(arel) if self.expand_records_eager_load_value
|
|
70
122
|
arel
|
|
71
123
|
end
|
|
72
124
|
|
|
125
|
+
def inheritance_discriminated?
|
|
126
|
+
return false if self.itself_only_value === true
|
|
127
|
+
return false if select_values.present?
|
|
128
|
+
return false unless from_clause.empty?
|
|
129
|
+
|
|
130
|
+
model.physically_inheritances?
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def build_record_class_marker
|
|
134
|
+
regclass.as(_record_class_column_name)
|
|
135
|
+
end
|
|
136
|
+
|
|
73
137
|
# Build all necessary data for inheritances
|
|
74
138
|
def build_inheritances(arel)
|
|
75
|
-
return if self.
|
|
139
|
+
return if self.expand_records_values.empty?
|
|
76
140
|
|
|
77
|
-
|
|
141
|
+
columns = build_inheritances_joins(arel, self.expand_records_values)
|
|
142
|
+
# The joins are still needed, but an explicit select owns the projection
|
|
143
|
+
return if columns.empty? || select_values.present?
|
|
78
144
|
|
|
79
|
-
|
|
80
|
-
|
|
145
|
+
mergeable = inheritance_mergeable_attributes
|
|
146
|
+
projections = columns.map do |column, arel_tables|
|
|
81
147
|
next arel_tables.first[column] if arel_tables.size == 1
|
|
82
148
|
|
|
83
149
|
if mergeable.include?(column)
|
|
@@ -87,8 +153,7 @@ module Torque
|
|
|
87
153
|
end
|
|
88
154
|
end
|
|
89
155
|
|
|
90
|
-
|
|
91
|
-
self.select_extra_values += columns.flatten if columns.any?
|
|
156
|
+
arel.project(*projections.flatten)
|
|
92
157
|
end
|
|
93
158
|
|
|
94
159
|
# Build as many left outer join as necessary for each dependent table
|
|
@@ -111,15 +176,68 @@ module Torque
|
|
|
111
176
|
columns
|
|
112
177
|
end
|
|
113
178
|
|
|
114
|
-
def build_auto_caster_marker(arel, types)
|
|
115
|
-
attribute = regclass.pg_cast(:varchar).in(types.map(&:table_name))
|
|
116
|
-
attribute.as(self.class._auto_cast_attribute.to_s)
|
|
117
|
-
end
|
|
118
|
-
|
|
119
179
|
def regclass
|
|
120
180
|
arel_table['tableoid'].pg_cast(:regclass)
|
|
121
181
|
end
|
|
122
182
|
|
|
183
|
+
module Expansion
|
|
184
|
+
def preload_associations(records)
|
|
185
|
+
super
|
|
186
|
+
return if expand_records_scoped_value.blank?
|
|
187
|
+
|
|
188
|
+
expand_records_scoped_value.each do |base_model, targets|
|
|
189
|
+
preloaded_association_names.each do |name|
|
|
190
|
+
reflection = model.reflect_on_association(name)
|
|
191
|
+
next if reflection.nil? || reflection.polymorphic? || !(reflection.klass <= base_model)
|
|
192
|
+
|
|
193
|
+
loaded = records.flat_map { |record| record.association(name).target }
|
|
194
|
+
PostgreSQL::Inheritance::Expander.new(reflection.klass, loaded.compact, targets).call
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
private
|
|
200
|
+
|
|
201
|
+
# Records only carry the columns of the queried table, so
|
|
202
|
+
# expanding has to happen after they have been instantiated
|
|
203
|
+
def exec_queries
|
|
204
|
+
records = super
|
|
205
|
+
warn_about_missing_record_class(records)
|
|
206
|
+
return records if expand_records_values.empty? || expand_records_eager_load_value
|
|
207
|
+
|
|
208
|
+
PostgreSQL::Inheritance::Expander.new(model, records, expand_records_values).call
|
|
209
|
+
records.each(&:readonly!) if readonly_value
|
|
210
|
+
records
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def preloaded_association_names
|
|
214
|
+
list = preload_values + (eager_loading? ? [] : includes_values)
|
|
215
|
+
list.flat_map { |entry| entry.is_a?(Hash) ? entry.keys : entry }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Warn once per query that actually lost the real class, instead of
|
|
219
|
+
# once per explicit select that merely omits the marker
|
|
220
|
+
def warn_about_missing_record_class(records)
|
|
221
|
+
return unless model.physically_inheritances?
|
|
222
|
+
return if records.empty? || group_values.present? || distinct_value || select_values.empty?
|
|
223
|
+
return unless from_clause.empty?
|
|
224
|
+
return if itself_only_value === true
|
|
225
|
+
return if select_values.any? { |value| record_class_marker?(value) }
|
|
226
|
+
return if records.any? { |record| record.class != model }
|
|
227
|
+
|
|
228
|
+
warn(<<~MSG.squish)
|
|
229
|
+
#{model.name} was queried with an explicit select that omits
|
|
230
|
+
:_regclass, so its records will not be instantiated as their
|
|
231
|
+
real class.
|
|
232
|
+
MSG
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def record_class_marker?(value)
|
|
236
|
+
return value.right.to_s == _record_class_column_name if value.is_a?(::Arel::Nodes::As)
|
|
237
|
+
value.is_a?(String) && value.include?(_record_class_column_name)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
end
|
|
123
241
|
end
|
|
124
242
|
end
|
|
125
243
|
end
|
|
@@ -46,18 +46,34 @@ module Torque
|
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
# Merge settings related to inheritance tables
|
|
49
|
+
# Merge settings related to inheritance tables, going through the
|
|
50
|
+
# public operations so that their conflicts are still detected
|
|
50
51
|
def merge_inheritance
|
|
51
52
|
return unless relation.is_a?(Relation::Inheritance)
|
|
52
53
|
|
|
53
|
-
relation.
|
|
54
|
+
relation.itself_only! if other.itself_only_value.present?
|
|
54
55
|
|
|
55
|
-
if other.
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
return if other.expand_records_values.blank?
|
|
57
|
+
|
|
58
|
+
if relation.model == other.model
|
|
59
|
+
types = (relation.expand_records_values + other.expand_records_values).uniq
|
|
60
|
+
relation.expand_records!(*types, eager_load: other.expand_records_eager_load_value)
|
|
61
|
+
else
|
|
62
|
+
raise_eager_load_conflict! if other.expand_records_eager_load_value
|
|
63
|
+
|
|
64
|
+
relation.expand_records_scoped_value = relation.expand_records_scoped_value
|
|
65
|
+
.merge(other.model => other.expand_records_values) { |_, a, b| (a + b).uniq }
|
|
58
66
|
end
|
|
59
67
|
end
|
|
60
68
|
|
|
69
|
+
def raise_eager_load_conflict!
|
|
70
|
+
raise InheritanceError.new(<<~MSG.squish)
|
|
71
|
+
Expanding the records of a different model happens through a
|
|
72
|
+
preload, which has no query to eager load the inherited tables
|
|
73
|
+
into, so the two cannot be merged.
|
|
74
|
+
MSG
|
|
75
|
+
end
|
|
76
|
+
|
|
61
77
|
# Merge settings related to buckets
|
|
62
78
|
def merge_buckets
|
|
63
79
|
return unless defined?(Relation::Buckets) && relation.is_a?(Relation::Buckets)
|