rails_age 0.6.4 → 0.7.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.
@@ -12,24 +12,67 @@ module ApacheAge
12
12
  info.blank? ? "#{label} (#{id})" : "#{info} (#{label})"
13
13
  end
14
14
 
15
- def self.all
16
- all_nodes_sql = <<~SQL
17
- SELECT *
18
- FROM cypher('age_schema', $$
19
- MATCH (node)
20
- RETURN node
21
- $$) as (node agtype);
22
- SQL
23
- age_results = ActiveRecord::Base.connection.execute(all_nodes_sql)
24
- return [] if age_results.values.count.zero?
25
-
26
- age_results.values.map do |result|
27
- json_string = result.first.split('::').first
28
- hash = JSON.parse(json_string)
29
- attribs = hash.slice('id', 'label').symbolize_keys
30
- attribs[:properties] = hash['properties'].symbolize_keys
31
-
32
- new(**attribs)
15
+ class << self
16
+ def age_type = 'vertex'
17
+
18
+ def ensure_query_builder!
19
+ @query_builder ||= ApacheAge::Entities::QueryBuilder.new(@node_class || self)
20
+ end
21
+
22
+ def cypher(node_class: nil, graph_name: nil)
23
+ @node_class = node_class || self
24
+ @query_builder = ApacheAge::Entities::QueryBuilder.new(@node_class, graph_name:)
25
+ self
26
+ end
27
+
28
+ def match(match_string)
29
+ ensure_query_builder!
30
+ @query_builder.match(match_string)
31
+ self
32
+ end
33
+
34
+ def where(*args)
35
+ ensure_query_builder!
36
+ @query_builder.where(*args)
37
+ self
38
+ end
39
+
40
+ def order(ordering)
41
+ ensure_query_builder!
42
+ @query_builder.order(ordering)
43
+ self
44
+ end
45
+
46
+ def limit(limit_value)
47
+ ensure_query_builder!
48
+ @query_builder.limit(limit_value)
49
+ self
50
+ end
51
+
52
+ def return(*variables)
53
+ ensure_query_builder!
54
+ @query_builder.return(*variables)
55
+ self
56
+ end
57
+
58
+ def execute
59
+ ensure_query_builder!
60
+ @query_builder.execute
61
+ end
62
+
63
+ def first
64
+ ensure_query_builder!
65
+ @query_builder.first
66
+ end
67
+
68
+ def all
69
+ ensure_query_builder!
70
+ @query_builder.all
71
+ end
72
+
73
+ def to_sql
74
+ ensure_query_builder!
75
+ @query_builder.to_sql
33
76
  end
34
77
  end
35
78
  end
@@ -1,4 +1,262 @@
1
+ # Query Paths
2
+ # DSL - When all edges are of the same type
3
+ # - Path.cypher(path_edge: HasChild, path_length: "1..5")
4
+ # .where(start_node: {first_name: 'Zeke'})
5
+ # .where('end_node.last_name CONTAINS ?', 'Flintstone')
6
+ # .limit(3)
7
+ # with full control of the matching paths
8
+ # JUST FATHER LINEAGE (where can't handle edge path properties - not one element and get error:
9
+ # `ERROR: array index must resolve to an integer value`
10
+ # so instead match as an edge property instead as shown below
11
+ # - Path.cypher(path_edge: HasChild, path_length: "1..5", path_properties: {guardian_role: 'father'})
12
+ # .where(start_node: {first_name: 'Zeke'})
13
+ # .where('end_node.last_name =~ ?', 'Flintstone')
14
+ # .limit(3)
15
+ # - Path
16
+ # .match('(start_node)-[HasChild*1..5 {guardian_role: 'father'}]->(end_node)')
17
+ # .where(start_node: {first_name: 'Zeke'})
18
+ # .where('end_node.last_name =~ ?', 'Flintstone')
19
+ # .limit(3)
20
+ #
21
+ # # DSL RESULTS:
22
+ # [
23
+ # [
24
+ # Person.find(844424930131969), # Zeke Flintstone
25
+ # Edge.find(1407374883553281), # HasChild(mother)
26
+ # Person.find(844424930131971) # Rockbottom Flintstone
27
+ # ],
28
+ # [
29
+ # Person.find(844424930131969), # Zeke Flintstone
30
+ # Edge.find(1407374883553281), # HasChild(mother)
31
+ # Person.find(844424930131971), # Rockbottom Flintstone
32
+ # Edge.find(1407374883553284), # HasChild(falther)
33
+ # Person.find(844424930131975) # Giggles Flintstone
34
+ # ],
35
+ # [
36
+ # Person.find(844424930131969), # Zeke Flintstone
37
+ # Edge.find(1407374883553281), # HasChild(mother)
38
+ # Person.find(844424930131971), # Rockbottom Flintstone
39
+ # Edge.find(1407374883553283), # HasChild(father)
40
+ # Person.find(844424930131974) # Ed Flintstone
41
+ # ]
42
+ # ]
43
+ # SQL:
44
+ # - SELECT *
45
+ # FROM cypher('age_schema', $$
46
+ # MATCH path = (start_node)-[HasChild*1..5]->(end_node)
47
+ # WHERE start_node.first_name = 'Zeke' AND end_node.last_name CONTAINS 'Flintstone'
48
+ # RETURN path
49
+ # LIMIT 3
50
+ # $$) AS (path agtype);
51
+ #
52
+ # SQL:
53
+ # SELECT *
54
+ # FROM cypher('age_schema', $$
55
+ # MATCH path = (start_node)-[HasChild*1..5 {guardian_role: 'father'}]->(end_node)
56
+ # WHERE start_node.first_name = "Jed" AND end_node.last_name =~ 'Flintstone'
57
+ # RETURN path
58
+ # LIMIT 3
59
+ # $$) AS (path agtype);
60
+ #
61
+ # SQL RESULTS:
62
+ # [
63
+ # {"id": 844424930131969, "label": "Person", "properties": {"gender": "female", "last_name": "Flintstone", "first_name": "Zeke"}}::vertex,
64
+ # {"id": 1407374883553281, "label": "HasChild", "end_id": 844424930131971, "start_id": 844424930131969, "properties": {"guardian_role": "mother"}}::edge,
65
+ # {"id": 844424930131971, "label": "Person", "properties": {"gender": "male", "last_name": "Flintstone", "first_name": "Rockbottom"}}::vertex
66
+ # ]::path
67
+ # [
68
+ # {"id": 844424930131969, "label": "Person", "properties": {"gender": "female", "last_name": "Flintstone", "first_name": "Zeke"}}::vertex,
69
+ # {"id": 1407374883553281, "label": "HasChild", "end_id": 844424930131971, "start_id": 844424930131969, "properties": {"guardian_role": "mother"}}::edge,
70
+ # {"id": 844424930131971, "label": "Person", "properties": {"gender": "male", "last_name": "Flintstone", "first_name": "Rockbottom"}}::vertex,
71
+ # {"id": 1407374883553284, "label": "HasChild", "end_id": 844424930131975, "start_id": 844424930131971, "properties": {"guardian_role": "father"}}::edge,
72
+ # {"id": 844424930131975, "label": "Person", "properties": {"gender": "male", "last_name": "Flintstone", "first_name": "Giggles"}}::vertex
73
+ # ]::path
74
+ # [
75
+ # {"id": 844424930131969, "label": "Person", "properties": {"gender": "female", "last_name": "Flintstone", "first_name": "Zeke"}}::vertex,
76
+ # {"id": 1407374883553281, "label": "HasChild", "end_id": 844424930131971, "start_id": 844424930131969, "properties": {"guardian_role": "mother"}}::edge,
77
+ # {"id": 844424930131971, "label": "Person", "properties": {"gender": "male", "last_name": "Flintstone", "first_name": "Rockbottom"}}::vertex, {"id": 1407374883553283, "label": "HasChild", "end_id": 844424930131974, "start_id": 844424930131971, "properties": {"guardian_role": "father"}}::edge,
78
+ # {"id": 844424930131974, "label": "Person", "properties": {"gender": "male", "last_name": "Flintstone", "first_name": "Ed"}}::vertex
79
+ # ]::path
80
+ # (3 rows)
81
+
1
82
  module ApacheAge
2
83
  class Path
84
+ include ApacheAge::Entities::Path
85
+
86
+ attr_reader :query_builder, :path_edge, :path_length, :path_properties, :start_node_filter, :end_node_filter
87
+
88
+ class << self
89
+ def age_type = 'path'
90
+
91
+ # Convert a path result or collection of path results to hashes
92
+ # This handles both a single path (array of nodes/edges) and multiple paths
93
+ def path_to_h(path_result)
94
+ if path_result.first.is_a?(Array)
95
+ # It's a collection of paths
96
+ path_result.map { |path| path.map(&:to_h) }
97
+ else
98
+ # It's a single path
99
+ path_result.map(&:to_h)
100
+ end
101
+ end
102
+
103
+ # Convert a path result to rich hashes with additional context information
104
+ # This handles both a single path (array of nodes/edges) and multiple paths
105
+ def path_to_rich_h(path_result)
106
+ if path_result.first.is_a?(Array)
107
+ # It's a collection of paths
108
+ path_result.map { |path| path.map(&:to_rich_h) }
109
+ else
110
+ # It's a single path
111
+ path_result.map(&:to_rich_h)
112
+ end
113
+ end
114
+
115
+ def ensure_query_builder!
116
+ @query_builder ||= ApacheAge::Entities::QueryBuilder.new(self)
117
+ end
118
+
119
+ def cypher(path_edge: nil, path_length: nil, path_properties: {}, start_node_filter: nil, end_node_filter: nil, graph_name: nil)
120
+ unless path_edge.nil? || path_edge.ancestors.include?(ApacheAge::Entities::Edge)
121
+ raise ArgumentError, 'Path edge must be a valid edge class'
122
+ end
123
+
124
+ @path_edge = path_edge ? path_edge.name.gsub('::', '__') : nil
125
+ @path_length = path_length ? "*#{path_length}" : "*"
126
+ @path_properties = path_properties.blank? ? nil : " {#{path_properties.map { |k, v| "#{k}: '#{v}'" }.join(', ')}}"
127
+
128
+ # Format node filters for the match clause if provided
129
+ start_filter = format_node_filter(start_node_filter)
130
+ end_filter = format_node_filter(end_node_filter)
131
+
132
+ @has_edge_ordering = false
133
+ @match_clause = "path = (start_node#{start_filter})-[#{@path_edge}#{@path_length}#{@path_properties}]->(end_node#{end_filter})"
134
+ @query_builder =
135
+ ApacheAge::Entities::QueryBuilder.new(
136
+ self,
137
+ graph_name:,
138
+ return_clause: 'path',
139
+ match_clause: @match_clause
140
+ )
141
+ self
142
+ end
143
+
144
+ def match_clause
145
+ # Use the edge variable if we're ordering by edge properties
146
+ if @has_edge_ordering
147
+ "path = (start_node)-[edge:#{@path_edge}#{@path_length}#{@path_properties}]->(end_node)"
148
+ else
149
+ "path = (start_node)-[#{@path_edge}#{@path_length}#{@path_properties}]->(end_node)"
150
+ end
151
+ end
152
+
153
+ def match(match_string)
154
+ ensure_query_builder!
155
+ @query_builder.match(match_string)
156
+ self
157
+ end
158
+
159
+ # Delegate additional methods like `where`, `limit`, etc., to `QueryBuilder`
160
+ def where(*args)
161
+ ensure_query_builder!
162
+ @query_builder.where(*args)
163
+ self
164
+ end
165
+
166
+ # order is important to put last (but before limit)
167
+ # Path.cypher(...).where(...).order("start_node.first_name ASC").limit(5)
168
+ def order(ordering)
169
+ ensure_query_builder!
170
+
171
+ # Check if we're ordering by edge properties
172
+ if ordering.is_a?(Hash) && ordering.key?(:edge)
173
+ # Update match clause to include edge variable
174
+ @match_clause = "path = (start_node)-[edge:#{@path_edge}#{@path_length}#{@path_properties}]->(end_node)"
175
+ @query_builder.match_clause = @match_clause
176
+ end
177
+
178
+ @query_builder.order(ordering)
179
+ self
180
+ end
181
+
182
+ def limit(limit_value)
183
+ ensure_query_builder!
184
+ @query_builder.limit(limit_value)
185
+ self
186
+ end
187
+
188
+ def return(*variables)
189
+ ensure_query_builder!
190
+ @query_builder.return(*variables)
191
+ self
192
+ end
193
+
194
+ # Executes the query and returns results
195
+ def execute
196
+ ensure_query_builder!
197
+ @query_builder.execute
198
+ end
199
+
200
+ def first
201
+ ensure_query_builder!
202
+ @query_builder.first
203
+ end
204
+
205
+ def all
206
+ ensure_query_builder!
207
+ @query_builder.all
208
+ end
209
+
210
+ # Build the final SQL query
211
+ def to_sql
212
+ ensure_query_builder!
213
+ @query_builder.to_sql
214
+ end
215
+
216
+ private
217
+
218
+ # Format node filter hash into Cypher node property syntax {key: 'value', ...}
219
+ # Returns a properly formatted string for insertion into a MATCH clause
220
+ def format_node_filter(filter)
221
+ return '' if filter.nil? || !filter.is_a?(Hash) || filter.empty?
222
+
223
+ properties = filter.map do |key, value|
224
+ formatted_value = value.is_a?(String) ? "'#{value}'" : value
225
+ "#{key}: #{formatted_value}"
226
+ end.join(', ')
227
+
228
+ " {#{properties}}"
229
+ end
230
+
231
+ # PathResult is a custom Array subclass that adds to_rich_h convenience method
232
+ class PathResult < Array
233
+ def to_rich_h
234
+ map(&:to_rich_h)
235
+ end
236
+ end
237
+
238
+ # private
239
+
240
+ def execute_where(cypher_sql)
241
+ age_results = ActiveRecord::Base.connection.execute(cypher_sql)
242
+ return [] if age_results.values.count.zero?
243
+
244
+ age_results.values.map do |row|
245
+ path_data = row.first.split('::path').first
246
+ # [1..-2] - removes leading and trailing brackets
247
+ elements = path_data[1..-2].split(/(?<=\}::vertex,)|(?<=\}::edge,)/)
248
+ # elements.map do |element|
249
+ path_elements = elements.map do |element|
250
+ path_hash = JSON.parse(element.sub("::vertex,", "").sub("::vertex", "").sub("::edge,", "").sub("::edge", "").strip)
251
+ path_klass = path_hash['label'].gsub('__', '::').constantize
252
+ path_attribs = path_hash.except('label', 'properties').merge(path_hash['properties']).symbolize_keys
253
+ path_klass.new(**path_attribs)
254
+ end
255
+
256
+ # Wrap the result in our custom PathResult class
257
+ PathResult.new(path_elements)
258
+ end
259
+ end
260
+ end
3
261
  end
4
262
  end
@@ -1,3 +1,3 @@
1
1
  module RailsAge
2
- VERSION = '0.6.4'
2
+ VERSION = '0.7.0'
3
3
  end
data/lib/rails_age.rb CHANGED
@@ -13,10 +13,10 @@ module ApacheAge
13
13
  require 'apache_age/entities/entity'
14
14
  require 'apache_age/entities/node'
15
15
  require 'apache_age/entities/edge'
16
- # require 'apache_age/entities/path'
16
+ require 'apache_age/entities/path'
17
17
  require 'apache_age/node'
18
18
  require 'apache_age/edge'
19
- # require 'apache_age/path'
19
+ require 'apache_age/path'
20
20
  require 'apache_age/validators/expected_node_type'
21
21
  require 'apache_age/validators/unique_node'
22
22
  require 'apache_age/validators/unique_edge'
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_age
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.4
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bill Tihen
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-10-30 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rails
@@ -171,7 +170,6 @@ metadata:
171
170
  homepage_uri: https://github.com/marpori/rails_age
172
171
  source_code_uri: https://github.com/marpori/rails_age/blob/main
173
172
  changelog_uri: https://github.com/marpori/rails_age/blob/main/CHANGELOG.md
174
- post_install_message:
175
173
  rdoc_options: []
176
174
  require_paths:
177
175
  - lib
@@ -186,8 +184,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
186
184
  - !ruby/object:Gem::Version
187
185
  version: '0'
188
186
  requirements: []
189
- rubygems_version: 3.5.18
190
- signing_key:
187
+ rubygems_version: 3.6.9
191
188
  specification_version: 4
192
189
  summary: Apache AGE plugin for Rails 7.x
193
190
  test_files: []