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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45fb3b7f94084761a6a3d6a26c9e4d86f3b790768ba52d5a5735713bb978d37b
4
- data.tar.gz: af3866f393670857680d9334280058a0542155f3b76cb2cc64648fa0875e9f92
3
+ metadata.gz: aa956147280cd1ef2125eb39f3dd7867eadafde15806d95b90dab30d1290b135
4
+ data.tar.gz: a5b3a61d2fcf3026f9503b1bb4a9c6e60c6317f92266501f2890c2a02af1a01d
5
5
  SHA512:
6
- metadata.gz: 774867c2584780ca6bbae3edbdf82fd592ebb3b3cb65c8f97400ceba5b37db650ce7c0ec9da1b45b30c2cf789e1d2e2ca776b97cbfc8ac431b3af234b4ba9efe
7
- data.tar.gz: 05c03b0e12a5c77a53463cdfae58cce43b1f0cddfb811cad4a81e9ff137d49930434f53cbe0f6858e933eca15e9ce7e98b4d7cf57cb24d690a3a0d2b58cfc314
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
- - Edges
9
- That can be used within Rails applications using a Rails API.
10
- See the [README](README.md) for more information.
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
- And then execute:
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
- Or install it yourself as:
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
- $ gem install rails_age
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/btihen-dev/rails_graphdb_age_app) the summary usage is described below.
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/works_at.rb
189
+ # app/graphs/edges/has_job.rb
82
190
  module Edges
83
- class WorksAt
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::WorksAt.create(start_node: fred, end_node: quarry, employee_role: 'Crane Operator')
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
+ ```
@@ -1,4 +1,4 @@
1
- class ConfigureApacheAge < ActiveRecord::Migration[7.1]
1
+ class AddApacheAge < ActiveRecord::Migration[7.1]
2
2
  def up
3
3
  # Allow age extension
4
4
  execute('CREATE EXTENSION IF NOT EXISTS age;')
@@ -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