rails_age 0.6.4 → 0.7.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 +27 -23
- data/README.md +412 -92
- data/lib/apache_age/edge.rb +63 -0
- data/lib/apache_age/entities/class_methods.rb +69 -60
- data/lib/apache_age/entities/common_methods.rb +26 -0
- data/lib/apache_age/entities/edge.rb +0 -20
- data/lib/apache_age/entities/entity.rb +54 -0
- data/lib/apache_age/entities/node.rb +0 -5
- data/lib/apache_age/entities/path.rb +76 -14
- data/lib/apache_age/entities/query_builder.rb +41 -36
- data/lib/apache_age/node.rb +61 -18
- data/lib/apache_age/path.rb +258 -0
- data/lib/rails_age/version.rb +1 -1
- data/lib/rails_age.rb +2 -2
- metadata +3 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e75dd885fd2214ee3d6cbf5424406f26ba390a51a5bea08999998908e5bf24da
|
4
|
+
data.tar.gz: 9347d365deecd63c0ef5a48877c568db1e246d27d8cdfb031c693fc4c017ece2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 70170301c78e81a6756f2f2d09d63c9cbf79d5c24dcbd44c732b732b667172dcc74402f038142d9ae14024c9c944ccab467988d37014ccd0ec6837b2b93a4844
|
7
|
+
data.tar.gz: 68bcd97903344fba28cfa378b0feb96663997056c01a467a0c53399e124affd37462a4e2f58a8aa2377047ef61e411ffaf0cb8f63e1da6bd720623db14de0ab9
|
data/CHANGELOG.md
CHANGED
@@ -1,44 +1,48 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
-
## VERSION 0.
|
3
|
+
## VERSION 0.7.0 - 2025-06-01
|
4
4
|
|
5
|
-
|
6
|
-
* add start-/end-nodes types to edge generator (would make scaffold easier), ie:
|
7
|
-
`rails generate apache_age:edge HasPet owner_role start_node:person end_node:pet`
|
8
|
-
with property and specified start-/end-nodes (person and pet nodes must have already been created)
|
5
|
+
**Age Path** - nodes and edges combined
|
9
6
|
|
10
|
-
-
|
11
|
-
|
7
|
+
- query paths (control path, length/depth and filtering using `match`)
|
8
|
+
- code: `Path.cypher(path_edge: HasChild, path_length: "1..5", path_properties: {guardian_role: 'father'}, start_node_filter: {first_name: 'Zeke'}, end_node_filter: {last_name: 'Flintstone'})`
|
9
|
+
- code.to_sql: `SELECT * FROM cypher('age_schema', $$ MATCH path = (start_node {first_name: 'Zeke'})-[HasChild*1..5 {guardian_role: 'father'}]->(end_node {last_name: 'Flintstone'}) RETURN path $$) AS (path agtype);`
|
12
10
|
|
13
|
-
|
14
|
-
- **AGE visual paths graph**
|
15
|
-
* add `rails generate apache_age:visualize`
|
11
|
+
**to_rich_h**
|
16
12
|
|
17
|
-
|
13
|
+
- added to_rich_h method to nodes, edges and paths (displays additional context information for readability and represents data closer to the original age data)
|
18
14
|
|
19
|
-
|
20
|
-
* schema override
|
21
|
-
* query support
|
22
|
-
* paths support
|
23
|
-
* select attributes support
|
15
|
+
**Generic Queries**
|
24
16
|
|
25
|
-
|
17
|
+
- ApacheAge::Node and ApacheAge::Edge can be used as the base for a query and return results instantiating the correct class (node, edge or path)
|
26
18
|
|
27
|
-
|
19
|
+
**Read Me** largely updated
|
28
20
|
|
29
|
-
|
21
|
+
**Query Values**
|
30
22
|
|
31
|
-
-
|
23
|
+
- data casting within query code (so that matches are accurate)
|
24
|
+
* string
|
25
|
+
* integer
|
26
|
+
* decimal
|
27
|
+
* date
|
28
|
+
* datetime
|
29
|
+
* boolean
|
32
30
|
|
33
|
-
|
31
|
+
Not implemented (on the rails side)
|
32
|
+
* array
|
33
|
+
* hash
|
34
|
+
* json
|
34
35
|
|
35
|
-
- **Age Path** - nodes and edges combined
|
36
|
-
* add `rails generate apache_age:path_scaffold HasJob employee_role start_node:person end_node:company`
|
37
36
|
|
38
37
|
## VERSION 0.6.4 - 2024-10-30
|
39
38
|
|
40
39
|
- **Query Sanitize**:
|
41
40
|
* allow and sanitize query strings with multiple attributes, ie: `Person.where("find.first_name = ? AND find.last_name = ?", 'John', 'Doe')`
|
41
|
+
NOTE: for now the following keyords MuST be in caps!
|
42
|
+
```
|
43
|
+
operators = ['=', '>', '<', '<>', '>=', '<=', '=~', 'ENDS WITH', 'CONTAINS', 'STARTS WITH', 'IN', 'IS NULL', 'IS NOT NULL']
|
44
|
+
separators = ["AND NOT", "OR NOT", "AND", "OR", "NOT"]
|
45
|
+
```
|
42
46
|
|
43
47
|
## VERSION 0.6.3 - 2024-10-27
|
44
48
|
|
data/README.md
CHANGED
@@ -2,36 +2,387 @@
|
|
2
2
|
|
3
3
|
Apache Age integration within a Rails application.
|
4
4
|
|
5
|
+
Inspired by: https://github.com/apache/age/issues/370
|
6
|
+
|
5
7
|
## Quick Start - Essentials
|
6
8
|
|
7
|
-
|
9
|
+
### Overview
|
10
|
+
|
11
|
+
This Gem uses 3 Major Concepts:
|
12
|
+
|
13
|
+
1. **Nodes** (Vertices) - usually nouns
|
14
|
+
2. **Edges** (Relationships) - usually verbs (or relational adjectives)
|
15
|
+
3. **Paths** (connections between nodes - heavy dependend on the `match` feature in cypher)
|
16
|
+
|
17
|
+
In this mini example we have the Flintstone Family Tree.
|
18
|
+
- Person (Node) - has attributes: first_name, last_name, gender
|
19
|
+
- Pet (Node) - has attributes: name, gender, species
|
20
|
+
- HasChild (Edge) - has attributes: guardian_role
|
21
|
+
- HasSibling (Edge) - has attributes: relation
|
22
|
+
- HasSpouse (Edge) - has attributes: spousal_role
|
23
|
+
- HasJob (Edge) - has attributes: employee_role
|
24
|
+
|
25
|
+
Paths are how you build the `network` of nodes and edges.
|
26
|
+
|
27
|
+
|
28
|
+
### Generators - simplify usage and rails integration
|
29
|
+
|
30
|
+
we have an installers and generators (we handle namespacing fully)
|
31
|
+
|
32
|
+
RailsAge generators (and generally support standard Rails datatypes) - not all AGE datatypes are supported (yet)
|
33
|
+
|
34
|
+
This includes:
|
35
|
+
* string
|
36
|
+
* integer
|
37
|
+
* decimal
|
38
|
+
* date
|
39
|
+
* datetime
|
40
|
+
* boolean
|
41
|
+
|
42
|
+
AGE supports (but not RailsAge):
|
43
|
+
* array
|
44
|
+
* hash
|
45
|
+
* json
|
46
|
+
|
47
|
+
**Installers**
|
8
48
|
|
9
49
|
```bash
|
10
|
-
|
11
|
-
bundle install
|
12
|
-
bin/rails apache_age:install
|
13
|
-
# optional: prevents `bin/rails db:migrate` from modifying the schema file,
|
14
|
-
# bin/rails apache_age:override_db_migrate
|
15
|
-
git add .
|
16
|
-
git commit -m "Add & configure Apache Age within Rails"
|
17
|
-
```
|
50
|
+
rails generate apache_age:install
|
18
51
|
|
19
|
-
|
52
|
+
# optional, but handy, it prevents `bin/rails db:migrate` from breaking the schema file,
|
53
|
+
rails generate apache_age:override_db_migrate
|
54
|
+
```
|
20
55
|
|
21
56
|
**NODES**
|
22
57
|
|
23
58
|
```bash
|
24
59
|
rails generate apache_age:scaffold_node Company company_name
|
25
|
-
|
26
60
|
rails generate apache_age:scaffold_node Person first_name last_name
|
27
61
|
```
|
28
62
|
|
29
63
|
**EDGES**
|
30
64
|
|
31
65
|
```bash
|
66
|
+
rails generate apache_age:scaffold_edge HasChild guardian_role
|
32
67
|
rails generate apache_age:scaffold_edge HasJob employee_role start_date:date
|
68
|
+
rails generate apache_age:scaffold_edge HasSibling start_relation
|
69
|
+
rails generate apache_age:scaffold_edge HasSpouse spousal_role
|
70
|
+
```
|
71
|
+
|
72
|
+
|
73
|
+
### INSTALL APACHE AGE
|
74
|
+
|
75
|
+
[Install Apache Age](https://age.apache.org/getstarted/quickstart)
|
76
|
+
|
77
|
+
**Quick Docker Install**
|
78
|
+
|
79
|
+
```bash
|
80
|
+
# pull the docker image
|
81
|
+
docker pull apache/age
|
82
|
+
|
83
|
+
# Create AGE docker container
|
84
|
+
docker run \
|
85
|
+
--name age \
|
86
|
+
-p 5455:5432 \
|
87
|
+
-e POSTGRES_USER=postgresUser \
|
88
|
+
-e POSTGRES_PASSWORD=postgresPW \
|
89
|
+
-e POSTGRES_DB=postgresDB \
|
90
|
+
-d \
|
91
|
+
apache/age
|
92
|
+
|
93
|
+
# enter the container and connect to the database (and test it)
|
94
|
+
docker exec -it age psql -d postgresDB -U postgresUser
|
95
|
+
|
96
|
+
# For every connection of AGE you start, you will need to load the AGE extension.
|
97
|
+
CREATE EXTENSION age;
|
98
|
+
LOAD 'age';
|
99
|
+
SET search_path = ag_catalog, "$user", public;
|
100
|
+
```
|
101
|
+
|
102
|
+
|
103
|
+
### RAILS PROJECT (Flintstone Family)
|
104
|
+
|
105
|
+
**NOTE:** you must be using Postgres as your database!
|
106
|
+
|
107
|
+
Apache Age requires using Postgres (`-d postgresql`)!
|
108
|
+
|
109
|
+
```bash
|
110
|
+
# SETUP A RAILS PROJECT (Flintstone Family Tree)
|
111
|
+
rails new stone_age -T -d postgresql
|
112
|
+
cd stone_age
|
113
|
+
git commit -m "initail commit"
|
114
|
+
|
115
|
+
# if using the default dockerized AGE PostgreSQL server then add the following to your `config/database.yml`
|
116
|
+
# BE SURE TO MATCH THE USERNAME AND PASSWORD TO WHAT YOU SET IN THE DOCKER RUN COMMAND!
|
117
|
+
host: localhost
|
118
|
+
port: 5455
|
119
|
+
username: postgresUser
|
120
|
+
password: postgresPW
|
121
|
+
|
122
|
+
# create the database
|
123
|
+
rails db:create
|
124
|
+
|
125
|
+
# add the rails_age gem to your Gemfile
|
126
|
+
bundle add rails_age
|
127
|
+
|
128
|
+
# download the rails_age gem
|
129
|
+
bundle install
|
130
|
+
|
131
|
+
# configure the Apache AGE gem (ignore the OID warnings)
|
132
|
+
bin/rails apache_age:install
|
133
|
+
# if you get the error: `PG::FeatureNotSupported: ERROR: extension "age" is not available`
|
134
|
+
# then you have not installed the AGE code (in PostgreSQL) follow this instrcutions at:
|
135
|
+
# https://github.com/apache/age?tab=readme-ov-file#installation
|
136
|
+
|
137
|
+
# optional: prevents `bin/rails db:migrate` from modifying the schema file,
|
138
|
+
bin/rails apache_age:override_db_migrate
|
139
|
+
bin/rails db:migrate
|
140
|
+
|
141
|
+
# create nodes
|
142
|
+
bin/rails generate apache_age:scaffold_node Company company_name industry
|
143
|
+
bin/rails generate apache_age:scaffold_node Person first_name last_name gender
|
144
|
+
bin/rails generate apache_age:scaffold_node Pet name gender species
|
145
|
+
# adjust the validations in the nodes files
|
146
|
+
|
147
|
+
# create edges (relationships)
|
148
|
+
bin/rails generate apache_age:scaffold_edge HasChild guardian_role:string
|
149
|
+
bin/rails generate apache_age:scaffold_edge HasSibling relation:string
|
150
|
+
bin/rails generate apache_age:scaffold_edge HasSpouse spousal_role:string
|
151
|
+
bin/rails generate apache_age:scaffold_edge HasJob employee_role:string
|
152
|
+
|
153
|
+
# adjust the edge validations and start_node and end_node types: ie: thefore
|
154
|
+
# `HasChild` edge should add the model type following start_node and end_node
|
155
|
+
# in this case: `:person`
|
156
|
+
# app/edges/has_child.rb
|
157
|
+
class HasChild
|
158
|
+
include ApacheAge::Entities::Edge
|
159
|
+
|
160
|
+
attribute :guardian_role, :string
|
161
|
+
attribute :start_node, :person
|
162
|
+
attribute :end_node, :person
|
163
|
+
end
|
164
|
+
|
165
|
+
# seed file uses `db/seed.rb` (seed provides NO pets)
|
166
|
+
bin/rails db:seed
|
167
|
+
|
168
|
+
# list noutes
|
169
|
+
bin/rails routes
|
170
|
+
|
171
|
+
# start server `bin/rails s`
|
172
|
+
# or more likely when using JS:
|
173
|
+
bin/start
|
174
|
+
|
175
|
+
# visit the routes in your browser - you should have a basic but working AGE app - that can do both AGE (Graph) and normal Rails activities
|
33
176
|
```
|
34
177
|
|
178
|
+
|
179
|
+
|
180
|
+
## Queries
|
181
|
+
|
182
|
+
Mostly mimic **ActiveRecord** queries - if the class is an AGE based object (ie: a node or edge) then the queries are rewritten into AGE queries before being executed. Age queries also automatically unwrap the results turning them into the appropriate Ruby class (node or edge).
|
183
|
+
|
184
|
+
1. `.all`
|
185
|
+
2. `.first`
|
186
|
+
2. `.save`
|
187
|
+
2. `.update`
|
188
|
+
2. `.destroy`
|
189
|
+
2. `.destroy_all` (not implemented)
|
190
|
+
2. `.update_attributes`
|
191
|
+
2. `.find(id)`
|
192
|
+
2. `.exists?` (not implemented)
|
193
|
+
3. `.find_by(first_name: 'Zeke', last_name: 'Flintstone')`
|
194
|
+
3. `.where(first_name: 'Zeke', last_name: 'Flintstone')`
|
195
|
+
4. `.order(last_name: :desc, first_name: :asc)`
|
196
|
+
5. `.limit(3)` (returns the first 3 records)
|
197
|
+
6. `.to_sql` (returns the SQL query)
|
198
|
+
7. `.cypher` (primarily for path queries - _builds a specialized cypher query for efficetly doing path queries - building efficient `match` statements_)
|
199
|
+
|
200
|
+
### Node Queries
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
Dog.all
|
204
|
+
Dog.where(name: 'Pema')
|
205
|
+
|
206
|
+
Person.all
|
207
|
+
Person.where(first_name: 'Zeke', last_name: 'Flintstone')
|
208
|
+
|
209
|
+
ApacheAge::Node.all
|
210
|
+
# untested applying a where on nodes of different types, but should work
|
211
|
+
ApacheAge::Node.where(first_name: 'Zeke', last_name: 'Flintstone')
|
212
|
+
```
|
213
|
+
|
214
|
+
### Edge Queries
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
HasChild.all
|
218
|
+
HasChild.where(guardian_role: 'father')
|
219
|
+
|
220
|
+
HasJob.all
|
221
|
+
HasJob.where(employee_role: 'doctor')
|
222
|
+
|
223
|
+
ApacheAge::Edge.all
|
224
|
+
# untested applying a where on edges of different types, but should work
|
225
|
+
ApacheAge::Edge.where(employee_role: 'doctor')
|
226
|
+
```
|
227
|
+
|
228
|
+
### Path Queries
|
229
|
+
|
230
|
+
the connections between nodes and edges - this is important for advanced queries and relies heavily on the `match` feature in cypher (using match is faster for large datasets than using where to filter the data!)
|
231
|
+
|
232
|
+
A Path Query must have the `cypher` method called on the `Path` class.
|
233
|
+
```ruby
|
234
|
+
Path.cypher(path_edge: HasChild, path_length: "1..5", path_properties: {guardian_role: 'father'}, start_node_filter: {first_name: 'Zeke'}, end_node_filter: {last_name: 'Flintstone'})
|
235
|
+
.order(start_node: :last_name)
|
236
|
+
.limit(3)
|
237
|
+
```
|
238
|
+
**NOTE**: you can order on the start_node and end_node attributes, BUT NOT ON THE EDGE ATTRIBUTES (age limitation), even though you can filter on the edge attributes!
|
239
|
+
|
240
|
+
A path query returns an array of paths (each path is an array of nodes and edges)
|
241
|
+
```ruby
|
242
|
+
[
|
243
|
+
[betty (node), bettys_son (edge), bamm_bamm (node)],
|
244
|
+
[betty (node), bettys_son (edge), bamm_bamm (node), bamm_bamms_son (edge), chip (node)]
|
245
|
+
]
|
246
|
+
```
|
247
|
+
|
248
|
+
To make a path more readable for debugging you can use the `to_rich_h` method
|
249
|
+
|
250
|
+
```ruby
|
251
|
+
# hash metadata (rich hash) and is data is in properties (closer to the DB format)
|
252
|
+
path_query.all.first.to_rich_h
|
253
|
+
path_query.all.map(&:to_rich_h)
|
254
|
+
# default to_h works on paths too with (flat data hash of record):
|
255
|
+
path_query.all.first.map(&:to_h)
|
256
|
+
path_query.all.map { |p| p.map(&:to_h) }
|
257
|
+
```
|
258
|
+
|
259
|
+
SQL Comparison with `match` and with `where` (in small datasets you won't see efficiency differences, but for large datasets `match` is much more efficient)
|
260
|
+
```ruby
|
261
|
+
# EFFICIENT - the data is filtered in one step during the data traversal
|
262
|
+
Path.cypher(path_edge: HasChild, path_length: "1..5", path_properties: {guardian_role: 'father'}, start_node_filter: {first_name: 'Zeke'}, end_node_filter: {last_name: 'Flintstone'})
|
263
|
+
.order(start_node: {last_name: :asc}, length: :desc)
|
264
|
+
.limit(3).to_sql
|
265
|
+
# SELECT *
|
266
|
+
# FROM cypher('age_schema', $$
|
267
|
+
# MATCH path = (start_node {first_name: 'Zeke'})-[HasChild*1..5 {guardian_role: 'father'}]->(end_node {last_name: 'Flintstone'})
|
268
|
+
# RETURN path
|
269
|
+
# ORDER BY start_node.last_name ASC, length(path) DESC
|
270
|
+
# LIMIT 3
|
271
|
+
# $$) AS (path agtype);
|
272
|
+
|
273
|
+
|
274
|
+
# INEFFICIENT - because where is done after the match traversal (two steps through the data)
|
275
|
+
Path.cypher(path_edge: HasChild, path_length: "1..5", path_properties: {guardian_role: 'father'})
|
276
|
+
.where(start_node: {first_name: 'Zeke'})
|
277
|
+
.where('end_node.last_name =~ ?', 'Flintstone')
|
278
|
+
.order(start_node: {last_name: :asc}, length: :desc)
|
279
|
+
.limit(3).to_sql
|
280
|
+
# SELECT *
|
281
|
+
# FROM cypher('age_schema', $$
|
282
|
+
# MATCH path = (start_node)-[HasChild*1..5 {guardian_role: 'father'}]->(end_node)
|
283
|
+
# WHERE start_node.first_name = 'Zeke' AND end_node.last_name =~ 'Flintstone'
|
284
|
+
# RETURN path
|
285
|
+
# ORDER BY start_node.last_name ASC, length(path) DESC
|
286
|
+
# LIMIT 3
|
287
|
+
# $$) AS (path agtype);
|
288
|
+
```
|
289
|
+
|
290
|
+
## Console Usage
|
291
|
+
|
292
|
+
**NOTE**
|
293
|
+
using rails attributes complicates the default output of the model, thus it is strong recommended to use the `to_rich_h` method to display the results with meta_data so the class is known (or `to_h` - just the essential data).
|
294
|
+
|
295
|
+
```ruby
|
296
|
+
dino = Pet.create(name: 'Dino', gender: 'male', species: 'dinosaur')
|
297
|
+
dino.to_rich_h
|
298
|
+
|
299
|
+
# find a person
|
300
|
+
fred = Person.find_by(first_name: 'Fred', last_name: 'Flintstone')
|
301
|
+
fred.to_rich_h
|
302
|
+
|
303
|
+
pebbles = Person.find_by(first_name: 'Pebbles')
|
304
|
+
pebbles.to_rich_h
|
305
|
+
|
306
|
+
# find an edge
|
307
|
+
father_relationship = HasChild.find_by(start_node: fred, end_node: pebbles)
|
308
|
+
father_relationship.to_h
|
309
|
+
> {:id=>1407374883553310,
|
310
|
+
:end_id=>844424930131996,
|
311
|
+
:start_id=>844424930131986,
|
312
|
+
:role=>"father",
|
313
|
+
:end_node=>{:id=>844424930131996, :last_name=>"Flintstone", :first_name=>"Pebbles", :gender=>"female"},
|
314
|
+
:start_node=>{:id=>844424930131986, :last_name=>"Flintstone", :first_name=>"Fred", :gender=>"male"}}
|
315
|
+
|
316
|
+
# where - find multiple nodes
|
317
|
+
family = Person.where(last_name: 'Flintstone').order(:first_name).limit(4).all.puts family.map(&:to_h)
|
318
|
+
|
319
|
+
family
|
320
|
+
> [{:id=>844424930131974, :last_name=>"Flintstone", :first_name=>"Ed", :gender=>"male"},
|
321
|
+
> {:id=>844424930131976, :last_name=>"Flintstone", :first_name=>"Edna", :gender=>"female"},
|
322
|
+
? {:id=>844424930131986, :last_name=>"Flintstone", :first_name=>"Fred", :gender=>"male"},
|
323
|
+
> {:id=>844424930131975, :last_name=>"Flintstone", :first_name=>"Giggles", :gender=>"male"}]
|
324
|
+
|
325
|
+
# all - unsorted
|
326
|
+
all_family = Person.where(last_name: 'Flintstone').all
|
327
|
+
puts all_family.map(&:to_h)
|
328
|
+
> {:id=>844424930131969, :last_name=>"Flintstone", :first_name=>"Zeke", :gender=>"female"}
|
329
|
+
> {:id=>844424930131970, :last_name=>"Flintstone", :first_name=>"Jed", :gender=>"male"}
|
330
|
+
> {:id=>844424930131971, :last_name=>"Flintstone", :first_name=>"Rockbottom", :gender=>"male"}
|
331
|
+
> {:id=>844424930131974, :last_name=>"Flintstone", :first_name=>"Ed", :gender=>"male"}
|
332
|
+
> {:id=>844424930131975, :last_name=>"Flintstone", :first_name=>"Giggles", :gender=>"male"}
|
333
|
+
> {:id=>844424930131976, :last_name=>"Flintstone", :first_name=>"Edna", :gender=>"female"}
|
334
|
+
> {:id=>844424930131986, :last_name=>"Flintstone", :first_name=>"Fred", :gender=>"male"}
|
335
|
+
> {:id=>844424930131987, :last_name=>"Flintstone", :first_name=>"Wilma", :gender=>"female"}
|
336
|
+
> {:id=>844424930131995, :last_name=>"Flintstone", :first_name=>"Stoney", :gender=>"male"}
|
337
|
+
> {:id=>844424930131996, :last_name=>"Flintstone", :first_name=>"Pebbles", :gender=>"female"}
|
338
|
+
|
339
|
+
# where - multiple edges (relations) - for now only edge attributes and start/end nodes can be queried
|
340
|
+
parental_relations = HasChild.where(end_node: pebbles)
|
341
|
+
puts parental_relations.map(&:to_h)
|
342
|
+
> {:id=>1407374883553310, :end_id=>844424930131996, :start_id=>844424930131986, :role=>"father", :end_node=>{:id=>844424930131996, :last_name=>"Flintstone", :first_name=>"Pebbles", :gender=>"female"}, :start_node=>{:id=>844424930131986, :last_name=>"Flintstone", :first_name=>"Fred", :gender=>"male"}}
|
343
|
+
> {:id=>1407374883553309, :end_id=>844424930131996, :start_id=>844424930131987, :role=>"mother", :end_node=>{:id=>844424930131996, :last_name=>"Flintstone", :first_name=>"Pebbles", :gender=>"female"}, :start_node=>{:id=>844424930131987, :last_name=>"Flintstone", :first_name=>"Wilma", :gender=>"female"}}
|
344
|
+
|
345
|
+
# Path Queries - returns all elements that match a given path to find specific sets of relationships
|
346
|
+
path = Path.cypher(path_edge: HasChild, path_length: "1..5", path_properties: {guardian_role: 'father'}, start_node_filter: {first_name: 'Zeke'}, end_node_filter: {last_name: 'Flintstone'})
|
347
|
+
.order(start_node: {last_name: :asc}, length: :desc)
|
348
|
+
.limit(3)
|
349
|
+
|
350
|
+
# should redo the example using: path.all.map(&:to_rich_h) to make more readable
|
351
|
+
path.all.map { |p| p.map(&:to_h) }
|
352
|
+
>[[{:id=>844424930131969, :first_name=>"Zeke", :last_name=>"Flintstone", :gender=>"female"},
|
353
|
+
{:id=>1407374883553281,
|
354
|
+
:end_id=>844424930131971,
|
355
|
+
:start_id=>844424930131969,
|
356
|
+
:guardian_role=>"mother",
|
357
|
+
:end_node=>{:id=>844424930131971, :first_name=>"Rockbottom", :last_name=>"Flintstone", :gender=>"male"},
|
358
|
+
:start_node=>{:id=>844424930131969, :first_name=>"Zeke", :last_name=>"Flintstone", :gender=>"female"}},
|
359
|
+
{:id=>844424930131971, :first_name=>"Rockbottom", :last_name=>"Flintstone", :gender=>"male"}],
|
360
|
+
[{:id=>844424930131969, :first_name=>"Zeke", :last_name=>"Flintstone", :gender=>"female"},
|
361
|
+
{:id=>1407374883553281,
|
362
|
+
:end_id=>844424930131971,
|
363
|
+
:start_id=>844424930131969,
|
364
|
+
:guardian_role=>"mother",
|
365
|
+
:end_node=>{:id=>844424930131971, :first_name=>"Rockbottom", :last_name=>"Flintstone", :gender=>"male"},
|
366
|
+
:start_node=>{:id=>844424930131969, :first_name=>"Zeke", :last_name=>"Flintstone", :gender=>"female"}},
|
367
|
+
{:id=>844424930131971, :first_name=>"Rockbottom", :last_name=>"Flintstone", :gender=>"male"},
|
368
|
+
{:id=>1407374883553284,
|
369
|
+
:end_id=>844424930131975,
|
370
|
+
:start_id=>844424930131971,
|
371
|
+
:guardian_role=>"father",
|
372
|
+
:end_node=>{:id=>844424930131975, :first_name=>"Giggles", :last_name=>"Flintstone", :gender=>"male"},
|
373
|
+
:start_node=>{:id=>844424930131971, :first_name=>"Rockbottom", :last_name=>"Flintstone", :gender=>"male"}},
|
374
|
+
{:id=>844424930131975, :first_name=>"Giggles", :last_name=>"Flintstone", :gender=>"male"}]]
|
375
|
+
|
376
|
+
raw_pg_results = Person.where(last_name: 'Flintstone').order(:first_name).limit(4).execute
|
377
|
+
=> #<PG::Result:0x000000012255f348 status=PGRES_TUPLES_OK ntuples=4 nfields=1 cmd_tuples=4>
|
378
|
+
raw_pg_results.values
|
379
|
+
> [["{\"id\": 844424930131974, \"label\": \"Person\", \"properties\": {\"gender\": \"male\", \"last_name\": \"Flintstone\", \"first_name\": \"Ed\"}}::vertex"],
|
380
|
+
> ["{\"id\": 844424930131976, \"label\": \"Person\", \"properties\": {\"gender\": \"female\", \"last_name\": \"Flintstone\", \"first_name\": \"Edna\"}}::vertex"],
|
381
|
+
> ["{\"id\": 844424930131986, \"label\": \"Person\", \"properties\": {\"gender\": \"male\", \"last_name\": \"Flintstone\", \"first_name\": \"Fred\"}}::vertex"],
|
382
|
+
> ["{\"id\": 844424930131975, \"label\": \"Person\", \"properties\": {\"gender\": \"male\", \"last_name\": \"Flintstone\", \"first_name\": \"Giggles\"}}::vertex"]]
|
383
|
+
```
|
384
|
+
|
385
|
+
|
35
386
|
Ideally, edit the HasJob class so that `start_node` would use a type `:person` and the `end_node` uses at type `:company` - this is not yet supported by the generator, but easy to do manually as shown below. (The problem is that I havent been able to figure out how load all the rails types in the testing environment).
|
36
387
|
|
37
388
|
ie:
|
@@ -78,6 +429,56 @@ $ psql -h localhost -p 5455 -U docker_username
|
|
78
429
|
> \q
|
79
430
|
```
|
80
431
|
|
432
|
+
### Experiment directly with AGE
|
433
|
+
|
434
|
+
Play with AGE/cypher directly (if desired) - see: https://age.apache.org/getstarted/quickstart
|
435
|
+
|
436
|
+
To create a graph, use the create_graph function located in the ag_catalog namespace.
|
437
|
+
|
438
|
+
enter postgres via:
|
439
|
+
- `psql -h localhost -p 5455 -U docker_username`
|
440
|
+
or
|
441
|
+
- `docker exec -it age psql -d postgresDB -U postgresUser`
|
442
|
+
|
443
|
+
then in psql:
|
444
|
+
```sql
|
445
|
+
-- For every connection of AGE you start, you will need to load the AGE extension.
|
446
|
+
CREATE EXTENSION age;
|
447
|
+
LOAD 'age';
|
448
|
+
SET search_path = ag_catalog, "$user", public;
|
449
|
+
|
450
|
+
SELECT create_graph('graph_name');
|
451
|
+
|
452
|
+
# To create a single vertex with label and properties, use the CREATE clause.
|
453
|
+
SELECT *
|
454
|
+
FROM cypher('graph_name', $$
|
455
|
+
CREATE (:label {property:"Node A"})
|
456
|
+
$$) as (v agtype);
|
457
|
+
|
458
|
+
# create a second vertex (node)
|
459
|
+
SELECT *
|
460
|
+
FROM cypher('graph_name', $$
|
461
|
+
CREATE (:label {property:"Node B"})
|
462
|
+
$$) as (v agtype);
|
463
|
+
|
464
|
+
# To create an edge between the nodes and set its properties:
|
465
|
+
SELECT *
|
466
|
+
FROM cypher('graph_name', $$
|
467
|
+
MATCH (a:label), (b:label)
|
468
|
+
WHERE a.property = 'Node A' AND b.property = 'Node B'
|
469
|
+
CREATE (a)-[e:RELTYPE {property:a.property + '<->' + b.property}]->(b)
|
470
|
+
RETURN e
|
471
|
+
$$) as (e agtype);
|
472
|
+
|
473
|
+
# Query the connected nodes:
|
474
|
+
SELECT * from cypher('graph_name', $$
|
475
|
+
MATCH (V)-[R]-(V2)
|
476
|
+
RETURN V,R,V2
|
477
|
+
$$) as (V agtype, R agtype, V2 agtype);
|
478
|
+
|
479
|
+
\q
|
480
|
+
```
|
481
|
+
|
81
482
|
### Install and Configure Rails (if not done already)
|
82
483
|
|
83
484
|
AGE REQUIRES POSTGRESQL!
|
@@ -269,87 +670,6 @@ The generator will only allow `:node` (default type) since at the time of runnin
|
|
269
670
|
`rails generate apache_age:node HasPet start_node:person end_node:pet caretaker_role`
|
270
671
|
but that doesn't work yet!
|
271
672
|
|
272
|
-
### AGE Rails Quick Example
|
273
|
-
|
274
|
-
```bash
|
275
|
-
rails new stone_age --database=postresql
|
276
|
-
cd stone_age
|
277
|
-
|
278
|
-
bundle add rails_age
|
279
|
-
bundle install
|
280
|
-
bin/rails apache_age:install
|
281
|
-
bin/rails apache_age:override_db_migrate
|
282
|
-
rails db:create
|
283
|
-
rails db:migrate
|
284
|
-
rails generate apache_age:scaffold_node Person first_name, last_name, gender
|
285
|
-
rails generate apache_age:scaffold_node Pet name gender species
|
286
|
-
rails generate apache_age:scaffold_edge HasChild role:string
|
287
|
-
rails generate apache_age:scaffold_edge HasSibling role:string
|
288
|
-
rails generate apache_age:scaffold_edge HasSpouse role:string
|
289
|
-
|
290
|
-
# seed file: [db/seed.rb](SEED.md)
|
291
|
-
rails db:seed
|
292
|
-
|
293
|
-
# Console Usage (seed doesn't provide any pets)
|
294
|
-
dino = Pet.create(name: 'Dino', gender: 'male', species: 'dinosaur')
|
295
|
-
dino.to_h
|
296
|
-
|
297
|
-
# find a person
|
298
|
-
fred = Person.find_by(first_name: 'Fred', last_name: 'Flintstone')
|
299
|
-
fred.to_h
|
300
|
-
|
301
|
-
pebbles = Person.find_by(first_name: 'Pebbles')
|
302
|
-
pebbles.to_h
|
303
|
-
|
304
|
-
# find an edge
|
305
|
-
father_relationship = HasChild.find_by(start_node: fred, end_node: pebbles)
|
306
|
-
father_relationship.to_h
|
307
|
-
> {:id=>1407374883553310,
|
308
|
-
:end_id=>844424930131996,
|
309
|
-
:start_id=>844424930131986,
|
310
|
-
:role=>"father",
|
311
|
-
:end_node=>{:id=>844424930131996, :last_name=>"Flintstone", :first_name=>"Pebbles", :gender=>"female"},
|
312
|
-
:start_node=>{:id=>844424930131986, :last_name=>"Flintstone", :first_name=>"Fred", :gender=>"male"}}
|
313
|
-
|
314
|
-
# where - find multiple nodes
|
315
|
-
family = Person.where(last_name: 'Flintstone').order(:first_name).limit(4).all.puts family.map(&:to_h)
|
316
|
-
|
317
|
-
family
|
318
|
-
> [{:id=>844424930131974, :last_name=>"Flintstone", :first_name=>"Ed", :gender=>"male"},
|
319
|
-
> {:id=>844424930131976, :last_name=>"Flintstone", :first_name=>"Edna", :gender=>"female"},
|
320
|
-
? {:id=>844424930131986, :last_name=>"Flintstone", :first_name=>"Fred", :gender=>"male"},
|
321
|
-
> {:id=>844424930131975, :last_name=>"Flintstone", :first_name=>"Giggles", :gender=>"male"}]
|
322
|
-
|
323
|
-
# all - unsorted
|
324
|
-
all_family = Person.where(last_name: 'Flintstone').all
|
325
|
-
puts all_family.map(&:to_h)
|
326
|
-
> {:id=>844424930131969, :last_name=>"Flintstone", :first_name=>"Zeke", :gender=>"female"}
|
327
|
-
> {:id=>844424930131970, :last_name=>"Flintstone", :first_name=>"Jed", :gender=>"male"}
|
328
|
-
> {:id=>844424930131971, :last_name=>"Flintstone", :first_name=>"Rockbottom", :gender=>"male"}
|
329
|
-
> {:id=>844424930131974, :last_name=>"Flintstone", :first_name=>"Ed", :gender=>"male"}
|
330
|
-
> {:id=>844424930131975, :last_name=>"Flintstone", :first_name=>"Giggles", :gender=>"male"}
|
331
|
-
> {:id=>844424930131976, :last_name=>"Flintstone", :first_name=>"Edna", :gender=>"female"}
|
332
|
-
> {:id=>844424930131986, :last_name=>"Flintstone", :first_name=>"Fred", :gender=>"male"}
|
333
|
-
> {:id=>844424930131987, :last_name=>"Flintstone", :first_name=>"Wilma", :gender=>"female"}
|
334
|
-
> {:id=>844424930131995, :last_name=>"Flintstone", :first_name=>"Stoney", :gender=>"male"}
|
335
|
-
> {:id=>844424930131996, :last_name=>"Flintstone", :first_name=>"Pebbles", :gender=>"female"}
|
336
|
-
|
337
|
-
# where - multiple edges (relations) - for now only edge attributes and start/end nodes can be queried
|
338
|
-
parental_relations = HasChild.where(end_node: pebbles)
|
339
|
-
puts parental_relations.map(&:to_h)
|
340
|
-
> {:id=>1407374883553310, :end_id=>844424930131996, :start_id=>844424930131986, :role=>"father", :end_node=>{:id=>844424930131996, :last_name=>"Flintstone", :first_name=>"Pebbles", :gender=>"female"}, :start_node=>{:id=>844424930131986, :last_name=>"Flintstone", :first_name=>"Fred", :gender=>"male"}}
|
341
|
-
> {:id=>1407374883553309, :end_id=>844424930131996, :start_id=>844424930131987, :role=>"mother", :end_node=>{:id=>844424930131996, :last_name=>"Flintstone", :first_name=>"Pebbles", :gender=>"female"}, :start_node=>{:id=>844424930131987, :last_name=>"Flintstone", :first_name=>"Wilma", :gender=>"female"}}
|
342
|
-
|
343
|
-
|
344
|
-
raw_pg_results = Person.where(last_name: 'Flintstone').order(:first_name).limit(4).execute
|
345
|
-
=> #<PG::Result:0x000000012255f348 status=PGRES_TUPLES_OK ntuples=4 nfields=1 cmd_tuples=4>
|
346
|
-
raw_pg_results.values
|
347
|
-
> [["{\"id\": 844424930131974, \"label\": \"Person\", \"properties\": {\"gender\": \"male\", \"last_name\": \"Flintstone\", \"first_name\": \"Ed\"}}::vertex"],
|
348
|
-
> ["{\"id\": 844424930131976, \"label\": \"Person\", \"properties\": {\"gender\": \"female\", \"last_name\": \"Flintstone\", \"first_name\": \"Edna\"}}::vertex"],
|
349
|
-
> ["{\"id\": 844424930131986, \"label\": \"Person\", \"properties\": {\"gender\": \"male\", \"last_name\": \"Flintstone\", \"first_name\": \"Fred\"}}::vertex"],
|
350
|
-
> ["{\"id\": 844424930131975, \"label\": \"Person\", \"properties\": {\"gender\": \"male\", \"last_name\": \"Flintstone\", \"first_name\": \"Giggles\"}}::vertex"]]
|
351
|
-
```
|
352
|
-
|
353
673
|
### Age Cypher Queries
|
354
674
|
|
355
675
|
```ruby
|