rails_age 0.1.0 → 0.3.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 +51 -4
- data/README.md +177 -10
- data/db/migrate/{20240521062349_configure_apache_age.rb → 20240521062349_add_apache_age.rb} +1 -1
- data/lib/apache_age/entities/class_methods.rb +119 -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 +210 -0
- metadata +31 -19
- 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: aa956147280cd1ef2125eb39f3dd7867eadafde15806d95b90dab30d1290b135
|
4
|
+
data.tar.gz: a5b3a61d2fcf3026f9503b1bb4a9c6e60c6317f92266501f2890c2a02af1a01d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a2dbb2a72e8f64056bf6531fab29da715bdc89d1748dce2fe92a44de11b5a8c74dbc354c18a903472c996c90e01f7947c921819d3cded09b4ffbfe1cbd0da33c
|
7
|
+
data.tar.gz: d5265daf4c4a1928bdd8f7f08a1efd884f8c4ce5da38d56321f58f6f507905a24d7587d2f92bacfe5a4141e500f7f81060726a2723368b443d8892bf2e66a9b3
|
data/CHANGELOG.md
CHANGED
@@ -1,10 +1,57 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## VERSION 0.4.0 - 2024-xx-xx
|
4
|
+
|
5
|
+
- **Edges**
|
6
|
+
* `find_edge` is deprecated - use `find_by` with :start_node, :end_node to find an edge with specific nodes
|
7
|
+
- **cypher**
|
8
|
+
* query support
|
9
|
+
* paths support
|
10
|
+
* select attributes support
|
11
|
+
- **Paths**
|
12
|
+
* ?
|
13
|
+
|
14
|
+
## VERSION 0.3.1 - 2024-xx-xx
|
15
|
+
|
16
|
+
- **Genetator**
|
17
|
+
* add `rails generate apache_age:node` to create a node model (with its type in initializer)
|
18
|
+
* add `rails generate apache_age:edge` to create an edge model (with its type in initializer)
|
19
|
+
- **Installer**
|
20
|
+
* refactored into multiple independent tasks?
|
21
|
+
|
22
|
+
## VERSION 0.3.0 - 2024-05-28
|
23
|
+
|
24
|
+
- **Installer** (`rails generate apache_age:install`)
|
25
|
+
* copy Age PG Extenstion migration to `db/migrate`
|
26
|
+
* run the AGE PG Migration
|
27
|
+
* repair `db/schema.rb` (rails mangles schema after running pg extension)
|
28
|
+
* update `database.yml` with schema search paths
|
29
|
+
|
30
|
+
NOTE: the `rails generate apache_age:install` can be run at any time to repair the schema (or other config) file if needed.
|
31
|
+
|
32
|
+
## VERSION 0.2.0 - 2024-05-26
|
33
|
+
|
34
|
+
- **Edges**
|
35
|
+
* add class methods to `find_edge` (with {properties, end_id, start_id})
|
36
|
+
* add missing methods to use in rails controllers
|
37
|
+
* validate edge start- & end-nodes are valid
|
38
|
+
* add unique edge validations
|
39
|
+
- **Nodes**
|
40
|
+
* add missing methods to use in rails controllers
|
41
|
+
* add unique node validations
|
42
|
+
|
3
43
|
## VERSION 0.1.0 - 2024-05-21
|
4
44
|
|
5
45
|
Initial release has the following features:
|
6
46
|
|
7
|
-
- Nodes
|
8
|
-
|
9
|
-
|
10
|
-
|
47
|
+
- **Nodes:**
|
48
|
+
* `.create`, `.read`, `.update`, `.delete`, `.all`, `.find(by id)`, `.find_by(age_properties)`
|
49
|
+
* verified with usage in a controller and views
|
50
|
+
- **Edges:**
|
51
|
+
*`.create`, `.read`, `.update`, `.delete`, `.all`, `.find(by id)`, `.find_by(age_properties)`
|
52
|
+
* verified with usage in a controller and views
|
53
|
+
- **Entities:**
|
54
|
+
* `.all`, `.find(id)`, `.find_by(age_property)` use these when class, label, edge, node
|
55
|
+
|
56
|
+
These can be used within Rails applications using a Rails APIs including within controllers and views.
|
57
|
+
See the [README](README.md) for more information.
|
data/README.md
CHANGED
@@ -4,24 +4,127 @@ 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
|
10
12
|
gem "rails_age"
|
11
13
|
```
|
12
14
|
|
13
|
-
|
15
|
+
### Quick Install
|
14
16
|
|
15
17
|
```bash
|
16
18
|
$ bundle
|
19
|
+
$ bin/rails apache_age:install
|
20
|
+
$ git add .
|
21
|
+
$ git commit -m "Add Apache Age to Rails"
|
17
22
|
```
|
18
23
|
|
19
|
-
|
24
|
+
NOTE: it is important to add the db/schema.rb to your git repository because `rails db:migrate` will inappropriately modify the schema file. However, you can run `bin/rails apache_age:install` at any time to repair the schema file if needed.
|
25
|
+
|
26
|
+
### Manual Install
|
27
|
+
|
28
|
+
create a migration to add the Apache Age extension to your database
|
29
|
+
```bash
|
30
|
+
$ bin/rails g migration AddApacheAge
|
31
|
+
```
|
32
|
+
copy the contents of https://github.com/marpori/rails_age/blob/main/db/migrate/20240521062349_add_apache_age.rb
|
33
|
+
```ruby
|
34
|
+
class AddApacheAge < ActiveRecord::Migration[7.1]
|
35
|
+
def up
|
36
|
+
# Allow age extension
|
37
|
+
execute('CREATE EXTENSION IF NOT EXISTS age;')
|
38
|
+
|
39
|
+
# Load the age code
|
40
|
+
execute("LOAD 'age';")
|
41
|
+
|
42
|
+
# Load the ag_catalog into the search path
|
43
|
+
execute('SET search_path = ag_catalog, "$user", public;')
|
44
|
+
|
45
|
+
# Create age_schema graph if it doesn't exist
|
46
|
+
execute("SELECT create_graph('age_schema');")
|
47
|
+
end
|
48
|
+
|
49
|
+
def down
|
50
|
+
execute <<-SQL
|
51
|
+
DO $$
|
52
|
+
BEGIN
|
53
|
+
IF EXISTS (
|
54
|
+
SELECT 1
|
55
|
+
FROM pg_constraint
|
56
|
+
WHERE conname = 'fk_graph_oid'
|
57
|
+
) THEN
|
58
|
+
ALTER TABLE ag_catalog.ag_label
|
59
|
+
DROP CONSTRAINT fk_graph_oid;
|
60
|
+
END IF;
|
61
|
+
END $$;
|
62
|
+
SQL
|
63
|
+
|
64
|
+
execute("SELECT drop_graph('age_schema', true);")
|
65
|
+
execute('DROP SCHEMA IF EXISTS ag_catalog CASCADE;')
|
66
|
+
execute('DROP EXTENSION IF EXISTS age;')
|
67
|
+
end
|
68
|
+
end
|
69
|
+
```
|
70
|
+
into your new migration file
|
20
71
|
|
72
|
+
then run the migration
|
21
73
|
```bash
|
22
|
-
$
|
74
|
+
$ bin/rails db:migrate
|
23
75
|
```
|
24
76
|
|
77
|
+
Rails migrate will mangle the schema `db/schema.rb` file. You need to remove the lines that look like:
|
78
|
+
```ruby
|
79
|
+
create_schema "ag_catalog"
|
80
|
+
create_schema "age_schema"
|
81
|
+
|
82
|
+
# These are extensions that must be enabled in order to support this database
|
83
|
+
enable_extension "age"
|
84
|
+
enable_extension "plpgsql"
|
85
|
+
|
86
|
+
# Could not dump table "_ag_label_edge" because of following StandardError
|
87
|
+
# Unknown type 'graphid' for column 'id'
|
88
|
+
|
89
|
+
# Could not dump table "_ag_label_vertex" because of following StandardError
|
90
|
+
# Unknown type 'graphid' for column 'id'
|
91
|
+
|
92
|
+
# Could not dump table "ag_graph" because of following StandardError
|
93
|
+
# Unknown type 'regnamespace' for column 'namespace'
|
94
|
+
|
95
|
+
# Could not dump table "ag_label" because of following StandardError
|
96
|
+
# Unknown type 'regclass' for column 'relation'
|
97
|
+
|
98
|
+
add_foreign_key "ag_label", "ag_graph", column: "graph", primary_key: "graphid", name: "fk_graph_oid"
|
99
|
+
|
100
|
+
# other migrations
|
101
|
+
# ...
|
102
|
+
```
|
103
|
+
|
104
|
+
and replace them with the following lines:
|
105
|
+
```ruby
|
106
|
+
# These are extensions that must be enabled in order to support this database
|
107
|
+
enable_extension "plpgsql"
|
108
|
+
|
109
|
+
# Allow age extension
|
110
|
+
execute('CREATE EXTENSION IF NOT EXISTS age;')
|
111
|
+
|
112
|
+
# Load the age code
|
113
|
+
execute("LOAD 'age';")
|
114
|
+
|
115
|
+
# Load the ag_catalog into the search path
|
116
|
+
execute('SET search_path = ag_catalog, "$user", public;')
|
117
|
+
|
118
|
+
# Create age_schema graph if it doesn't exist
|
119
|
+
execute("SELECT create_graph('age_schema');")
|
120
|
+
|
121
|
+
# other migrations
|
122
|
+
# ...
|
123
|
+
```
|
124
|
+
|
125
|
+
NOTE: I like to add the schema.rb to git so that it is easy to revert the unwanted changes and keep the desired changes.
|
126
|
+
ALSO note that running: `bin/rails apache_age:install` will check and non-destructively repair any config files at any time (however a git commit before hand as a backup is a good idea incase something goes wrong!)
|
127
|
+
|
25
128
|
## Contributing
|
26
129
|
|
27
130
|
Create an MR and tests and I will review it.
|
@@ -34,7 +137,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
34
137
|
|
35
138
|
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
139
|
|
37
|
-
A full sample app can be found [here](https://github.com/
|
140
|
+
A full sample app can be found [here](https://github.com/marpori/rails_age_demo_app) the summary usage is described below.
|
38
141
|
|
39
142
|
### Nodes
|
40
143
|
|
@@ -42,10 +145,15 @@ A full sample app can be found [here](https://github.com/btihen-dev/rails_graphd
|
|
42
145
|
# app/graphs/nodes/company.rb
|
43
146
|
module Nodes
|
44
147
|
class Company
|
45
|
-
include ApacheAge::Vertex
|
148
|
+
include ApacheAge::Entities::Vertex
|
46
149
|
|
47
150
|
attribute :company_name, :string
|
151
|
+
|
48
152
|
validates :company_name, presence: true
|
153
|
+
validates_with(
|
154
|
+
ApacheAge::Validators::UniqueVertexValidator,
|
155
|
+
attributes: [:company_name]
|
156
|
+
)
|
49
157
|
end
|
50
158
|
end
|
51
159
|
```
|
@@ -54,7 +162,7 @@ end
|
|
54
162
|
# app/graphs/nodes/person.rb
|
55
163
|
module Nodes
|
56
164
|
class Person
|
57
|
-
include ApacheAge::Vertex
|
165
|
+
include ApacheAge::Entities::Vertex
|
58
166
|
|
59
167
|
attribute :first_name, :string, default: nil
|
60
168
|
attribute :last_name, :string, default: nil
|
@@ -78,13 +186,30 @@ end
|
|
78
186
|
### Edges
|
79
187
|
|
80
188
|
```ruby
|
81
|
-
# app/graphs/edges/
|
189
|
+
# app/graphs/edges/has_job.rb
|
82
190
|
module Edges
|
83
|
-
class
|
84
|
-
include ApacheAge::Edge
|
191
|
+
class HasJob
|
192
|
+
include ApacheAge::Entities::Edge
|
85
193
|
|
86
194
|
attribute :employee_role, :string
|
195
|
+
attribute :start_node, :person
|
196
|
+
attribute :end_node, :company
|
197
|
+
|
87
198
|
validates :employee_role, presence: true
|
199
|
+
validate :validate_unique
|
200
|
+
# or with a one-liner
|
201
|
+
# validates_with(
|
202
|
+
# ApacheAge::Validators::UniqueEdgeValidator,
|
203
|
+
# attributes: %i[employee_role start_node end_node]
|
204
|
+
# )
|
205
|
+
|
206
|
+
private
|
207
|
+
|
208
|
+
def validate_unique
|
209
|
+
ApacheAge::Validators::UniqueEdgeValidator
|
210
|
+
.new(attributes: %i[employee_role start_node end_node])
|
211
|
+
.validate(self)
|
212
|
+
end
|
88
213
|
end
|
89
214
|
end
|
90
215
|
```
|
@@ -98,10 +223,46 @@ fred.to_h
|
|
98
223
|
quarry = Nodes::Company.create(company_name: 'Bedrock Quarry')
|
99
224
|
quarry.to_h
|
100
225
|
|
101
|
-
job = Edges::
|
226
|
+
job = Edges::HasJob.create(start_node: fred, end_node: quarry, employee_role: 'Crane Operator')
|
102
227
|
job.to_h
|
103
228
|
```
|
104
229
|
|
230
|
+
### Update Routes
|
231
|
+
|
232
|
+
```ruby
|
233
|
+
Rails.application.routes.draw do
|
234
|
+
# mount is not needed with the engine
|
235
|
+
# mount RailsAge::Engine => "/rails_age"
|
236
|
+
|
237
|
+
# defines the route for the people controller
|
238
|
+
resources :people
|
239
|
+
|
240
|
+
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
241
|
+
# Can be used by load balancers and uptime monitors to verify that the app is live.
|
242
|
+
get 'up' => 'rails/health#show', as: :rails_health_check
|
243
|
+
|
244
|
+
# Defines the root path route ("/")
|
245
|
+
root 'people#index'
|
246
|
+
end
|
247
|
+
```
|
248
|
+
|
249
|
+
### Types (Optional)
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
# spec/dummy/config/initializers/types.rb
|
253
|
+
require 'apache_age/types/age_type_generator'
|
254
|
+
|
255
|
+
Rails.application.config.to_prepare do
|
256
|
+
# Ensure the files are loaded
|
257
|
+
require_dependency 'nodes/company'
|
258
|
+
require_dependency 'nodes/person'
|
259
|
+
|
260
|
+
# Register the custom types
|
261
|
+
ActiveModel::Type.register(:company, ApacheAge::Types::AgeTypeGenerator.create_type_for(Nodes::Company))
|
262
|
+
ActiveModel::Type.register(:person, ApacheAge::Types::AgeTypeGenerator.create_type_for(Nodes::Person))
|
263
|
+
end
|
264
|
+
```
|
265
|
+
|
105
266
|
### Controller Usage
|
106
267
|
|
107
268
|
```ruby
|
@@ -176,3 +337,9 @@ class PeopleController < ApplicationController
|
|
176
337
|
end
|
177
338
|
end
|
178
339
|
```
|
340
|
+
|
341
|
+
### Views
|
342
|
+
|
343
|
+
```erb
|
344
|
+
|
345
|
+
```
|
@@ -0,0 +1,119 @@
|
|
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_by(attributes)
|
12
|
+
return nil if attributes.reject{ |k,v| v.blank? }.empty?
|
13
|
+
|
14
|
+
edge_keys = [:start_id, :start_node, :end_id, :end_node]
|
15
|
+
return find_edge(attributes) if edge_keys.any? { |key| attributes.include?(key) }
|
16
|
+
|
17
|
+
where_clause = attributes.map { |k, v| "find.#{k} = '#{v}'" }.join(' AND ')
|
18
|
+
cypher_sql = find_sql(where_clause)
|
19
|
+
|
20
|
+
execute_find(cypher_sql)
|
21
|
+
end
|
22
|
+
|
23
|
+
def find(id)
|
24
|
+
where_clause = "id(find) = #{id}"
|
25
|
+
cypher_sql = find_sql(where_clause)
|
26
|
+
execute_find(cypher_sql)
|
27
|
+
end
|
28
|
+
|
29
|
+
def all
|
30
|
+
age_results = ActiveRecord::Base.connection.execute(all_sql)
|
31
|
+
return [] if age_results.values.count.zero?
|
32
|
+
|
33
|
+
age_results.values.map do |result|
|
34
|
+
json_string = result.first.split('::').first
|
35
|
+
hash = JSON.parse(json_string)
|
36
|
+
attribs = hash.except('label', 'properties').merge(hash['properties']).symbolize_keys
|
37
|
+
|
38
|
+
new(**attribs)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Private stuff
|
43
|
+
|
44
|
+
def find_edge(attributes)
|
45
|
+
where_attribs =
|
46
|
+
attributes
|
47
|
+
.compact
|
48
|
+
.except(:end_id, :start_id, :end_node, :start_node)
|
49
|
+
.map { |k, v| "find.#{k} = '#{v}'" }.join(' AND ')
|
50
|
+
where_attribs = where_attribs.empty? ? nil : where_attribs
|
51
|
+
|
52
|
+
end_id = attributes[:end_id] || attributes[:end_node]&.id
|
53
|
+
start_id = attributes[:start_id] || attributes[:start_node]&.id
|
54
|
+
where_end_id = end_id ? "id(end_node) = #{end_id}" : nil
|
55
|
+
where_start_id = start_id ? "id(start_node) = #{start_id}" : nil
|
56
|
+
|
57
|
+
where_clause = [where_attribs, where_start_id, where_end_id].compact.join(' AND ')
|
58
|
+
return nil if where_clause.empty?
|
59
|
+
|
60
|
+
cypher_sql = find_edge_sql(where_clause)
|
61
|
+
|
62
|
+
execute_find(cypher_sql)
|
63
|
+
end
|
64
|
+
|
65
|
+
def age_graph = 'age_schema'
|
66
|
+
def age_label = name.gsub('::', '__')
|
67
|
+
def age_type = name.constantize.new.age_type
|
68
|
+
|
69
|
+
def match_clause
|
70
|
+
age_type == 'vertex' ? "(find:#{age_label})" : "(start_node)-[find:#{age_label}]->(end_node)"
|
71
|
+
end
|
72
|
+
|
73
|
+
def execute_find(cypher_sql)
|
74
|
+
age_result = ActiveRecord::Base.connection.execute(cypher_sql)
|
75
|
+
return nil if age_result.values.count.zero?
|
76
|
+
|
77
|
+
age_type = age_result.values.first.first.split('::').last
|
78
|
+
json_data = age_result.values.first.first.split('::').first
|
79
|
+
|
80
|
+
hash = JSON.parse(json_data)
|
81
|
+
attribs = hash.except('label', 'properties').merge(hash['properties']).symbolize_keys
|
82
|
+
|
83
|
+
new(**attribs)
|
84
|
+
end
|
85
|
+
|
86
|
+
def all_sql
|
87
|
+
<<-SQL
|
88
|
+
SELECT *
|
89
|
+
FROM cypher('#{age_graph}', $$
|
90
|
+
MATCH #{match_clause}
|
91
|
+
RETURN find
|
92
|
+
$$) as (#{age_label} agtype);
|
93
|
+
SQL
|
94
|
+
end
|
95
|
+
|
96
|
+
def find_sql(where_clause)
|
97
|
+
<<-SQL
|
98
|
+
SELECT *
|
99
|
+
FROM cypher('#{age_graph}', $$
|
100
|
+
MATCH #{match_clause}
|
101
|
+
WHERE #{where_clause}
|
102
|
+
RETURN find
|
103
|
+
$$) as (#{age_label} agtype);
|
104
|
+
SQL
|
105
|
+
end
|
106
|
+
|
107
|
+
def find_edge_sql(where_clause)
|
108
|
+
<<-SQL
|
109
|
+
SELECT *
|
110
|
+
FROM cypher('#{age_graph}', $$
|
111
|
+
MATCH #{match_clause}
|
112
|
+
WHERE #{where_clause}
|
113
|
+
RETURN find
|
114
|
+
$$) as (#{age_label} agtype);
|
115
|
+
SQL
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
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
|