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