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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -23
- data/README.md +412 -92
- data/lib/apache_age/edge.rb +63 -0
- data/lib/apache_age/entities/class_methods.rb +69 -60
- data/lib/apache_age/entities/common_methods.rb +26 -0
- data/lib/apache_age/entities/edge.rb +0 -20
- data/lib/apache_age/entities/entity.rb +54 -0
- data/lib/apache_age/entities/node.rb +0 -5
- data/lib/apache_age/entities/path.rb +76 -14
- data/lib/apache_age/entities/query_builder.rb +41 -36
- data/lib/apache_age/node.rb +61 -18
- data/lib/apache_age/path.rb +258 -0
- data/lib/rails_age/version.rb +1 -1
- data/lib/rails_age.rb +2 -2
- metadata +3 -6
data/lib/apache_age/edge.rb
CHANGED
@@ -1,5 +1,68 @@
|
|
1
1
|
module ApacheAge
|
2
2
|
class Edge
|
3
3
|
include ApacheAge::Entities::Edge
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def age_type = 'edge'
|
7
|
+
|
8
|
+
def ensure_query_builder!
|
9
|
+
@query_builder ||= ApacheAge::Entities::QueryBuilder.new(self)
|
10
|
+
end
|
11
|
+
|
12
|
+
def cypher(edge_class, graph_name: nil)
|
13
|
+
@query_builder = ApacheAge::Entities::QueryBuilder.new(edge_class, graph_name:)
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def match(match_string)
|
18
|
+
ensure_query_builder!
|
19
|
+
@query_builder.match(match_string)
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def where(*args)
|
24
|
+
ensure_query_builder!
|
25
|
+
@query_builder.where(*args)
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def order(ordering)
|
30
|
+
ensure_query_builder!
|
31
|
+
@query_builder.order(ordering)
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def limit(limit_value)
|
36
|
+
ensure_query_builder!
|
37
|
+
@query_builder.limit(limit_value)
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
def return(*variables)
|
42
|
+
ensure_query_builder!
|
43
|
+
@query_builder.return(*variables)
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
def execute
|
48
|
+
ensure_query_builder!
|
49
|
+
@query_builder.execute
|
50
|
+
end
|
51
|
+
|
52
|
+
def first
|
53
|
+
ensure_query_builder!
|
54
|
+
@query_builder.first
|
55
|
+
end
|
56
|
+
|
57
|
+
def all
|
58
|
+
ensure_query_builder!
|
59
|
+
@query_builder.all
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_sql
|
63
|
+
ensure_query_builder!
|
64
|
+
@query_builder.to_sql
|
65
|
+
end
|
66
|
+
end
|
4
67
|
end
|
5
68
|
end
|
@@ -18,42 +18,28 @@ module ApacheAge
|
|
18
18
|
def find(id) = where(id: id).first
|
19
19
|
|
20
20
|
def find_by(attributes)
|
21
|
-
return nil if attributes.reject { |
|
21
|
+
return nil if attributes.reject { |_k, v| v.blank? }.empty?
|
22
22
|
|
23
23
|
where(attributes).limit(1).first
|
24
24
|
end
|
25
25
|
|
26
26
|
# Private stuff
|
27
27
|
|
28
|
-
# used? or dead code?
|
29
|
-
def where_edge(attributes)
|
30
|
-
where_attribs =
|
31
|
-
attributes
|
32
|
-
.compact
|
33
|
-
.except(:end_id, :start_id, :end_node, :start_node)
|
34
|
-
.map { |k, v| ActiveRecord::Base.sanitize_sql(["find.#{k} = ?", v]) }
|
35
|
-
.join(' AND ')
|
36
|
-
where_attribs = where_attribs.empty? ? nil : where_attribs
|
37
|
-
|
38
|
-
end_id = attributes[:end_id] || attributes[:end_node]&.id
|
39
|
-
start_id = attributes[:start_id] || attributes[:start_node]&.id
|
40
|
-
where_end_id = end_id ? ActiveRecord::Base.sanitize_sql(["id(end_node) = ?", end_id]) : nil
|
41
|
-
where_start_id = start_id ? ActiveRecord::Base.sanitize_sql(["id(start_node) = ?", start_id]) : nil
|
42
|
-
|
43
|
-
where_clause = [where_attribs, where_start_id, where_end_id].compact.join(' AND ')
|
44
|
-
return nil if where_clause.empty?
|
45
|
-
|
46
|
-
cypher_sql = edge_sql(where_clause)
|
47
|
-
|
48
|
-
execute_where(cypher_sql)
|
49
|
-
end
|
50
|
-
|
51
28
|
def age_graph = 'age_schema'
|
52
29
|
def age_label = name.gsub('::', '__')
|
53
30
|
def age_type = name.constantize.new.age_type
|
54
31
|
|
55
32
|
def match_clause
|
56
|
-
age_type
|
33
|
+
case age_type
|
34
|
+
when 'vertex'
|
35
|
+
# this allows us to Query for all nodes or a specific class of nodes
|
36
|
+
self == ApacheAge::Node ? '(find)' : "(find:#{age_label})"
|
37
|
+
when 'edge'
|
38
|
+
# this allows us to Query for all edges or a specific class of edges
|
39
|
+
self == ApacheAge::Edge ? '(start_node)-[find]->(end_node)' : "(start_node)-[find:#{age_label}]->(end_node)"
|
40
|
+
when 'path'
|
41
|
+
"(start_node)-[edge:#{@path_edge.gsub('::', '__')}*#{@path_length} #{path_properties}]->(end_node)"
|
42
|
+
end
|
57
43
|
end
|
58
44
|
|
59
45
|
def execute_sql(cypher_sql) = ActiveRecord::Base.connection.execute(cypher_sql)
|
@@ -67,47 +53,31 @@ module ApacheAge
|
|
67
53
|
age_results.values.map do |value|
|
68
54
|
json_data = value.first.split('::').first
|
69
55
|
hash = JSON.parse(json_data)
|
56
|
+
# once we have the record we use the label to find the class
|
57
|
+
klass = hash['label'].gsub('__', '::').constantize
|
70
58
|
attribs = hash.except('label', 'properties').merge(hash['properties']).symbolize_keys
|
71
59
|
|
72
|
-
new(
|
60
|
+
# knowing the class and attributes we can create a new instance (wether all results are of the same class or not)
|
61
|
+
# This allows us to return results for, ApacheAge::Node, ApacheAge::Edge, or any specific type of node or edge
|
62
|
+
klass.new(**attribs)
|
73
63
|
end
|
74
64
|
end
|
75
65
|
|
76
|
-
def all_sql
|
77
|
-
<<-SQL
|
78
|
-
SELECT *
|
79
|
-
FROM cypher('#{age_graph}', $$
|
80
|
-
MATCH #{match_clause}
|
81
|
-
RETURN find
|
82
|
-
$$) as (#{age_label} agtype);
|
83
|
-
SQL
|
84
|
-
end
|
85
|
-
|
86
|
-
def node_sql(where_clause)
|
87
|
-
<<-SQL
|
88
|
-
SELECT *
|
89
|
-
FROM cypher('#{age_graph}', $$
|
90
|
-
MATCH #{match_clause}
|
91
|
-
WHERE #{where_clause}
|
92
|
-
RETURN find
|
93
|
-
$$) as (#{age_label} agtype);
|
94
|
-
SQL
|
95
|
-
end
|
96
|
-
|
97
|
-
def edge_sql(where_clause)
|
98
|
-
<<-SQL
|
99
|
-
SELECT *
|
100
|
-
FROM cypher('#{age_graph}', $$
|
101
|
-
MATCH #{match_clause}
|
102
|
-
WHERE #{where_clause}
|
103
|
-
RETURN find
|
104
|
-
$$) as (#{age_label} agtype);
|
105
|
-
SQL
|
106
|
-
end
|
107
|
-
|
108
66
|
private
|
109
67
|
|
110
68
|
def where_node_clause(attributes)
|
69
|
+
# # Make sure we're not treating a simple node attribute query as a start_node hash
|
70
|
+
# # This fixes the issue with where(first_name: 'Barney') getting treated as a path query
|
71
|
+
# if attributes.key?(:start_node) && attributes[:start_node].is_a?(Hash)
|
72
|
+
# # Handle the special case where start_node contains properties
|
73
|
+
# start_node_attrs = attributes[:start_node].map do |k, v|
|
74
|
+
# query_string = k == :id ? "id(find) = ?" : "find.#{k} = ?"
|
75
|
+
# ActiveRecord::Base.sanitize_sql([query_string, v])
|
76
|
+
# end.join(' AND ')
|
77
|
+
# return start_node_attrs
|
78
|
+
# end
|
79
|
+
|
80
|
+
# Normal case - regular node attributes
|
111
81
|
build_core_where_clause(attributes)
|
112
82
|
end
|
113
83
|
|
@@ -144,16 +114,55 @@ module ApacheAge
|
|
144
114
|
.flatten.compact.join(' AND ')
|
145
115
|
end
|
146
116
|
|
117
|
+
# def where_path_clause(attributes)
|
118
|
+
# end
|
147
119
|
|
148
120
|
def build_core_where_clause(attributes)
|
149
121
|
attributes
|
150
122
|
.compact
|
151
123
|
.map do |k, v|
|
152
|
-
|
153
|
-
|
124
|
+
if k == :id
|
125
|
+
"id(find) = #{v}"
|
126
|
+
else
|
127
|
+
# Format the value appropriately based on its type
|
128
|
+
formatted_value = format_for_cypher(k, v)
|
129
|
+
"find.#{k} = #{formatted_value}"
|
130
|
+
end
|
154
131
|
end
|
155
132
|
.join(' AND ')
|
156
133
|
end
|
134
|
+
|
135
|
+
# Formats a value appropriately for use in a Cypher query based on its type
|
136
|
+
def format_for_cypher(attribute_name, value)
|
137
|
+
return 'null' if value.nil?
|
138
|
+
|
139
|
+
# Find the attribute type if possible
|
140
|
+
attribute_type = attribute_types[attribute_name.to_s] if respond_to?(:attribute_types)
|
141
|
+
|
142
|
+
# Format based on Ruby class if no attribute type info is available
|
143
|
+
case
|
144
|
+
when attribute_type.is_a?(ActiveModel::Type::Boolean) || value == true || value == false
|
145
|
+
value.to_s # No quotes for booleans
|
146
|
+
when attribute_type.is_a?(ActiveModel::Type::Integer) || value.is_a?(Integer)
|
147
|
+
value.to_s # No quotes for integers
|
148
|
+
when attribute_type.is_a?(ActiveModel::Type::Float) || attribute_type.is_a?(ActiveModel::Type::Decimal) || value.is_a?(Float) || value.is_a?(BigDecimal)
|
149
|
+
value.to_s # No quotes for floats/decimals
|
150
|
+
when (attribute_type.is_a?(ActiveModel::Type::Date) && (attribute_type.class != ActiveModel::Type::DateTime)) || value.class == Date
|
151
|
+
"'#{value.strftime('%Y-%m-%d')}'" # Format dates as 'YYYY-MM-DD'
|
152
|
+
when (attribute_type.is_a?(ActiveModel::Type::DateTime) && (attribute_type.class != ActiveModel::Type::Date)) || value.is_a?(Time) || value.is_a?(DateTime)
|
153
|
+
utc_time = value.respond_to?(:utc) ? value.utc : value
|
154
|
+
"'#{utc_time.strftime('%Y-%m-%d %H:%M:%S.%6N')}'" # Format datetime (not natively supported in AGE, so formatting is a workaround)
|
155
|
+
# when value.is_a?(Array) || value.is_a?(Hash) || value.is_a?(Json)
|
156
|
+
# # For JSON data, serialize to JSON string and ensure it's properly quoted
|
157
|
+
# "'#{value.to_json.gsub("\'", "\\'")}'"
|
158
|
+
# when value.is_a?(ActiveModel::Type::Array) ||value.is_a?(ActiveModel::Type::Hash) || value.is_a?(ActiveModel::Type::Json)
|
159
|
+
# # For JSON data, serialize to JSON string and ensure it's properly quoted
|
160
|
+
# "'#{value.to_json.gsub("\'", "\\'")}'"
|
161
|
+
else
|
162
|
+
# Default to string treatment with proper escaping
|
163
|
+
"'#{value.to_s.gsub("\'", "\\'")}'"
|
164
|
+
end
|
165
|
+
end
|
157
166
|
end
|
158
167
|
end
|
159
168
|
end
|
@@ -24,6 +24,32 @@ module ApacheAge
|
|
24
24
|
base_h.symbolize_keys
|
25
25
|
end
|
26
26
|
|
27
|
+
# Enhanced hash representation with display information
|
28
|
+
# This provides more context without modifying the core attributes
|
29
|
+
def to_rich_h
|
30
|
+
# Start with the basic id and add the info string
|
31
|
+
result = { _meta: "#{age_label} (#{age_type})", id: id }
|
32
|
+
|
33
|
+
# Group all other attributes under properties
|
34
|
+
properties_hash = {}
|
35
|
+
attributes.to_hash.except('id').each do |key, value|
|
36
|
+
# Skip node objects (we'll handle them specially below)
|
37
|
+
next if %w[start_node end_node].include?(key)
|
38
|
+
|
39
|
+
properties_hash[key.to_sym] = value
|
40
|
+
end
|
41
|
+
|
42
|
+
result[:properties] = properties_hash
|
43
|
+
|
44
|
+
# Handle nested objects for edges
|
45
|
+
if age_type == 'edge'
|
46
|
+
result[:start_node] = start_node.to_rich_h if start_node&.respond_to?(:to_rich_h)
|
47
|
+
result[:end_node] = end_node.to_rich_h if end_node&.respond_to?(:to_rich_h)
|
48
|
+
end
|
49
|
+
|
50
|
+
result
|
51
|
+
end
|
52
|
+
|
27
53
|
def update_attributes(attribs)
|
28
54
|
attribs.except(id:).each do |key, value|
|
29
55
|
send("#{key}=", value) if respond_to?("#{key}=")
|
@@ -47,26 +47,6 @@ module ApacheAge
|
|
47
47
|
errors.add(:end_node, 'invalid') if end_node && !end_node.valid?
|
48
48
|
end
|
49
49
|
|
50
|
-
# Discover attribute class
|
51
|
-
# name_type = model.class.attribute_types['name']
|
52
|
-
# age_type = model.class.attribute_types['age']
|
53
|
-
# company_type = model.class.attribute_types['company']
|
54
|
-
# # Determine the class from the attribute type (for custom types)
|
55
|
-
# name_class = name_type.class # This will generally be ActiveModel::Type::String
|
56
|
-
# age_class = age_type.class # This will generally be ActiveModel::Type::Integer
|
57
|
-
# # For custom types, you may need to look deeper
|
58
|
-
# company_class = company_type.cast_type.class
|
59
|
-
|
60
|
-
# AgeSchema::Edges::HasJob.create(
|
61
|
-
# start_node: fred, end_node: quarry, employee_role: 'Crane Operator'
|
62
|
-
# )
|
63
|
-
# SELECT *
|
64
|
-
# FROM cypher('age_schema', $$
|
65
|
-
# MATCH (start_vertex:Person), (end_vertex:Company)
|
66
|
-
# WHERE id(start_vertex) = 1125899906842634 and id(end_vertex) = 844424930131976
|
67
|
-
# CREATE (start_vertex)-[edge:HasJob {employee_role: 'Crane Operator'}]->(end_vertex)
|
68
|
-
# RETURN edge
|
69
|
-
# $$) as (edge agtype);
|
70
50
|
def create_sql
|
71
51
|
self.start_node = start_node.save unless start_node.persisted?
|
72
52
|
self.end_node = end_node.save unless end_node.persisted?
|
@@ -2,6 +2,10 @@ module ApacheAge
|
|
2
2
|
module Entities
|
3
3
|
class Entity
|
4
4
|
class << self
|
5
|
+
def ensure_query_builder!
|
6
|
+
@query_builder ||= ApacheAge::Entities::QueryBuilder.new(self)
|
7
|
+
end
|
8
|
+
|
5
9
|
def find_by(attributes)
|
6
10
|
where_clause =
|
7
11
|
attributes
|
@@ -18,6 +22,56 @@ module ApacheAge
|
|
18
22
|
|
19
23
|
def find(id) = find_by(id: id)
|
20
24
|
|
25
|
+
def match(match_string)
|
26
|
+
ensure_query_builder!
|
27
|
+
@query_builder.match(match_string)
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
def where(*args)
|
32
|
+
ensure_query_builder!
|
33
|
+
@query_builder.where(*args)
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
def order(ordering)
|
38
|
+
ensure_query_builder!
|
39
|
+
@query_builder.order(ordering)
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def limit(limit_value)
|
44
|
+
ensure_query_builder!
|
45
|
+
@query_builder.limit(limit_value)
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
def return(*variables)
|
50
|
+
ensure_query_builder!
|
51
|
+
@query_builder.return(*variables)
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
def execute
|
56
|
+
ensure_query_builder!
|
57
|
+
@query_builder.execute
|
58
|
+
end
|
59
|
+
|
60
|
+
def first
|
61
|
+
ensure_query_builder!
|
62
|
+
@query_builder.first
|
63
|
+
end
|
64
|
+
|
65
|
+
def all
|
66
|
+
ensure_query_builder!
|
67
|
+
@query_builder.all
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_sql
|
71
|
+
ensure_query_builder!
|
72
|
+
@query_builder.to_sql
|
73
|
+
end
|
74
|
+
|
21
75
|
private
|
22
76
|
|
23
77
|
def age_graph = 'age_schema'
|
@@ -25,10 +25,6 @@ module ApacheAge
|
|
25
25
|
# RETURN company
|
26
26
|
# $$) as (Company agtype);
|
27
27
|
def create_sql
|
28
|
-
# can't use sanitiye without a solution for '_' in the alias name & label
|
29
|
-
# alias_name = ActiveRecord::Base.sanitize_sql_like(age_alias || age_label.downcase)
|
30
|
-
# label_name = ActiveRecord::Base.sanitize_sql_like(age_label)
|
31
|
-
|
32
28
|
alias_name = age_alias || age_label.downcase
|
33
29
|
sanitized_properties =
|
34
30
|
self
|
@@ -69,7 +65,6 @@ module ApacheAge
|
|
69
65
|
$$) as (#{age_label} agtype);
|
70
66
|
SQL
|
71
67
|
end
|
72
|
-
|
73
68
|
end
|
74
69
|
end
|
75
70
|
end
|
@@ -4,23 +4,85 @@ module ApacheAge
|
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
6
|
included do
|
7
|
-
|
8
|
-
include
|
9
|
-
|
7
|
+
extend ApacheAge::Entities::ClassMethods
|
8
|
+
include ApacheAge::Entities::CommonMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
def age_type = 'path'
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
attribute :start_id, :integer
|
15
|
-
# override with a specific node type in the defining class
|
16
|
-
attribute :end_node
|
17
|
-
attribute :start_node
|
13
|
+
def ensure_query_builder!
|
14
|
+
@query_builder ||= ApacheAge::Entities::QueryBuilder.new(self.class)
|
15
|
+
end
|
18
16
|
|
19
|
-
|
20
|
-
|
17
|
+
def match_clause
|
18
|
+
"path = (start_node)-[#{path_edge}#{path_length}#{path_properties}]->(end_node)"
|
19
|
+
end
|
21
20
|
|
22
|
-
|
23
|
-
|
21
|
+
def match(match_string)
|
22
|
+
@match_clause = match_string
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
# Delegate additional methods like `where`, `limit`, etc., to `QueryBuilder`
|
27
|
+
def where(*args)
|
28
|
+
ensure_query_builder!
|
29
|
+
@query_builder.where(*args)
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
def order(ordering)
|
34
|
+
ensure_query_builder!
|
35
|
+
@query_builder.order(ordering)
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
def limit(limit_value)
|
40
|
+
ensure_query_builder!
|
41
|
+
@query_builder.limit(limit_value)
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
def return(*variables)
|
46
|
+
ensure_query_builder!
|
47
|
+
@query_builder.return(*variables)
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
# transforms the query and returns results
|
52
|
+
def execute
|
53
|
+
ensure_query_builder!
|
54
|
+
@query_builder.execute
|
55
|
+
end
|
56
|
+
|
57
|
+
def first
|
58
|
+
ensure_query_builder!
|
59
|
+
@query_builder.first
|
60
|
+
end
|
61
|
+
|
62
|
+
def all
|
63
|
+
ensure_query_builder!
|
64
|
+
@query_builder.all
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_sql
|
68
|
+
ensure_query_builder!
|
69
|
+
@query_builder.to_sql
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def execute_where(cypher_sql)
|
75
|
+
age_results = ActiveRecord::Base.connection.execute(cypher_sql)
|
76
|
+
binding.irb
|
77
|
+
return [] if age_results.values.count.zero?
|
78
|
+
|
79
|
+
age_results.values.map do |value|
|
80
|
+
json_data = value.first.split('::').first
|
81
|
+
hash = JSON.parse(json_data)
|
82
|
+
attribs = hash.except('label', 'properties').merge(hash['properties']).symbolize_keys
|
83
|
+
|
84
|
+
new(**attribs)
|
85
|
+
end
|
24
86
|
end
|
25
87
|
end
|
26
88
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# query =
|
2
|
-
# Person
|
3
|
-
# cypher('age_schema')
|
2
|
+
# Person
|
3
|
+
# .cypher('age_schema')
|
4
4
|
# .match("(a:Person), (b:Person)")
|
5
5
|
# .where("a.name = 'Node A'", "b.name = 'Node B'")
|
6
6
|
# .return("a.name", "b.name")
|
@@ -15,28 +15,27 @@ module ApacheAge
|
|
15
15
|
module Entities
|
16
16
|
class QueryBuilder
|
17
17
|
attr_accessor :where_clauses, :order_clause, :limit_clause, :model_class, :match_clause,
|
18
|
-
:graph_name, :return_clause, :return_names, :return_variables
|
19
|
-
|
20
|
-
|
18
|
+
:graph_name, :return_clause, :return_names, :return_variables,
|
19
|
+
:path_edge_name, :path_length, :path_properties
|
20
|
+
|
21
|
+
def initialize(
|
22
|
+
model_class, return_clause: nil, match_clause: nil, graph_name: nil
|
23
|
+
# model_class, path_edge: nil, path_length: nil, path_properties: nil, return_clause: nil, match_clause: nil, graph_name: nil
|
24
|
+
)
|
25
|
+
# @path_edge = path_length
|
26
|
+
# @path_length = path_length
|
27
|
+
# @path_properties = path_properties
|
21
28
|
@model_class = model_class
|
22
29
|
@where_clauses = []
|
23
|
-
@
|
24
|
-
@
|
30
|
+
@return_clause = return_clause ? return_clause : 'find'
|
31
|
+
@return_names = [@return_clause]
|
25
32
|
@return_variables = []
|
26
33
|
@order_clause = nil
|
27
34
|
@limit_clause = nil
|
28
|
-
@match_clause = model_class.match_clause
|
35
|
+
@match_clause = match_clause ? match_clause : model_class.match_clause
|
29
36
|
@graph_name = graph_name || model_class.age_graph
|
30
37
|
end
|
31
38
|
|
32
|
-
# TODO: allow for multiple graphs
|
33
|
-
# def cypher(graph_name = 'age_schema')
|
34
|
-
# return self if graph_name.blank?
|
35
|
-
|
36
|
-
# @graph_name = graph_name
|
37
|
-
# self
|
38
|
-
# end
|
39
|
-
|
40
39
|
def match(match_string)
|
41
40
|
@match_clause = match_string
|
42
41
|
self
|
@@ -124,31 +123,36 @@ module ApacheAge
|
|
124
123
|
|
125
124
|
private
|
126
125
|
|
127
|
-
#
|
126
|
+
# Handle ordering criteria for paths
|
128
127
|
def parse_ordering(ordering)
|
129
128
|
if ordering.is_a?(Hash)
|
130
|
-
ordering
|
131
|
-
|
132
|
-
|
133
|
-
|
129
|
+
ordering.map do |key, value|
|
130
|
+
if key == :length
|
131
|
+
# Special case for path length
|
132
|
+
"length(path) #{value.to_s.upcase}"
|
133
|
+
elsif key == :start_node || key == :end_node
|
134
|
+
# Node property ordering
|
135
|
+
if value.is_a?(Hash)
|
136
|
+
property = value.keys.first
|
137
|
+
direction = value.values.first.to_s.upcase
|
138
|
+
"#{key}.#{property} #{direction}"
|
139
|
+
else
|
140
|
+
"#{key} #{value.to_s.upcase}"
|
141
|
+
end
|
142
|
+
else
|
143
|
+
# Default case
|
144
|
+
"find.#{key} #{ActiveRecord::Base.sanitize_sql_like(value.to_s)}"
|
145
|
+
end
|
146
|
+
end.join(', ')
|
134
147
|
elsif ordering.is_a?(Symbol)
|
135
|
-
|
148
|
+
# Default for symbol
|
149
|
+
"find.#{ordering}"
|
136
150
|
elsif ordering.is_a?(String)
|
151
|
+
# Pass strings through as-is
|
137
152
|
ordering
|
138
153
|
elsif ordering.is_a?(Array)
|
139
|
-
|
140
|
-
|
141
|
-
order
|
142
|
-
.map { |k, v| "find.#{k} #{ActiveRecord::Base.sanitize_sql_like(v.to_s)}" }
|
143
|
-
.join(', ')
|
144
|
-
elsif order.is_a?(Symbol)
|
145
|
-
"find.#{order}"
|
146
|
-
elsif order.is_a?(String)
|
147
|
-
order
|
148
|
-
else
|
149
|
-
raise ArgumentError, 'Array elements must be a string, symbol, or hash'
|
150
|
-
end
|
151
|
-
end.join(', ')
|
154
|
+
# Process arrays recursively
|
155
|
+
ordering.map { |order| parse_ordering(order) }.join(', ')
|
152
156
|
else
|
153
157
|
raise ArgumentError, 'Ordering must be a string, symbol, hash, or array'
|
154
158
|
end
|
@@ -185,7 +189,8 @@ module ApacheAge
|
|
185
189
|
# Skip transformation if part is one of the logical operators or separators
|
186
190
|
next part if operators.include?(part.strip) || separators.include?(part.strip)
|
187
191
|
|
188
|
-
if
|
192
|
+
# if string contains a dot or is an integer (plus or minus), skip transformation
|
193
|
+
if part.include?(".") || !!(part.strip =~ /\A-?\d+\z/)
|
189
194
|
part # Keep parts with prefixes as they are
|
190
195
|
elsif part =~ /\s*(\w+)\s*$/
|
191
196
|
attribute = $1
|