rails_age 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -4
- data/README.md +120 -6
- data/lib/apache_age/entities/class_methods.rb +112 -0
- data/lib/apache_age/entities/common_methods.rb +118 -0
- data/lib/apache_age/entities/edge.rb +98 -0
- data/lib/apache_age/entities/entity.rb +69 -0
- data/lib/apache_age/entities/vertex.rb +53 -0
- data/lib/apache_age/types/age_type_generator.rb +46 -0
- data/lib/apache_age/validators/unique_edge_validator.rb +53 -0
- data/lib/apache_age/validators/unique_vertex_validator.rb +28 -0
- data/lib/apache_age/validators/vertex_type_validator.rb +14 -0
- data/lib/rails_age/version.rb +1 -1
- data/lib/rails_age.rb +10 -7
- data/lib/tasks/install.rake +91 -0
- metadata +30 -18
- data/lib/apache_age/class_methods.rb +0 -75
- data/lib/apache_age/common_methods.rb +0 -126
- data/lib/apache_age/edge.rb +0 -64
- data/lib/apache_age/entity.rb +0 -67
- data/lib/apache_age/vertex.rb +0 -52
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 349925c4b715d64987c69a6a20b23e67c9076d91152477a976ade763805fdbf7
|
4
|
+
data.tar.gz: 4c7f1e430fe94ac3cb59eac8e6cb94bc03edab9532074c0df75be233b1210973
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: af26f1a10253235703203804e508aa0d0dfbe7e44cc92097f70111587663c9af75e6cfd730fcb0986dc6093bb9bb16c847ab66ad7d3249057d3fe8adf443c78e
|
7
|
+
data.tar.gz: f98980a1937efeef70c1e548b2f62f5b2a7e2f383f707a92e8392a8f45b1bc9ac882a1f478fff0f24f1fcb3cb145e0f037202356ea4a2e76737ee81f4c70b459
|
data/CHANGELOG.md
CHANGED
@@ -1,10 +1,37 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## VERSION 0.3.0 - 2024-xx-xx
|
4
|
+
|
5
|
+
- **cypher**
|
6
|
+
* query support
|
7
|
+
* paths support
|
8
|
+
* select attributes support
|
9
|
+
- **Paths**
|
10
|
+
* ?
|
11
|
+
|
12
|
+
## VERSION 0.2.0 - 2024-05-26
|
13
|
+
|
14
|
+
- **Edges**
|
15
|
+
* add class methods to find_edge(with {properties, end_id, start_id})
|
16
|
+
* add missing methods to use in rails controllers
|
17
|
+
* validate edge start- & end-nodes are valid
|
18
|
+
* add unique edge validations
|
19
|
+
- **Nodes**
|
20
|
+
* add missing methods to use in rails controllers
|
21
|
+
* add unique node validations
|
22
|
+
|
3
23
|
## VERSION 0.1.0 - 2024-05-21
|
4
24
|
|
5
25
|
Initial release has the following features:
|
6
26
|
|
7
|
-
- Nodes
|
8
|
-
|
9
|
-
|
10
|
-
|
27
|
+
- **Nodes:**
|
28
|
+
* Create, Read, Update, Delete & .find(by id), .find_by(age_property), .all
|
29
|
+
* verified with usage in a controller and views
|
30
|
+
- **Edges:**
|
31
|
+
* Create, Read, Update, Delete & .find(by id), .find_by(age_property), .all
|
32
|
+
* verified with usage in a controller and views
|
33
|
+
- **Entities:**
|
34
|
+
* find (by id), find_by(age_property), all; when class/label and/or edge/node is unknown)
|
35
|
+
|
36
|
+
These can be used within Rails applications using a Rails APIs including within controllers and views.
|
37
|
+
See the [README](README.md) for more information.
|
data/README.md
CHANGED
@@ -4,6 +4,8 @@ Simplify Apache Age usage within a Rails application.
|
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
7
|
+
**NOTE:** you must be using Postgres as your database! Apache Age requires it.
|
8
|
+
|
7
9
|
Add this line to your application's Gemfile:
|
8
10
|
|
9
11
|
```ruby
|
@@ -22,6 +24,74 @@ Or install it yourself as:
|
|
22
24
|
$ gem install rails_age
|
23
25
|
```
|
24
26
|
|
27
|
+
finally (tempoarily you need to copy and run the migration)
|
28
|
+
https://github.com/marpori/rails_age/blob/main/db/migrate/20240521062349_configure_apache_age.rb
|
29
|
+
|
30
|
+
```bash
|
31
|
+
# db/migrate/20240521062349_configure_apache_age.rb
|
32
|
+
class ConfigureApacheAge < ActiveRecord::Migration[7.1]
|
33
|
+
def up
|
34
|
+
# Allow age extension
|
35
|
+
execute('CREATE EXTENSION IF NOT EXISTS age;')
|
36
|
+
|
37
|
+
# Load the age code
|
38
|
+
execute("LOAD 'age';")
|
39
|
+
|
40
|
+
# Load the ag_catalog into the search path
|
41
|
+
execute('SET search_path = ag_catalog, "$user", public;')
|
42
|
+
|
43
|
+
# Create age_schema graph if it doesn't exist
|
44
|
+
execute("SELECT create_graph('age_schema');")
|
45
|
+
end
|
46
|
+
|
47
|
+
def down
|
48
|
+
execute <<-SQL
|
49
|
+
DO $$
|
50
|
+
BEGIN
|
51
|
+
IF EXISTS (
|
52
|
+
SELECT 1
|
53
|
+
FROM pg_constraint
|
54
|
+
WHERE conname = 'fk_graph_oid'
|
55
|
+
) THEN
|
56
|
+
ALTER TABLE ag_catalog.ag_label
|
57
|
+
DROP CONSTRAINT fk_graph_oid;
|
58
|
+
END IF;
|
59
|
+
END $$;
|
60
|
+
SQL
|
61
|
+
|
62
|
+
execute("SELECT drop_graph('age_schema', true);")
|
63
|
+
execute('DROP SCHEMA IF EXISTS ag_catalog CASCADE;')
|
64
|
+
execute('DROP EXTENSION IF EXISTS age;')
|
65
|
+
end
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
and fix the TOP of `schema.rb` file to match the following (note: the version number should be the same as the LARGEST version number in your `db/migrations` folder)
|
70
|
+
https://github.com/marpori/rails_age/blob/main/db/schema.rb
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
# db/schema.rb
|
74
|
+
ActiveRecord::Schema[7.1].define(version: 2024_05_21_062349) do
|
75
|
+
# These are extensions that must be enabled in order to support this database
|
76
|
+
enable_extension "plpgsql"
|
77
|
+
|
78
|
+
# Allow age extension
|
79
|
+
execute('CREATE EXTENSION IF NOT EXISTS age;')
|
80
|
+
|
81
|
+
# Load the age code
|
82
|
+
execute("LOAD 'age';")
|
83
|
+
|
84
|
+
# Load the ag_catalog into the search path
|
85
|
+
execute('SET search_path = ag_catalog, "$user", public;')
|
86
|
+
|
87
|
+
# Create age_schema graph if it doesn't exist
|
88
|
+
execute("SELECT create_graph('age_schema');")
|
89
|
+
|
90
|
+
# other migrations
|
91
|
+
# ...
|
92
|
+
end
|
93
|
+
```
|
94
|
+
|
25
95
|
## Contributing
|
26
96
|
|
27
97
|
Create an MR and tests and I will review it.
|
@@ -34,7 +104,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
34
104
|
|
35
105
|
I suggest you creat a folder within app called `graphs` and under that create a folder called `nodes` and `edges`. This will help you keep your code organized.
|
36
106
|
|
37
|
-
A full sample app can be found [here](https://github.com/
|
107
|
+
A full sample app can be found [here](https://github.com/marpori/rails_age_demo_app) the summary usage is described below.
|
38
108
|
|
39
109
|
### Nodes
|
40
110
|
|
@@ -42,7 +112,7 @@ A full sample app can be found [here](https://github.com/btihen-dev/rails_graphd
|
|
42
112
|
# app/graphs/nodes/company.rb
|
43
113
|
module Nodes
|
44
114
|
class Company
|
45
|
-
include ApacheAge::Vertex
|
115
|
+
include ApacheAge::Entities::Vertex
|
46
116
|
|
47
117
|
attribute :company_name, :string
|
48
118
|
validates :company_name, presence: true
|
@@ -54,7 +124,7 @@ end
|
|
54
124
|
# app/graphs/nodes/person.rb
|
55
125
|
module Nodes
|
56
126
|
class Person
|
57
|
-
include ApacheAge::Vertex
|
127
|
+
include ApacheAge::Entities::Vertex
|
58
128
|
|
59
129
|
attribute :first_name, :string, default: nil
|
60
130
|
attribute :last_name, :string, default: nil
|
@@ -80,10 +150,12 @@ end
|
|
80
150
|
```ruby
|
81
151
|
# app/graphs/edges/works_at.rb
|
82
152
|
module Edges
|
83
|
-
class
|
84
|
-
include ApacheAge::Edge
|
153
|
+
class HasJob
|
154
|
+
include ApacheAge::Entities::Edge
|
85
155
|
|
86
156
|
attribute :employee_role, :string
|
157
|
+
attribute :start_node, :person # if using optional age types
|
158
|
+
# attribute :end_node, :person # if using optional age types
|
87
159
|
validates :employee_role, presence: true
|
88
160
|
end
|
89
161
|
end
|
@@ -98,10 +170,46 @@ fred.to_h
|
|
98
170
|
quarry = Nodes::Company.create(company_name: 'Bedrock Quarry')
|
99
171
|
quarry.to_h
|
100
172
|
|
101
|
-
job = Edges::
|
173
|
+
job = Edges::HasJob.create(start_node: fred, end_node: quarry, employee_role: 'Crane Operator')
|
102
174
|
job.to_h
|
103
175
|
```
|
104
176
|
|
177
|
+
### Update Routes
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
Rails.application.routes.draw do
|
181
|
+
# mount is not needed with the engine
|
182
|
+
# mount RailsAge::Engine => "/rails_age"
|
183
|
+
|
184
|
+
# defines the route for the people controller
|
185
|
+
resources :people
|
186
|
+
|
187
|
+
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
188
|
+
# Can be used by load balancers and uptime monitors to verify that the app is live.
|
189
|
+
get 'up' => 'rails/health#show', as: :rails_health_check
|
190
|
+
|
191
|
+
# Defines the root path route ("/")
|
192
|
+
root 'people#index'
|
193
|
+
end
|
194
|
+
```
|
195
|
+
|
196
|
+
### Types (Optional)
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
# spec/dummy/config/initializers/types.rb
|
200
|
+
require 'apache_age/types/age_type_generator'
|
201
|
+
|
202
|
+
Rails.application.config.to_prepare do
|
203
|
+
# Ensure the files are loaded
|
204
|
+
require_dependency 'nodes/company'
|
205
|
+
require_dependency 'nodes/person'
|
206
|
+
|
207
|
+
# Register the custom types
|
208
|
+
ActiveModel::Type.register(:company, ApacheAge::Types::AgeTypeGenerator.create_type_for(Nodes::Company))
|
209
|
+
ActiveModel::Type.register(:person, ApacheAge::Types::AgeTypeGenerator.create_type_for(Nodes::Person))
|
210
|
+
end
|
211
|
+
```
|
212
|
+
|
105
213
|
### Controller Usage
|
106
214
|
|
107
215
|
```ruby
|
@@ -176,3 +284,9 @@ class PeopleController < ApplicationController
|
|
176
284
|
end
|
177
285
|
end
|
178
286
|
```
|
287
|
+
|
288
|
+
### Views
|
289
|
+
|
290
|
+
```erb
|
291
|
+
|
292
|
+
```
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module ApacheAge
|
2
|
+
module Entities
|
3
|
+
module ClassMethods
|
4
|
+
# for now we only allow one predertimed graph
|
5
|
+
def create(attributes)
|
6
|
+
instance = new(**attributes)
|
7
|
+
instance.save
|
8
|
+
instance
|
9
|
+
end
|
10
|
+
|
11
|
+
def find_edge(attributes)
|
12
|
+
where_attribs =
|
13
|
+
attributes
|
14
|
+
.compact
|
15
|
+
.except(:end_id, :start_id, :end_node, :start_node)
|
16
|
+
.map { |k, v| "find.#{k} = '#{v}'" }.join(' AND ')
|
17
|
+
where_attribs = where_attribs.empty? ? nil : where_attribs
|
18
|
+
|
19
|
+
end_id = attributes[:end_id] || attributes[:end_node]&.id
|
20
|
+
start_id = attributes[:start_id] || attributes[:start_node]&.id
|
21
|
+
where_end_id = end_id ? "id(end_node) = #{end_id}" : nil
|
22
|
+
where_start_id = start_id ? "id(start_node) = #{start_id}" : nil
|
23
|
+
|
24
|
+
where_clause = [where_attribs, where_start_id, where_end_id].compact.join(' AND ')
|
25
|
+
return nil if where_clause.empty?
|
26
|
+
|
27
|
+
cypher_sql = find_edge_sql(where_clause)
|
28
|
+
execute_find(cypher_sql)
|
29
|
+
end
|
30
|
+
|
31
|
+
def find_by(attributes)
|
32
|
+
where_clause = attributes.map { |k, v| "find.#{k} = '#{v}'" }.join(' AND ')
|
33
|
+
cypher_sql = find_sql(where_clause)
|
34
|
+
execute_find(cypher_sql)
|
35
|
+
end
|
36
|
+
|
37
|
+
def find(id)
|
38
|
+
where_clause = "id(find) = #{id}"
|
39
|
+
cypher_sql = find_sql(where_clause)
|
40
|
+
execute_find(cypher_sql)
|
41
|
+
end
|
42
|
+
|
43
|
+
def all
|
44
|
+
age_results = ActiveRecord::Base.connection.execute(all_sql)
|
45
|
+
return [] if age_results.values.count.zero?
|
46
|
+
|
47
|
+
age_results.values.map do |result|
|
48
|
+
json_string = result.first.split('::').first
|
49
|
+
hash = JSON.parse(json_string)
|
50
|
+
attribs = hash.except('label', 'properties').merge(hash['properties']).symbolize_keys
|
51
|
+
|
52
|
+
new(**attribs)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Private stuff
|
57
|
+
|
58
|
+
def age_graph = 'age_schema'
|
59
|
+
def age_label = name.gsub('::', '__')
|
60
|
+
def age_type = name.constantize.new.age_type
|
61
|
+
|
62
|
+
def match_clause
|
63
|
+
age_type == 'vertex' ? "(find:#{age_label})" : "(start_node)-[find:#{age_label}]->(end_node)"
|
64
|
+
end
|
65
|
+
|
66
|
+
def execute_find(cypher_sql)
|
67
|
+
age_result = ActiveRecord::Base.connection.execute(cypher_sql)
|
68
|
+
return nil if age_result.values.count.zero?
|
69
|
+
|
70
|
+
age_type = age_result.values.first.first.split('::').last
|
71
|
+
json_data = age_result.values.first.first.split('::').first
|
72
|
+
|
73
|
+
hash = JSON.parse(json_data)
|
74
|
+
attribs = hash.except('label', 'properties').merge(hash['properties']).symbolize_keys
|
75
|
+
|
76
|
+
new(**attribs)
|
77
|
+
end
|
78
|
+
|
79
|
+
def all_sql
|
80
|
+
<<-SQL
|
81
|
+
SELECT *
|
82
|
+
FROM cypher('#{age_graph}', $$
|
83
|
+
MATCH #{match_clause}
|
84
|
+
RETURN find
|
85
|
+
$$) as (#{age_label} agtype);
|
86
|
+
SQL
|
87
|
+
end
|
88
|
+
|
89
|
+
def find_sql(where_clause)
|
90
|
+
<<-SQL
|
91
|
+
SELECT *
|
92
|
+
FROM cypher('#{age_graph}', $$
|
93
|
+
MATCH #{match_clause}
|
94
|
+
WHERE #{where_clause}
|
95
|
+
RETURN find
|
96
|
+
$$) as (#{age_label} agtype);
|
97
|
+
SQL
|
98
|
+
end
|
99
|
+
|
100
|
+
def find_edge_sql(where_clause)
|
101
|
+
<<-SQL
|
102
|
+
SELECT *
|
103
|
+
FROM cypher('#{age_graph}', $$
|
104
|
+
MATCH #{match_clause}
|
105
|
+
WHERE #{where_clause}
|
106
|
+
RETURN find
|
107
|
+
$$) as (#{age_label} agtype);
|
108
|
+
SQL
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module ApacheAge
|
2
|
+
module Entities
|
3
|
+
module CommonMethods
|
4
|
+
# for now we just can just use one schema
|
5
|
+
def age_graph = 'age_schema'
|
6
|
+
def age_label = self.class.name.gsub('::', '__')
|
7
|
+
def persisted? = id.present?
|
8
|
+
def to_s = ":#{age_label} #{properties_to_s}"
|
9
|
+
|
10
|
+
def to_h
|
11
|
+
base_h = attributes.to_hash
|
12
|
+
if age_type == 'edge'
|
13
|
+
# remove the nodes (in attribute form and re-add in hash form)
|
14
|
+
base_h = base_h.except('start_node', 'end_node')
|
15
|
+
base_h[:end_node] = end_node.to_h if end_node
|
16
|
+
base_h[:start_node] = start_node.to_h if start_node
|
17
|
+
end
|
18
|
+
base_h.symbolize_keys
|
19
|
+
end
|
20
|
+
|
21
|
+
def update_attributes(attribs)
|
22
|
+
attribs.except(id:).each do |key, value|
|
23
|
+
send("#{key}=", value) if respond_to?("#{key}=")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def update(attribs)
|
28
|
+
update_attributes(attribs)
|
29
|
+
save
|
30
|
+
end
|
31
|
+
|
32
|
+
def save
|
33
|
+
return false unless valid?
|
34
|
+
|
35
|
+
cypher_sql = (persisted? ? update_sql : create_sql)
|
36
|
+
response_hash = execute_sql(cypher_sql)
|
37
|
+
|
38
|
+
self.id = response_hash['id']
|
39
|
+
|
40
|
+
if age_type == 'edge'
|
41
|
+
self.end_id = response_hash['end_id']
|
42
|
+
self.start_id = response_hash['start_id']
|
43
|
+
# reload the nodes? (can we change the nodes?)
|
44
|
+
# self.end_node = ApacheAge::Entity.find(end_id)
|
45
|
+
# self.start_node = ApacheAge::Entity.find(start_id)
|
46
|
+
end
|
47
|
+
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
def destroy
|
52
|
+
match_clause = (age_type == 'vertex' ? "(done:#{age_label})" : "()-[done:#{age_label}]->()")
|
53
|
+
delete_clause = (age_type == 'vertex' ? 'DETACH DELETE done' : 'DELETE done')
|
54
|
+
cypher_sql =
|
55
|
+
<<-SQL
|
56
|
+
SELECT *
|
57
|
+
FROM cypher('#{age_graph}', $$
|
58
|
+
MATCH #{match_clause}
|
59
|
+
WHERE id(done) = #{id}
|
60
|
+
#{delete_clause}
|
61
|
+
return done
|
62
|
+
$$) as (deleted agtype);
|
63
|
+
SQL
|
64
|
+
|
65
|
+
hash = execute_sql(cypher_sql)
|
66
|
+
return nil if hash.blank?
|
67
|
+
|
68
|
+
self.id = nil
|
69
|
+
self
|
70
|
+
end
|
71
|
+
alias destroy! destroy
|
72
|
+
alias delete destroy
|
73
|
+
|
74
|
+
# private
|
75
|
+
|
76
|
+
def age_properties
|
77
|
+
attrs = attributes.except('id')
|
78
|
+
attrs = attrs.except('end_node', 'start_node', 'end_id', 'start_id') if age_type == 'edge'
|
79
|
+
attrs.symbolize_keys
|
80
|
+
end
|
81
|
+
|
82
|
+
def age_hash
|
83
|
+
hash =
|
84
|
+
{
|
85
|
+
id:,
|
86
|
+
label: age_label,
|
87
|
+
properties: age_properties
|
88
|
+
}
|
89
|
+
hash.merge!(end_id:, start_id:) if age_type == 'edge'
|
90
|
+
hash.transform_keys(&:to_s)
|
91
|
+
end
|
92
|
+
|
93
|
+
def properties_to_s
|
94
|
+
string_values =
|
95
|
+
age_properties.each_with_object([]) do |(key, val), array|
|
96
|
+
array << "#{key}: '#{val}'"
|
97
|
+
end
|
98
|
+
"{#{string_values.join(', ')}}"
|
99
|
+
end
|
100
|
+
|
101
|
+
def age_alias
|
102
|
+
return nil if id.blank?
|
103
|
+
|
104
|
+
# we start the alias with a since we can't start with a number
|
105
|
+
'a' + Digest::SHA256.hexdigest(id.to_s).to_i(16).to_s(36)[0..9]
|
106
|
+
end
|
107
|
+
|
108
|
+
def execute_sql(cypher_sql)
|
109
|
+
age_result = ActiveRecord::Base.connection.execute(cypher_sql)
|
110
|
+
age_type = age_result.values.first.first.split('::').last
|
111
|
+
json_data = age_result.values.first.first.split('::').first
|
112
|
+
# json_data = age_result.to_a.first.values.first.split("::#{age_type}").first
|
113
|
+
|
114
|
+
JSON.parse(json_data)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module ApacheAge
|
2
|
+
module Entities
|
3
|
+
module Edge
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
include ActiveModel::Model
|
8
|
+
include ActiveModel::Dirty
|
9
|
+
include ActiveModel::Attributes
|
10
|
+
|
11
|
+
attribute :id, :integer
|
12
|
+
attribute :end_id, :integer
|
13
|
+
attribute :start_id, :integer
|
14
|
+
# allow user to optionally specify the class type (or not) thus not adding: `:vertex`
|
15
|
+
attribute :end_node
|
16
|
+
attribute :start_node
|
17
|
+
|
18
|
+
validates :end_node, :start_node, presence: true
|
19
|
+
validate :validate_nodes
|
20
|
+
|
21
|
+
extend ApacheAge::Entities::ClassMethods
|
22
|
+
include ApacheAge::Entities::CommonMethods
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(**attributes)
|
26
|
+
super
|
27
|
+
self.end_id ||= end_node.id if end_node
|
28
|
+
self.start_id ||= start_node.id if start_node
|
29
|
+
self.end_node ||= Entity.find(end_id) if end_id
|
30
|
+
self.start_node ||= Entity.find(start_id) if start_id
|
31
|
+
end
|
32
|
+
|
33
|
+
def age_type = 'edge'
|
34
|
+
def end_class = end_node.class
|
35
|
+
def start_class = start_node.class
|
36
|
+
def end_node_class = end_node.class
|
37
|
+
def start_node_class = start_node.class
|
38
|
+
|
39
|
+
# Private methods
|
40
|
+
|
41
|
+
# Custom validation method to validate start_node and end_node
|
42
|
+
def validate_nodes
|
43
|
+
errors.add(:start_node, 'invalid') if start_node && !start_node.valid?
|
44
|
+
errors.add(:end_node, 'invalid') if end_node && !end_node.valid?
|
45
|
+
end
|
46
|
+
|
47
|
+
# Discover attribute class
|
48
|
+
# name_type = model.class.attribute_types['name']
|
49
|
+
# age_type = model.class.attribute_types['age']
|
50
|
+
# company_type = model.class.attribute_types['company']
|
51
|
+
# # Determine the class from the attribute type (for custom types)
|
52
|
+
# name_class = name_type.class # This will generally be ActiveModel::Type::String
|
53
|
+
# age_class = age_type.class # This will generally be ActiveModel::Type::Integer
|
54
|
+
# # For custom types, you may need to look deeper
|
55
|
+
# company_class = company_type.cast_type.class
|
56
|
+
|
57
|
+
# AgeSchema::Edges::HasJob.create(
|
58
|
+
# start_node: fred, end_node: quarry, employee_role: 'Crane Operator'
|
59
|
+
# )
|
60
|
+
# SELECT *
|
61
|
+
# FROM cypher('age_schema', $$
|
62
|
+
# MATCH (start_vertex:Person), (end_vertex:Company)
|
63
|
+
# WHERE id(start_vertex) = 1125899906842634 and id(end_vertex) = 844424930131976
|
64
|
+
# CREATE (start_vertex)-[edge:HasJob {employee_role: 'Crane Operator'}]->(end_vertex)
|
65
|
+
# RETURN edge
|
66
|
+
# $$) as (edge agtype);
|
67
|
+
def create_sql
|
68
|
+
self.start_node = start_node.save unless start_node.persisted?
|
69
|
+
self.end_node = end_node.save unless end_node.persisted?
|
70
|
+
<<-SQL
|
71
|
+
SELECT *
|
72
|
+
FROM cypher('#{age_graph}', $$
|
73
|
+
MATCH (from_node:#{start_node.age_label}), (to_node:#{end_node.age_label})
|
74
|
+
WHERE id(from_node) = #{start_node.id} and id(to_node) = #{end_node.id}
|
75
|
+
CREATE (from_node)-[edge#{self}]->(to_node)
|
76
|
+
RETURN edge
|
77
|
+
$$) as (edge agtype);
|
78
|
+
SQL
|
79
|
+
end
|
80
|
+
|
81
|
+
# So far just properties of string type with '' around them
|
82
|
+
def update_sql
|
83
|
+
alias_name = age_alias || age_label.downcase
|
84
|
+
set_caluse =
|
85
|
+
age_properties.map { |k, v| v ? "#{alias_name}.#{k} = '#{v}'" : "#{alias_name}.#{k} = NULL" }.join(', ')
|
86
|
+
<<-SQL
|
87
|
+
SELECT *
|
88
|
+
FROM cypher('#{age_graph}', $$
|
89
|
+
MATCH ()-[#{alias_name}:#{age_label}]->()
|
90
|
+
WHERE id(#{alias_name}) = #{id}
|
91
|
+
SET #{set_caluse}
|
92
|
+
RETURN #{alias_name}
|
93
|
+
$$) as (#{age_label} agtype);
|
94
|
+
SQL
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module ApacheAge
|
2
|
+
module Entities
|
3
|
+
class Entity
|
4
|
+
class << self
|
5
|
+
def find_by(attributes)
|
6
|
+
where_clause = attributes.map { |k, v| "find.#{k} = '#{v}'" }.join(' AND ')
|
7
|
+
handle_find(where_clause)
|
8
|
+
end
|
9
|
+
|
10
|
+
def find(id)
|
11
|
+
where_clause = "id(find) = #{id}"
|
12
|
+
handle_find(where_clause)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def age_graph = 'age_schema'
|
18
|
+
|
19
|
+
def handle_find(where_clause)
|
20
|
+
# try to find a vertex
|
21
|
+
match_node = '(find)'
|
22
|
+
cypher_sql = find_sql(match_node, where_clause)
|
23
|
+
age_response = execute_find(cypher_sql)
|
24
|
+
|
25
|
+
if age_response.nil?
|
26
|
+
# if not a vertex try to find an edge
|
27
|
+
match_edge = '()-[find]->()'
|
28
|
+
cypher_sql = find_sql(match_edge, where_clause)
|
29
|
+
age_response = execute_find(cypher_sql)
|
30
|
+
return nil if age_response.nil?
|
31
|
+
end
|
32
|
+
|
33
|
+
instantiate_result(age_response)
|
34
|
+
end
|
35
|
+
|
36
|
+
def execute_find(cypher_sql)
|
37
|
+
age_result = ActiveRecord::Base.connection.execute(cypher_sql)
|
38
|
+
return nil if age_result.values.first.nil?
|
39
|
+
|
40
|
+
age_result
|
41
|
+
end
|
42
|
+
|
43
|
+
def instantiate_result(age_response)
|
44
|
+
age_type = age_response.values.first.first.split('::').last
|
45
|
+
json_string = age_response.values.first.first.split('::').first
|
46
|
+
json_data = JSON.parse(json_string)
|
47
|
+
|
48
|
+
age_label = json_data['label']
|
49
|
+
attribs = json_data.except('label', 'properties')
|
50
|
+
.merge(json_data['properties'])
|
51
|
+
.symbolize_keys
|
52
|
+
|
53
|
+
"#{json_data['label'].gsub('__', '::')}".constantize.new(**attribs)
|
54
|
+
end
|
55
|
+
|
56
|
+
def find_sql(match_clause, where_clause)
|
57
|
+
<<-SQL
|
58
|
+
SELECT *
|
59
|
+
FROM cypher('#{age_graph}', $$
|
60
|
+
MATCH #{match_clause}
|
61
|
+
WHERE #{where_clause}
|
62
|
+
RETURN find
|
63
|
+
$$) as (found agtype);
|
64
|
+
SQL
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module ApacheAge
|
2
|
+
module Entities
|
3
|
+
module Vertex
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
include ActiveModel::Model
|
8
|
+
include ActiveModel::Dirty
|
9
|
+
include ActiveModel::Attributes
|
10
|
+
|
11
|
+
attribute :id, :integer
|
12
|
+
|
13
|
+
extend ApacheAge::Entities::ClassMethods
|
14
|
+
include ApacheAge::Entities::CommonMethods
|
15
|
+
end
|
16
|
+
|
17
|
+
def age_type = 'vertex'
|
18
|
+
|
19
|
+
# AgeSchema::Nodes::Company.create(company_name: 'Bedrock Quarry')
|
20
|
+
# SELECT *
|
21
|
+
# FROM cypher('age_schema', $$
|
22
|
+
# CREATE (company:Company {company_name: 'Bedrock Quarry'})
|
23
|
+
# RETURN company
|
24
|
+
# $$) as (Company agtype);
|
25
|
+
def create_sql
|
26
|
+
alias_name = age_alias || age_label.downcase
|
27
|
+
<<-SQL
|
28
|
+
SELECT *
|
29
|
+
FROM cypher('#{age_graph}', $$
|
30
|
+
CREATE (#{alias_name}#{self})
|
31
|
+
RETURN #{alias_name}
|
32
|
+
$$) as (#{age_label} agtype);
|
33
|
+
SQL
|
34
|
+
end
|
35
|
+
|
36
|
+
# So far just properties of string type with '' around them
|
37
|
+
def update_sql
|
38
|
+
alias_name = age_alias || age_label.downcase
|
39
|
+
set_caluse =
|
40
|
+
age_properties.map { |k, v| v ? "#{alias_name}.#{k} = '#{v}'" : "#{alias_name}.#{k} = NULL" }.join(', ')
|
41
|
+
<<-SQL
|
42
|
+
SELECT *
|
43
|
+
FROM cypher('#{age_graph}', $$
|
44
|
+
MATCH (#{alias_name}:#{age_label})
|
45
|
+
WHERE id(#{alias_name}) = #{id}
|
46
|
+
SET #{set_caluse}
|
47
|
+
RETURN #{alias_name}
|
48
|
+
$$) as (#{age_label} agtype);
|
49
|
+
SQL
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|