rails_age 0.1.0 → 0.2.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 +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
|