@@ -4,22 +4,121 @@ class JoinTree
44 # Stores relationship paths starting from the resource_klass. This allows consolidation of duplicate paths from
55 # relationships, filters and sorts. This enables the determination of table aliases as they are joined.
66
7- attr_reader :resource_klass , :options , :source_relationship
7+ attr_reader :resource_klass , :options , :source_relationship , :resource_joins , :joins
8+
9+ def initialize ( resource_klass :,
10+ options : { } ,
11+ source_relationship : nil ,
12+ relationships : nil ,
13+ filters : nil ,
14+ sort_criteria : nil )
815
9- def initialize ( resource_klass :, options : { } , source_relationship : nil , filters : nil , sort_criteria : nil )
1016 @resource_klass = resource_klass
1117 @options = options
12- @source_relationship = source_relationship
13-
14- @join_relationships = { }
1518
19+ @resource_joins = {
20+ root : {
21+ join_type : :root ,
22+ resource_klasses : {
23+ resource_klass => {
24+ relationships : { }
25+ }
26+ }
27+ }
28+ }
29+ add_source_relationship ( source_relationship )
1630 add_sort_criteria ( sort_criteria )
1731 add_filters ( filters )
32+ add_relationships ( relationships )
33+
34+ @joins = { }
35+ construct_joins ( @resource_joins )
1836 end
1937
20- # A hash of joins that can be used to create the required joins
21- def get_joins
22- walk_relation_node ( @join_relationships )
38+ private
39+
40+ def add_join ( path , default_type = :inner , default_polymorphic_join_type = :left )
41+ if source_relationship
42+ if source_relationship . polymorphic?
43+ # Polymorphic paths will come it with the resource_type as the first segment (for example `#documents.comments`)
44+ # We just need to prepend the relationship portion the
45+ sourced_path = "#{ source_relationship . name } #{ path } "
46+ else
47+ sourced_path = "#{ source_relationship . name } .#{ path } "
48+ end
49+ else
50+ sourced_path = path
51+ end
52+
53+ join_tree , _field = parse_path_to_tree ( sourced_path , resource_klass , default_type , default_polymorphic_join_type )
54+
55+ @resource_joins [ :root ] . deep_merge! ( join_tree ) { |key , val , other_val |
56+ if key == :join_type
57+ if val == other_val
58+ val
59+ else
60+ :inner
61+ end
62+ end
63+ }
64+ end
65+
66+ def process_path_to_tree ( path_segments , resource_klass , default_join_type , default_polymorphic_join_type )
67+ node = {
68+ resource_klasses : {
69+ resource_klass => {
70+ relationships : { }
71+ }
72+ }
73+ }
74+
75+ segment = path_segments . shift
76+
77+ if segment . is_a? ( PathSegment ::Relationship )
78+ node [ :resource_klasses ] [ resource_klass ] [ :relationships ] [ segment . relationship ] ||= { }
79+
80+ # join polymorphic as left joins
81+ node [ :resource_klasses ] [ resource_klass ] [ :relationships ] [ segment . relationship ] [ :join_type ] ||=
82+ segment . relationship . polymorphic? ? default_polymorphic_join_type : default_join_type
83+
84+ segment . relationship . resource_types . each do |related_resource_type |
85+ related_resource_klass = resource_klass . resource_klass_for ( related_resource_type )
86+
87+ # If the resource type was specified in the path segment we want to only process the next segments for
88+ # that resource type, otherwise process for all
89+ process_all_types = !segment . path_specified_resource_klass?
90+
91+ if process_all_types || related_resource_klass == segment . resource_klass
92+ related_resource_tree = process_path_to_tree ( path_segments . dup , related_resource_klass , default_join_type , default_polymorphic_join_type )
93+ node [ :resource_klasses ] [ resource_klass ] [ :relationships ] [ segment . relationship ] . deep_merge! ( related_resource_tree )
94+ end
95+ end
96+ end
97+ node
98+ end
99+
100+ def parse_path_to_tree ( path_string , resource_klass , default_join_type = :inner , default_polymorphic_join_type = :left )
101+ path = JSONAPI ::Path . new ( resource_klass : resource_klass , path_string : path_string )
102+ field = path . segments [ -1 ]
103+ return process_path_to_tree ( path . segments , resource_klass , default_join_type , default_polymorphic_join_type ) , field
104+ end
105+
106+ def add_source_relationship ( source_relationship )
107+ @source_relationship = source_relationship
108+
109+ if @source_relationship
110+ resource_klasses = { }
111+ source_relationship . resource_types . each do |related_resource_type |
112+ related_resource_klass = resource_klass . resource_klass_for ( related_resource_type )
113+ resource_klasses [ related_resource_klass ] = { relationships : { } }
114+ end
115+
116+ join_type = source_relationship . polymorphic? ? :left : :inner
117+
118+ @resource_joins [ :root ] [ :resource_klasses ] [ resource_klass ] [ :relationships ] [ @source_relationship ] = {
119+ source : true , resource_klasses : resource_klasses , join_type : join_type
120+ }
121+ end
23122 end
24123
25124 def add_filters ( filters )
@@ -41,42 +140,10 @@ def add_sort_criteria(sort_criteria)
41140 end
42141 end
43142
44- private
45-
46- def add_join_relationship ( parent_joins , join_name , relation_name , type )
47- parent_joins [ join_name ] ||= { relation_name : relation_name , relationship : { } , type : type }
48- if parent_joins [ join_name ] [ :type ] == :left && type == :inner
49- parent_joins [ join_name ] [ :type ] = :inner
50- end
51- parent_joins [ join_name ] [ :relationship ]
52- end
53-
54- def add_join ( path , default_type = :inner )
55- relationships , _field = resource_klass . parse_relationship_path ( path )
56-
57- current_joins = @join_relationships
58-
59- terminated = false
60-
143+ def add_relationships ( relationships )
144+ return if relationships . blank?
61145 relationships . each do |relationship |
62- if terminated
63- # ToDo: Relax this, if possible
64- # :nocov:
65- warn "Can not nest joins under polymorphic join"
66- # :nocov:
67- end
68-
69- if relationship . polymorphic?
70- relation_names = relationship . polymorphic_relations
71- relation_names . each do |relation_name |
72- join_name = "#{ relationship . name } [#{ relation_name } ]"
73- add_join_relationship ( current_joins , join_name , relation_name , :left )
74- end
75- terminated = true
76- else
77- join_name = relationship . name
78- current_joins = add_join_relationship ( current_joins , join_name , relationship . relation_name ( options ) , default_type )
79- end
146+ add_join ( relationship , :left )
80147 end
81148 end
82149
@@ -92,35 +159,69 @@ def relation_join_hash(path, path_hash = {})
92159 end
93160
94161 # Returns the paths from shortest to longest, allowing the capture of the table alias for earlier paths. For
95- # example posts, posts.comments and then posts.comments.author joined in that order will alow each
162+ # example posts, posts.comments and then posts.comments.author joined in that order will allow each
96163 # alias to be determined whereas just joining posts.comments.author will only record the author alias.
97164 # ToDo: Dependence on this specialized logic should be removed in the future, if possible.
98- def walk_relation_node ( node , paths = { } , current_relation_path = [ ] , current_relationship_path = [ ] )
99- node . each do |key , value |
100- if current_relation_path . empty? && source_relationship
101- current_relation_path << source_relationship . relation_name ( options )
165+ def construct_joins ( node , current_relation_path = [ ] , current_relationship_path = [ ] )
166+ node . each do |relationship , relationship_details |
167+ join_type = relationship_details [ :join_type ]
168+ if relationship == :root
169+ @joins [ :root ] = { alias : resource_klass . _table_name , join_type : :root }
170+
171+ # alias to the default table unless a source_relationship is specified
172+ unless source_relationship
173+ @joins [ '' ] = { alias : resource_klass . _table_name , join_type : :root }
174+ end
175+
176+ return construct_joins ( relationship_details [ :resource_klasses ] . values [ 0 ] [ :relationships ] ,
177+ current_relation_path ,
178+ current_relationship_path )
102179 end
103180
104- current_relation_path << value [ :relation_name ] . to_s
105- current_relationship_path << key . to_s
181+ relationship_details [ :resource_klasses ] . each do |resource_klass , resource_details |
182+ if relationship . polymorphic? && relationship . belongs_to?
183+ current_relationship_path << "#{ relationship . name . to_s } ##{ resource_klass . _type . to_s } "
184+ relation_name = resource_klass . _type . to_s . singularize
185+ else
186+ current_relationship_path << relationship . name . to_s
187+ relation_name = relationship . relation_name ( options ) . to_s
188+ end
106189
107- rel_path = current_relationship_path . join ( '.' )
108- paths [ rel_path ] ||= {
109- alias : nil ,
110- join_type : value [ :type ] ,
111- relation_join_hash : relation_join_hash ( current_relation_path . dup )
112- }
190+ current_relation_path << relation_name
191+
192+ rel_path = calc_path_string ( current_relationship_path )
193+
194+ @joins [ rel_path ] = {
195+ alias : nil ,
196+ join_type : join_type ,
197+ relation_join_hash : relation_join_hash ( current_relation_path . dup )
198+ }
113199
114- walk_relation_node ( value [ :relationship ] ,
115- paths ,
116- current_relation_path ,
117- current_relationship_path )
200+ construct_joins ( resource_details [ :relationships ] ,
201+ current_relation_path . dup ,
202+ current_relationship_path . dup )
118203
119- current_relation_path . pop
120- current_relationship_path . pop
204+ current_relation_path . pop
205+ current_relationship_path . pop
206+ end
121207 end
122- paths
208+ end
209+
210+ def calc_path_string ( path_array )
211+ if source_relationship
212+ if source_relationship . polymorphic?
213+ _relationship_name , resource_name = path_array [ 0 ] . split ( '#' , 2 )
214+ path = path_array . dup
215+ path [ 0 ] = "##{ resource_name } "
216+ else
217+ path = path_array . dup . drop ( 1 )
218+ end
219+ else
220+ path = path_array . dup
221+ end
222+
223+ path . join ( '.' )
123224 end
124225 end
125226 end
126- end
227+ end
0 commit comments