rails_age 0.6.0 → 0.6.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -18
- data/README.md +243 -7
- data/lib/apache_age/cypher.rb +95 -0
- data/lib/apache_age/entities/class_methods.rb +83 -43
- data/lib/apache_age/entities/common_methods.rb +5 -3
- data/lib/apache_age/entities/edge.rb +32 -7
- data/lib/apache_age/entities/entity.rb +21 -10
- data/lib/apache_age/entities/node.rb +29 -7
- data/lib/apache_age/entities/path.rb +27 -0
- data/lib/apache_age/entities/query_builder.rb +180 -0
- data/lib/apache_age/path.rb +4 -0
- data/lib/rails_age/version.rb +1 -1
- data/lib/rails_age.rb +4 -0
- metadata +22 -5
- data/lib/apache_age/entities/vertex.rb +0 -53
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '01018a3efe8d4cef46fce365b6fd442882d8bf4ff51e0ad3d3bdb235db80ebb7'
|
4
|
+
data.tar.gz: 69a20eb39f9609c6350463d6a391b7f08887079e51e56602a6357d914571ed8f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cc2ea56dcb102213397ca5ffc19edc095341e51307b0500d0be7bc7595cb65f64e74c6490a678703f5faf903d0396010b63b8dd33e162ed218e038a4e95e05f5
|
7
|
+
data.tar.gz: 30e0472fb006bf883cb57a5c7406571d4fc4715a9d6d96e792dd5d3b4998e538eab74f809b5d384072d88e77ce2d6fb143cdb6a5c20fd044fd74673edd2bb13a
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,20 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
-
## VERSION 0.
|
3
|
+
## VERSION 0.9.1 - xxxx-xx-xx
|
4
|
+
|
5
|
+
- **Edge Generator**
|
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)
|
9
|
+
|
10
|
+
- **Edge Scaffold** with node types?
|
11
|
+
* add `rails generate apache_age:edge_scaffold HasJob employee_role start_node:person end_node:company`
|
12
|
+
|
13
|
+
## VERSION 0.9.0 - 2024-xx-xx
|
14
|
+
- **AGE visual paths graph**
|
15
|
+
* add `rails generate apache_age:visualize`
|
16
|
+
|
17
|
+
## VERSION 0.8.0 - 2024-xx-xx
|
4
18
|
|
5
19
|
- **cypher queries** (like active record queries)
|
6
20
|
* schema override
|
@@ -8,37 +22,51 @@
|
|
8
22
|
* paths support
|
9
23
|
* select attributes support
|
10
24
|
|
11
|
-
## VERSION 0.
|
12
|
-
|
13
|
-
- **Age Path**
|
14
|
-
|
15
|
-
## VERSION 0.6.2 - 2024-xx-xx
|
25
|
+
## VERSION 0.8.0 - 2024-xx-xx
|
16
26
|
|
17
27
|
breaking change?: namespaces (by default) will use their own schema? (add to database.yml & schema.rb ?)
|
18
28
|
|
19
29
|
- **AGE Schema override**
|
20
30
|
|
21
|
-
- **
|
31
|
+
- **Multiple AGE Schema**
|
22
32
|
|
23
|
-
## VERSION 0.
|
33
|
+
## VERSION 0.7.0 - 2024-xx-xx
|
24
34
|
|
25
|
-
- **
|
26
|
-
*
|
35
|
+
- **Age Path** - nodes and edges combined
|
36
|
+
* add `rails generate apache_age:path_scaffold HasJob employee_role start_node:person end_node:company`
|
27
37
|
|
28
|
-
- **Edge Generator**
|
29
|
-
* add start-/end-nodes types to edge generator (would make scaffold easier), ie:
|
30
|
-
`rails generate apache_age:edge HasPet owner_role start_node:person end_node:pet`
|
31
|
-
with property and specified start-/end-nodes (person and pet nodes must have already been created)
|
32
38
|
|
33
|
-
|
34
|
-
|
39
|
+
## VERSION 0.6.3 - 2024-xx-xx
|
40
|
+
|
41
|
+
- **Query Sanitize**:
|
42
|
+
* reject attributes not defined in model
|
43
|
+
* sanitize strings using: id(find) = ?, 23 & find.first_name = ?, 'John'
|
44
|
+
|
45
|
+
## VERSION 0.6.2 - 2024-09-30
|
46
|
+
|
47
|
+
- **Query Sanitize**
|
48
|
+
* hashes sanitized
|
49
|
+
|
50
|
+
- **TODO**:
|
51
|
+
* reject attributes not defined in model
|
52
|
+
* sanitize strings using: id(find) = ?, 23 & find.first_name = ?, 'John'
|
53
|
+
|
54
|
+
## VERSION 0.6.1 - 2024-09-29
|
55
|
+
|
56
|
+
**Queries are not yet sanitize (injection filtered)!**
|
57
|
+
|
58
|
+
- **where nodes** - Edge and Node
|
59
|
+
- **where edges** - allow subquery on node attributes?
|
60
|
+
- **limit** - limit the number of results returned
|
61
|
+
|
62
|
+
## VERSION 0.6.0 - 2024-06-28
|
35
63
|
|
36
|
-
|
64
|
+
**Document showing errors**
|
37
65
|
|
38
66
|
**breaking changes**: update naming
|
39
67
|
* renamed `Entities::Vertex` module to `Entities::Node`
|
40
68
|
* renamed `UniqueVertex` to `UniqueNode`
|
41
|
-
* rebamed `AgeTypeGenerator` to `Type::Factory`
|
69
|
+
* rebamed `AgeTypeGenerator.create_type_for` to `Type::Factory.type_for`
|
42
70
|
* move `lib/generators/*` intp `lib/apache_age/generators`
|
43
71
|
|
44
72
|
here is the [commit](https://github.com/marpori/rails_age_demo_app/commit/a6f0708f2bbc165eddbafe63896068a72d803b17) to see the changes te demo app to make it work for release 0.6.0
|
data/README.md
CHANGED
@@ -147,24 +147,260 @@ rails generate apache_age:scaffold_node Person first_name last_name
|
|
147
147
|
rails generate apache_age:scaffold_node Animals/Pet pet_name birthdate:date
|
148
148
|
```
|
149
149
|
|
150
|
-
|
151
|
-
|
152
|
-
NOTE: the generator will only allow `:node` (default type) for start_node and end_node, however, it is strongly recommended to specify the start_node and end_node types manually. _Hopefully, I can find a way to get the generators to recognize and allow the usage of custom node types. Thus eventually, I hope: `rails generate apache_age:node HasPet start_node:person end_node:pet caretaker_role` will work._
|
153
|
-
|
150
|
+
now you can test your nodes at:
|
154
151
|
```bash
|
155
|
-
|
152
|
+
http://localhost/people
|
153
|
+
# and
|
154
|
+
http://localhost/animals_pets
|
156
155
|
```
|
157
156
|
|
158
|
-
|
157
|
+
Note: Turbo seems to interfere with the default rails template's ability to show errors, this can easily be fixed by disabling turbo for forms that where turbo isn't needed by adding `data: { turbo: false }` to the form, ie:
|
158
|
+
```ruby
|
159
|
+
<%= form_with(model: animals_pet, data: { turbo: false }) do |form| %>
|
160
|
+
...
|
161
|
+
<% end %>
|
162
|
+
```
|
163
|
+
|
164
|
+
### EDGE Scaffold Generation**
|
159
165
|
|
160
166
|
```bash
|
161
167
|
# without a namespace
|
162
168
|
rails generate apache_age:scaffold_edge HasPet caretaker_role
|
169
|
+
rails generate apache_age:edge HasJob employee_role begin_date:date
|
163
170
|
|
164
171
|
# with a namespace
|
165
|
-
rails generate apache_age:scaffold_edge People/
|
172
|
+
rails generate apache_age:scaffold_edge People/HasSpouce spousal_role
|
166
173
|
```
|
167
174
|
|
175
|
+
now you can test your edges at:
|
176
|
+
```bash
|
177
|
+
http://localhost/has_pets
|
178
|
+
http://localhost/has_jobs
|
179
|
+
# and
|
180
|
+
http://localhost/people/has_spouses
|
181
|
+
```
|
182
|
+
|
183
|
+
you can improve the view to only show the items you expect to be associated with the start- and end-node by changing the selects in the form from the generic form (finds all nodes):
|
184
|
+
```ruby
|
185
|
+
<div>
|
186
|
+
<%= form.label :end_node, style: "display: block" %>
|
187
|
+
<%= form.collection_select(:end_id, ApacheAge::Node.all, :id, :display, prompt: 'Select an End-Node') %>
|
188
|
+
</div>
|
189
|
+
```
|
190
|
+
to selecting a specific node expected (along with the desired 'name' in the list)
|
191
|
+
```ruby
|
192
|
+
<div>
|
193
|
+
<%= form.label :end_node, style: "display: block" %>
|
194
|
+
<%= form.collection_select(:end_id, Company.all, :id, :company_name, prompt: 'Select a Company') %>
|
195
|
+
</div>
|
196
|
+
```
|
197
|
+
so full form change for has_job could look like:
|
198
|
+
```ruby
|
199
|
+
# app/views/has_jobs/_form.html.erb
|
200
|
+
<%= form_with(model: has_job, url: form_url) do |form| %>
|
201
|
+
<% if has_job.errors.any? %>
|
202
|
+
<div style="color: red">
|
203
|
+
<h2><%= pluralize(has_job.errors.count, "error") %> prohibited this has_job from being saved:</h2>
|
204
|
+
|
205
|
+
<ul>
|
206
|
+
<% has_job.errors.each do |error| %>
|
207
|
+
<li><%= error.full_message %></li>
|
208
|
+
<% end %>
|
209
|
+
</ul>
|
210
|
+
</div>
|
211
|
+
<% end %>
|
212
|
+
|
213
|
+
<div>
|
214
|
+
<%= form.label :employee_role, style: "display: block" %>
|
215
|
+
<%= form.text_field :employee_role %>
|
216
|
+
</div>
|
217
|
+
|
218
|
+
<div>
|
219
|
+
<%= form.label :start_node, style: "display: block" %>
|
220
|
+
<%= form.collection_select(:start_id, Person.all, :id, :first_name, prompt: 'Select a person') %>
|
221
|
+
</div>
|
222
|
+
|
223
|
+
<div>
|
224
|
+
<%= form.label :end_node, style: "display: block" %>
|
225
|
+
<%= form.collection_select(:end_id, Company.all, :id, :company_name, prompt: 'Select a Company') %>
|
226
|
+
</div>
|
227
|
+
|
228
|
+
<div>
|
229
|
+
<%= form.submit %>
|
230
|
+
</div>
|
231
|
+
<% end %>
|
232
|
+
```
|
233
|
+
|
234
|
+
To make your code more robust (enforce that the appropriate node type is associate with the start- and end-nodes) you can adjust the edge definition by adding the node type to the `start_node` and `end_node` attributes.
|
235
|
+
from
|
236
|
+
```ruby
|
237
|
+
attribute :start_node
|
238
|
+
attribute :end_node
|
239
|
+
```
|
240
|
+
to:
|
241
|
+
```ruby
|
242
|
+
attribute :start_node, :person
|
243
|
+
attribute :end_node, :company
|
244
|
+
```
|
245
|
+
For example you can make the edge/has_pet.rb more robust by making the model look like:
|
246
|
+
```ruby
|
247
|
+
# app/edges/has_job.rb
|
248
|
+
class HasJob
|
249
|
+
include ApacheAge::Entities::Edge
|
250
|
+
|
251
|
+
attribute :employee_role, :string
|
252
|
+
attribute :start_node, :person
|
253
|
+
attribute :end_node, :company
|
254
|
+
|
255
|
+
validates :employee_role, presence: true
|
256
|
+
validate :validate_unique
|
257
|
+
|
258
|
+
private
|
259
|
+
|
260
|
+
def validate_unique
|
261
|
+
ApacheAge::Validators::UniqueEdge
|
262
|
+
.new(attributes: %i[employee_role start_node end_node])
|
263
|
+
.validate(self)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
```
|
267
|
+
|
268
|
+
The generator will only allow `:node` (default type) since at the time of running the generator (at least within tests, the custom types are not known), eventually, I hope to find a way to fix that and allow:
|
269
|
+
`rails generate apache_age:node HasPet start_node:person end_node:pet caretaker_role`
|
270
|
+
but that doesn't work yet!
|
271
|
+
|
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
|
+
### Age Cypher Queries
|
354
|
+
|
355
|
+
```ruby
|
356
|
+
flintstone_family =
|
357
|
+
Person.where(last_name: 'Flintstone')
|
358
|
+
.order(:first_name)
|
359
|
+
.limit(4).all
|
360
|
+
.map(&:to_h)
|
361
|
+
|
362
|
+
# generates the query
|
363
|
+
SELECT *
|
364
|
+
FROM cypher('age_schema', $$
|
365
|
+
MATCH (find:Person)
|
366
|
+
WHERE find.last_name = 'Flintstone'
|
367
|
+
RETURN find
|
368
|
+
ORDER BY find.first_name
|
369
|
+
LIMIT 4
|
370
|
+
$$) as (Person agtype);
|
371
|
+
|
372
|
+
# and returns:
|
373
|
+
[{:id=>844424930131974, :last_name=>"Flintstone", :first_name=>"Ed", :gender=>"male"},
|
374
|
+
{:id=>844424930131976, :last_name=>"Flintstone", :first_name=>"Edna", :gender=>"female"},
|
375
|
+
{:id=>844424930131986, :last_name=>"Flintstone", :first_name=>"Fred", :gender=>"male"},
|
376
|
+
{:id=>844424930131975, :last_name=>"Flintstone", :first_name=>"Giggles", :gender=>"male"}]
|
377
|
+
```
|
378
|
+
|
379
|
+
```ruby
|
380
|
+
query =
|
381
|
+
Person.
|
382
|
+
cypher('age_schema')
|
383
|
+
.match("(a:Person), (b:Person)")
|
384
|
+
.where("a.name = 'Node A'", "b.name = 'Node B'")
|
385
|
+
.return("a.name", "b.name")
|
386
|
+
.as("name_a agtype, name_b agtype")
|
387
|
+
.execute
|
388
|
+
```
|
389
|
+
|
390
|
+
or more generally (soon - not yet tested nor santized):
|
391
|
+
|
392
|
+
```ruby
|
393
|
+
tihen =
|
394
|
+
ApacheAge::Cypher
|
395
|
+
.new('age_schema')
|
396
|
+
.create("(person:Person {name: 'Tihen'})")
|
397
|
+
.return('person')
|
398
|
+
.as('Person agtype')
|
399
|
+
.execute
|
400
|
+
```
|
401
|
+
|
402
|
+
see [AGE Cypher Queries](AGE_CYPHER_QUERIES.md)
|
403
|
+
|
168
404
|
### AGE Usage within Rails Console
|
169
405
|
|
170
406
|
see [AGE Usage within Rails Console](AGE_CONSOLE_USAGE.md)
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module ApacheAge
|
2
|
+
class Cypher
|
3
|
+
class << self
|
4
|
+
attr_accessor :model_class
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(graph_name = 'age_schema')
|
8
|
+
@graph_name = graph_name
|
9
|
+
@query = ""
|
10
|
+
@as_type = "result agtype"
|
11
|
+
end
|
12
|
+
|
13
|
+
def match(pattern)
|
14
|
+
@query += "MATCH #{pattern} "
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
# WITH n.name as name, n.age as age
|
19
|
+
# WITH otherPerson, count(*) AS foaf WHERE foaf > 1
|
20
|
+
# with has a lot of cases - see docs
|
21
|
+
def with(*conditions)
|
22
|
+
@query += "WITH #{variables.join(', ')} "
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def where(*conditions)
|
27
|
+
condition_str = conditions.join(' AND ')
|
28
|
+
# @query += "WHERE #{condition_str} "
|
29
|
+
# If there's already a WHERE clause in the query, append to it (they need to be adjacent!)
|
30
|
+
@query += (@query.include?("WHERE") ? " AND #{condition_str} " : "WHERE #{condition_str} ")
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
# ORDER BY n.age DESC, n.name ASC
|
35
|
+
def order_by(*conditions)
|
36
|
+
@query += "ORDER BY #{variables.join(', ')} "
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
# can use full names n.name or aliases (with) name
|
41
|
+
def return(*variables)
|
42
|
+
@query += "RETURN #{variables.join(', ')} "
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
def create(node)
|
47
|
+
@query += "CREATE #{node} "
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
def set(properties)
|
52
|
+
@query += "SET #{properties} "
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
def remove(property)
|
57
|
+
@query += "REMOVE #{property} "
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
def delete(entity)
|
62
|
+
@query += "DELETE #{entity} "
|
63
|
+
self
|
64
|
+
end
|
65
|
+
|
66
|
+
def merge(pattern)
|
67
|
+
@query += "MERGE #{pattern} "
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
def skip(count)
|
72
|
+
@query += "SKIP #{count} "
|
73
|
+
self
|
74
|
+
end
|
75
|
+
|
76
|
+
def limit(count)
|
77
|
+
@query += "LIMIT #{count} "
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
def as(type)
|
82
|
+
@as_type = type
|
83
|
+
self
|
84
|
+
end
|
85
|
+
|
86
|
+
def to_cypher
|
87
|
+
"SELECT * FROM cypher('#{@graph_name}', $$ #{@query.strip} $$) AS (#{@as_type});"
|
88
|
+
end
|
89
|
+
|
90
|
+
def execute
|
91
|
+
cypher_sql = to_cypher
|
92
|
+
ActiveRecord::Base.connection.execute(cypher_sql)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -8,58 +8,44 @@ module ApacheAge
|
|
8
8
|
instance
|
9
9
|
end
|
10
10
|
|
11
|
-
def
|
12
|
-
|
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)
|
11
|
+
def where(attributes)
|
12
|
+
query_builder = QueryBuilder.new(self)
|
13
|
+
query_builder.where(attributes)
|
21
14
|
end
|
22
15
|
|
23
|
-
def
|
24
|
-
|
25
|
-
|
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?
|
16
|
+
def all = QueryBuilder.new(self).all
|
17
|
+
def first = QueryBuilder.new(self).limit(1).first
|
18
|
+
def find(id) = where(id: id).first
|
32
19
|
|
33
|
-
|
34
|
-
|
35
|
-
hash = JSON.parse(json_string)
|
36
|
-
attribs = hash.except('label', 'properties').merge(hash['properties']).symbolize_keys
|
20
|
+
def find_by(attributes)
|
21
|
+
return nil if attributes.reject { |k, v| v.blank? }.empty?
|
37
22
|
|
38
|
-
|
39
|
-
end
|
23
|
+
where(attributes).limit(1).first
|
40
24
|
end
|
41
25
|
|
42
26
|
# Private stuff
|
43
27
|
|
44
|
-
|
28
|
+
# used? or dead code?
|
29
|
+
def where_edge(attributes)
|
45
30
|
where_attribs =
|
46
31
|
attributes
|
47
|
-
|
48
|
-
|
49
|
-
|
32
|
+
.compact
|
33
|
+
.except(:end_id, :start_id, :end_node, :start_node)
|
34
|
+
.map { |k, v| ActiveRecord::Base.sanitize_sql(["find.#{k} = ?", v]) }
|
35
|
+
.join(' AND ')
|
50
36
|
where_attribs = where_attribs.empty? ? nil : where_attribs
|
51
37
|
|
52
38
|
end_id = attributes[:end_id] || attributes[:end_node]&.id
|
53
39
|
start_id = attributes[:start_id] || attributes[:start_node]&.id
|
54
|
-
where_end_id = end_id ? "id(end_node) =
|
55
|
-
where_start_id = start_id ? "id(start_node) =
|
40
|
+
where_end_id = end_id ? ActiveRecord::Base.sanitize_sql(["id(end_node) = ?", end_id]) : nil
|
41
|
+
where_start_id = start_id ? ActiveRecord::Base.sanitize_sql(["id(start_node) = ?", start_id]) : nil
|
56
42
|
|
57
43
|
where_clause = [where_attribs, where_start_id, where_end_id].compact.join(' AND ')
|
58
44
|
return nil if where_clause.empty?
|
59
45
|
|
60
|
-
cypher_sql =
|
46
|
+
cypher_sql = edge_sql(where_clause)
|
61
47
|
|
62
|
-
|
48
|
+
execute_where(cypher_sql)
|
63
49
|
end
|
64
50
|
|
65
51
|
def age_graph = 'age_schema'
|
@@ -70,17 +56,21 @@ module ApacheAge
|
|
70
56
|
age_type == 'vertex' ? "(find:#{age_label})" : "(start_node)-[find:#{age_label}]->(end_node)"
|
71
57
|
end
|
72
58
|
|
73
|
-
def
|
74
|
-
|
75
|
-
|
59
|
+
def execute_sql(cypher_sql) = ActiveRecord::Base.connection.execute(cypher_sql)
|
60
|
+
|
61
|
+
def execute_find(cypher_sql) = execute_where(cypher_sql).first
|
76
62
|
|
77
|
-
|
78
|
-
|
63
|
+
def execute_where(cypher_sql)
|
64
|
+
age_results = ActiveRecord::Base.connection.execute(cypher_sql)
|
65
|
+
return [] if age_results.values.count.zero?
|
79
66
|
|
80
|
-
|
81
|
-
|
67
|
+
age_results.values.map do |value|
|
68
|
+
json_data = value.first.split('::').first
|
69
|
+
hash = JSON.parse(json_data)
|
70
|
+
attribs = hash.except('label', 'properties').merge(hash['properties']).symbolize_keys
|
82
71
|
|
83
|
-
|
72
|
+
new(**attribs)
|
73
|
+
end
|
84
74
|
end
|
85
75
|
|
86
76
|
def all_sql
|
@@ -93,7 +83,7 @@ module ApacheAge
|
|
93
83
|
SQL
|
94
84
|
end
|
95
85
|
|
96
|
-
def
|
86
|
+
def node_sql(where_clause)
|
97
87
|
<<-SQL
|
98
88
|
SELECT *
|
99
89
|
FROM cypher('#{age_graph}', $$
|
@@ -104,7 +94,7 @@ module ApacheAge
|
|
104
94
|
SQL
|
105
95
|
end
|
106
96
|
|
107
|
-
def
|
97
|
+
def edge_sql(where_clause)
|
108
98
|
<<-SQL
|
109
99
|
SELECT *
|
110
100
|
FROM cypher('#{age_graph}', $$
|
@@ -114,6 +104,56 @@ module ApacheAge
|
|
114
104
|
$$) as (#{age_label} agtype);
|
115
105
|
SQL
|
116
106
|
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def where_node_clause(attributes)
|
111
|
+
build_core_where_clause(attributes)
|
112
|
+
end
|
113
|
+
|
114
|
+
def where_edge_clause(attributes)
|
115
|
+
core_attributes = attributes.except(:end_id, :start_id, :end_node, :start_node)
|
116
|
+
core_clauses = core_attributes.empty? ? nil : build_core_where_clause(core_attributes)
|
117
|
+
|
118
|
+
end_id =
|
119
|
+
if attributes[:end_id]
|
120
|
+
attributes[:end_id]
|
121
|
+
elsif attributes[:end_node].is_a?(Node)
|
122
|
+
attributes[:end_node]&.id
|
123
|
+
end
|
124
|
+
where_end_id = end_id ? ActiveRecord::Base.sanitize_sql(["id(end_node) = ?", end_id]) : nil
|
125
|
+
|
126
|
+
start_id =
|
127
|
+
if attributes[:start_id]
|
128
|
+
attributes[:start_id]
|
129
|
+
elsif attributes[:start_node].is_a?(Node)
|
130
|
+
attributes[:start_node]&.id
|
131
|
+
end
|
132
|
+
where_start_id = start_id ? ActiveRecord::Base.sanitize_sql(["id(start_node) = ?", start_id]) : nil
|
133
|
+
|
134
|
+
where_end_attrs =
|
135
|
+
if attributes[:end_node].is_a?(Hash)
|
136
|
+
attributes[:end_node].map { |k, v| ActiveRecord::Base.sanitize_sql(["end_node.#{k} = ?", v]) }
|
137
|
+
end
|
138
|
+
where_start_attrs =
|
139
|
+
if attributes[:start_node].is_a?(Hash)
|
140
|
+
attributes[:start_node].map { |k, v| ActiveRecord::Base.sanitize_sql(["start_node.#{k} = ?", v]) }
|
141
|
+
end
|
142
|
+
|
143
|
+
[core_clauses, where_start_id, where_end_id, where_start_attrs, where_end_attrs]
|
144
|
+
.flatten.compact.join(' AND ')
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
def build_core_where_clause(attributes)
|
149
|
+
attributes
|
150
|
+
.compact
|
151
|
+
.map do |k, v|
|
152
|
+
query_string = k == :id ? "id(find) = #{v}" : "find.#{k} = '#{v}'"
|
153
|
+
ActiveRecord::Base.sanitize_sql([query_string, v])
|
154
|
+
end
|
155
|
+
.join(' AND ')
|
156
|
+
end
|
117
157
|
end
|
118
158
|
end
|
119
159
|
end
|
@@ -57,13 +57,14 @@ module ApacheAge
|
|
57
57
|
def destroy
|
58
58
|
match_clause = (age_type == 'vertex' ? "(done:#{age_label})" : "()-[done:#{age_label}]->()")
|
59
59
|
delete_clause = (age_type == 'vertex' ? 'DETACH DELETE done' : 'DELETE done')
|
60
|
+
sanitized_id = ActiveRecord::Base.sanitize_sql(["id(done) = ?", id])
|
60
61
|
cypher_sql =
|
61
62
|
<<-SQL
|
62
63
|
SELECT *
|
63
64
|
FROM cypher('#{age_graph}', $$
|
64
65
|
MATCH #{match_clause}
|
65
|
-
WHERE
|
66
|
-
|
66
|
+
WHERE #{sanitized_id}
|
67
|
+
#{delete_clause}
|
67
68
|
return done
|
68
69
|
$$) as (deleted agtype);
|
69
70
|
SQL
|
@@ -99,7 +100,8 @@ module ApacheAge
|
|
99
100
|
def properties_to_s
|
100
101
|
string_values =
|
101
102
|
age_properties.each_with_object([]) do |(key, val), array|
|
102
|
-
|
103
|
+
sanitized_val = ActiveRecord::Base.sanitize_sql(["?", val])
|
104
|
+
array << "#{key}: #{sanitized_val}"
|
103
105
|
end
|
104
106
|
"{#{string_values.join(', ')}}"
|
105
107
|
end
|
@@ -7,6 +7,8 @@ module ApacheAge
|
|
7
7
|
include ActiveModel::Model
|
8
8
|
include ActiveModel::Dirty
|
9
9
|
include ActiveModel::Attributes
|
10
|
+
include ActiveModel::Validations
|
11
|
+
include ActiveModel::Validations::Callbacks
|
10
12
|
|
11
13
|
attribute :id, :integer
|
12
14
|
# attribute :label, :string
|
@@ -68,12 +70,25 @@ module ApacheAge
|
|
68
70
|
def create_sql
|
69
71
|
self.start_node = start_node.save unless start_node.persisted?
|
70
72
|
self.end_node = end_node.save unless end_node.persisted?
|
73
|
+
|
74
|
+
start_node_age_label = ActiveRecord::Base.sanitize_sql(start_node.age_label)
|
75
|
+
end_node_age_label = ActiveRecord::Base.sanitize_sql(end_node.age_label)
|
76
|
+
sanitized_start_id = ActiveRecord::Base.sanitize_sql(["?", start_node.id])
|
77
|
+
sanitized_end_id = ActiveRecord::Base.sanitize_sql(["?", end_node.id])
|
78
|
+
# cant use sanitize_sql_like because it escapes the % and _ characters
|
79
|
+
# label_name = ActiveRecord::Base.sanitize_sql_like(age_label)
|
80
|
+
|
81
|
+
reject_keys = %i[id start_id end_id start_node end_node]
|
82
|
+
sanitized_properties =
|
83
|
+
self.to_h.reject { |k, _v| reject_keys.include?(k) }.reject { |_k, v| v.nil? }
|
84
|
+
.map { |k, v| "#{k}: #{ActiveRecord::Base.sanitize_sql(["?", v])}" }
|
85
|
+
.join(', ')
|
71
86
|
<<-SQL
|
72
87
|
SELECT *
|
73
88
|
FROM cypher('#{age_graph}', $$
|
74
|
-
MATCH (from_node:#{
|
75
|
-
WHERE id(from_node) = #{
|
76
|
-
CREATE (from_node)-[edge#{
|
89
|
+
MATCH (from_node:#{start_node_age_label}), (to_node:#{end_node_age_label})
|
90
|
+
WHERE id(from_node) = #{sanitized_start_id} AND id(to_node) = #{sanitized_end_id}
|
91
|
+
CREATE (from_node)-[edge:#{age_label} {#{sanitized_properties}}]->(to_node)
|
77
92
|
RETURN edge
|
78
93
|
$$) as (edge agtype);
|
79
94
|
SQL
|
@@ -82,14 +97,24 @@ module ApacheAge
|
|
82
97
|
# So far just properties of string type with '' around them
|
83
98
|
def update_sql
|
84
99
|
alias_name = age_alias || age_label.downcase
|
85
|
-
|
86
|
-
age_properties.map
|
100
|
+
set_clause =
|
101
|
+
age_properties.map do |k, v|
|
102
|
+
if v
|
103
|
+
sanitized_value = ActiveRecord::Base.sanitize_sql(["?", v])
|
104
|
+
"#{alias_name}.#{k} = #{sanitized_value}"
|
105
|
+
else
|
106
|
+
"#{alias_name}.#{k} = NULL"
|
107
|
+
end
|
108
|
+
end.join(', ')
|
109
|
+
|
110
|
+
sanitized_id = ActiveRecord::Base.sanitize_sql(["?", id])
|
111
|
+
|
87
112
|
<<-SQL
|
88
113
|
SELECT *
|
89
114
|
FROM cypher('#{age_graph}', $$
|
90
115
|
MATCH ()-[#{alias_name}:#{age_label}]->()
|
91
|
-
WHERE id(#{alias_name}) = #{
|
92
|
-
SET #{
|
116
|
+
WHERE id(#{alias_name}) = #{sanitized_id}
|
117
|
+
SET #{set_clause}
|
93
118
|
RETURN #{alias_name}
|
94
119
|
$$) as (#{age_label} agtype);
|
95
120
|
SQL
|
@@ -3,14 +3,20 @@ module ApacheAge
|
|
3
3
|
class Entity
|
4
4
|
class << self
|
5
5
|
def find_by(attributes)
|
6
|
-
where_clause =
|
6
|
+
where_clause =
|
7
|
+
attributes
|
8
|
+
.map do |k, v|
|
9
|
+
if k == :id
|
10
|
+
ActiveRecord::Base.sanitize_sql(["id(find) = ?", v])
|
11
|
+
else
|
12
|
+
ActiveRecord::Base.sanitize_sql(["find.#{k} = ?", v])
|
13
|
+
end
|
14
|
+
end
|
15
|
+
.join(' AND ')
|
7
16
|
handle_find(where_clause)
|
8
17
|
end
|
9
18
|
|
10
|
-
def find(id)
|
11
|
-
where_clause = "id(find) = #{id}"
|
12
|
-
handle_find(where_clause)
|
13
|
-
end
|
19
|
+
def find(id) = find_by(id: id)
|
14
20
|
|
15
21
|
private
|
16
22
|
|
@@ -46,19 +52,24 @@ module ApacheAge
|
|
46
52
|
json_data = JSON.parse(json_string)
|
47
53
|
|
48
54
|
age_label = json_data['label']
|
49
|
-
attribs =
|
50
|
-
|
51
|
-
|
55
|
+
attribs =
|
56
|
+
json_data
|
57
|
+
.except('label', 'properties')
|
58
|
+
.merge(json_data['properties'])
|
59
|
+
.symbolize_keys
|
52
60
|
|
53
61
|
"#{json_data['label'].gsub('__', '::')}".constantize.new(**attribs)
|
54
62
|
end
|
55
63
|
|
56
64
|
def find_sql(match_clause, where_clause)
|
65
|
+
sanitized_match_clause = ActiveRecord::Base.sanitize_sql(match_clause)
|
66
|
+
sanitized_where_clause = where_clause # Already sanitized in `find_by` or `find`
|
67
|
+
|
57
68
|
<<-SQL
|
58
69
|
SELECT *
|
59
70
|
FROM cypher('#{age_graph}', $$
|
60
|
-
MATCH #{
|
61
|
-
WHERE #{
|
71
|
+
MATCH #{sanitized_match_clause}
|
72
|
+
WHERE #{sanitized_where_clause}
|
62
73
|
RETURN find
|
63
74
|
$$) as (found agtype);
|
64
75
|
SQL
|
@@ -7,6 +7,8 @@ module ApacheAge
|
|
7
7
|
include ActiveModel::Model
|
8
8
|
include ActiveModel::Dirty
|
9
9
|
include ActiveModel::Attributes
|
10
|
+
include ActiveModel::Validations
|
11
|
+
include ActiveModel::Validations::Callbacks
|
10
12
|
|
11
13
|
attribute :id, :integer
|
12
14
|
|
@@ -23,11 +25,21 @@ module ApacheAge
|
|
23
25
|
# RETURN company
|
24
26
|
# $$) as (Company agtype);
|
25
27
|
def create_sql
|
28
|
+
# can't use sanitiye without a solution for '_' in the alias name & label
|
29
|
+
# alias_name = ActiveRecord::Base.sanitize_sql_like(age_alias || age_label.downcase)
|
30
|
+
# label_name = ActiveRecord::Base.sanitize_sql_like(age_label)
|
31
|
+
|
26
32
|
alias_name = age_alias || age_label.downcase
|
27
|
-
|
33
|
+
sanitized_properties =
|
34
|
+
self
|
35
|
+
.to_h.reject { |k, v| k == :id }.reject { |k, v| v.nil? }
|
36
|
+
.map { |k, v| "#{k}: #{ActiveRecord::Base.sanitize_sql(["?", v])}" }
|
37
|
+
.join(', ')
|
38
|
+
|
39
|
+
<<~SQL.squish
|
28
40
|
SELECT *
|
29
41
|
FROM cypher('#{age_graph}', $$
|
30
|
-
CREATE (#{alias_name}#{
|
42
|
+
CREATE (#{alias_name}:#{age_label} {#{sanitized_properties}})
|
31
43
|
RETURN #{alias_name}
|
32
44
|
$$) as (#{age_label} agtype);
|
33
45
|
SQL
|
@@ -35,19 +47,29 @@ module ApacheAge
|
|
35
47
|
|
36
48
|
# So far just properties of string type with '' around them
|
37
49
|
def update_sql
|
38
|
-
alias_name = age_alias || age_label.downcase
|
39
|
-
|
40
|
-
|
50
|
+
alias_name = ActiveRecord::Base.sanitize_sql_like(age_alias || age_label.downcase)
|
51
|
+
sanitized_set_clause = age_properties.map do |k, v|
|
52
|
+
if v
|
53
|
+
sanitized_value = ActiveRecord::Base.sanitize_sql(["?", v])
|
54
|
+
"#{alias_name}.#{k} = #{sanitized_value}"
|
55
|
+
else
|
56
|
+
"#{alias_name}.#{k} = NULL"
|
57
|
+
end
|
58
|
+
end.join(', ')
|
59
|
+
|
60
|
+
sanitized_id = ActiveRecord::Base.sanitize_sql(["?", id])
|
61
|
+
|
41
62
|
<<-SQL
|
42
63
|
SELECT *
|
43
64
|
FROM cypher('#{age_graph}', $$
|
44
65
|
MATCH (#{alias_name}:#{age_label})
|
45
|
-
WHERE id(#{alias_name}) = #{
|
46
|
-
SET #{
|
66
|
+
WHERE id(#{alias_name}) = #{sanitized_id}
|
67
|
+
SET #{sanitized_set_clause}
|
47
68
|
RETURN #{alias_name}
|
48
69
|
$$) as (#{age_label} agtype);
|
49
70
|
SQL
|
50
71
|
end
|
72
|
+
|
51
73
|
end
|
52
74
|
end
|
53
75
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module ApacheAge
|
2
|
+
module Entities
|
3
|
+
module Path
|
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 :label, :string
|
13
|
+
attribute :end_id, :integer
|
14
|
+
attribute :start_id, :integer
|
15
|
+
# override with a specific node type in the defining class
|
16
|
+
attribute :end_node
|
17
|
+
attribute :start_node
|
18
|
+
|
19
|
+
validates :end_node, :start_node, presence: true
|
20
|
+
validate :validate_nodes
|
21
|
+
|
22
|
+
extend ApacheAge::Entities::ClassMethods
|
23
|
+
include ApacheAge::Entities::CommonMethods
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
# query =
|
2
|
+
# Person.
|
3
|
+
# cypher('age_schema')
|
4
|
+
# .match("(a:Person), (b:Person)")
|
5
|
+
# .where("a.name = 'Node A'", "b.name = 'Node B'")
|
6
|
+
# .return("a.name", "b.name")
|
7
|
+
# .as("name_a agtype, name_b agtype")
|
8
|
+
# .execute
|
9
|
+
# def cypher(graph_name = 'age_schema')
|
10
|
+
# ApacheAge::Cypher.new(graph_name)
|
11
|
+
# self
|
12
|
+
# end
|
13
|
+
|
14
|
+
module ApacheAge
|
15
|
+
module Entities
|
16
|
+
class QueryBuilder
|
17
|
+
attr_accessor :where_clauses, :order_clause, :limit_clause, :model_class, :match_clause,
|
18
|
+
:graph_name, :return_clause, :return_names, :return_variables
|
19
|
+
|
20
|
+
def initialize(model_class, graph_name: nil)
|
21
|
+
@model_class = model_class
|
22
|
+
@where_clauses = []
|
23
|
+
@return_names = ['find']
|
24
|
+
@return_clause = 'find'
|
25
|
+
@return_variables = []
|
26
|
+
@order_clause = nil
|
27
|
+
@limit_clause = nil
|
28
|
+
@match_clause = model_class.match_clause
|
29
|
+
@graph_name = graph_name || model_class.age_graph
|
30
|
+
end
|
31
|
+
|
32
|
+
# def cypher(graph_name = 'age_schema')
|
33
|
+
# return self if graph_name.blank?
|
34
|
+
|
35
|
+
# @graph_name = graph_name
|
36
|
+
# self
|
37
|
+
# end
|
38
|
+
|
39
|
+
def match(match_string)
|
40
|
+
@match_clause = match_string
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
# TODO: need to handle string inputs too: instead of: \
|
45
|
+
# "id(find) = #{id}" & "find.name = #{name}"
|
46
|
+
# we can have: "id(find) = ?", id & "find.name = ?", name
|
47
|
+
# ActiveRecord::Base.sanitize_sql([query_string, v])
|
48
|
+
def where(attributes)
|
49
|
+
return self if attributes.blank?
|
50
|
+
|
51
|
+
@where_clauses <<
|
52
|
+
if attributes.is_a?(String)
|
53
|
+
if attributes.include?('id(') || attributes.include?('find.')
|
54
|
+
attributes
|
55
|
+
else
|
56
|
+
"find.#{attributes}"
|
57
|
+
end
|
58
|
+
else
|
59
|
+
edge_keys = [:start_id, :start_node, :end_id, :end_node]
|
60
|
+
if edge_keys.any? { |key| attributes.include?(key) }
|
61
|
+
model_class.send(:where_edge_clause, attributes)
|
62
|
+
else
|
63
|
+
model_class.send(:where_node_clause, attributes)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
# New return method
|
71
|
+
def return(*variables)
|
72
|
+
return self if variables.blank?
|
73
|
+
|
74
|
+
@return_variables = variables
|
75
|
+
# @return_names = variables.empty? ? ['find'] : variables
|
76
|
+
# @return_clause = variables.empty? ? 'find' : "find.#{variables.join(', find.')}"
|
77
|
+
self
|
78
|
+
end
|
79
|
+
|
80
|
+
def order(ordering)
|
81
|
+
@order_clause = nil
|
82
|
+
return self if ordering.blank?
|
83
|
+
|
84
|
+
order_by_values = Array.wrap(ordering).map { |order| parse_ordering(order) }.join(', ')
|
85
|
+
@order_clause = "ORDER BY #{order_by_values}"
|
86
|
+
self
|
87
|
+
end
|
88
|
+
|
89
|
+
def limit(limit_value)
|
90
|
+
@limit_clause = "LIMIT #{limit_value}"
|
91
|
+
self
|
92
|
+
end
|
93
|
+
|
94
|
+
def all
|
95
|
+
cypher_sql = build_query
|
96
|
+
results = model_class.send(:execute_where, cypher_sql)
|
97
|
+
return results if return_variables.empty?
|
98
|
+
|
99
|
+
results.map(&:to_h).map { _1.slice(*return_variables) }
|
100
|
+
end
|
101
|
+
|
102
|
+
def execute
|
103
|
+
cypher_sql = build_query
|
104
|
+
model_class.send(:execute_sql, cypher_sql)
|
105
|
+
end
|
106
|
+
|
107
|
+
def first
|
108
|
+
cypher_sql = build_query(limit_clause || "LIMIT 1")
|
109
|
+
model_class.send(:execute_find, cypher_sql)
|
110
|
+
end
|
111
|
+
|
112
|
+
def to_sql
|
113
|
+
build_query.strip
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
# TODO: ensure ordering keys are present in the model
|
119
|
+
def parse_ordering(ordering)
|
120
|
+
if ordering.is_a?(Hash)
|
121
|
+
ordering =
|
122
|
+
ordering
|
123
|
+
.map { |k, v| "find.#{k} #{ActiveRecord::Base.sanitize_sql_like(v.to_s)}" }
|
124
|
+
.join(', ')
|
125
|
+
elsif ordering.is_a?(Symbol)
|
126
|
+
ordering = "find.#{ordering}"
|
127
|
+
elsif ordering.is_a?(String)
|
128
|
+
ordering
|
129
|
+
elsif ordering.is_a?(Array)
|
130
|
+
ordering = ordering.map do |order|
|
131
|
+
if order.is_a?(Hash)
|
132
|
+
order
|
133
|
+
.map { |k, v| "find.#{k} #{ActiveRecord::Base.sanitize_sql_like(v.to_s)}" }
|
134
|
+
.join(', ')
|
135
|
+
elsif order.is_a?(Symbol)
|
136
|
+
"find.#{order}"
|
137
|
+
elsif order.is_a?(String)
|
138
|
+
order
|
139
|
+
else
|
140
|
+
raise ArgumentError, 'Array elements must be a string, symbol, or hash'
|
141
|
+
end
|
142
|
+
end.join(', ')
|
143
|
+
else
|
144
|
+
raise ArgumentError, 'Ordering must be a string, symbol, hash, or array'
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def build_query(_extra_clause = nil)
|
149
|
+
where_sql = where_clauses.any? ? "WHERE #{where_clauses.join(' AND ')}" : ''
|
150
|
+
order_by = order_clause.present? ? order_clause : ''
|
151
|
+
<<-SQL.squish
|
152
|
+
SELECT *
|
153
|
+
FROM cypher('#{graph_name}', $$
|
154
|
+
MATCH #{match_clause}
|
155
|
+
#{where_sql}
|
156
|
+
RETURN #{return_clause}
|
157
|
+
#{order_clause}
|
158
|
+
#{limit_clause}
|
159
|
+
$$) AS (#{return_names.join(' agtype, ')} agtype);
|
160
|
+
SQL
|
161
|
+
end
|
162
|
+
# def build_query(_extra_clause = nil)
|
163
|
+
# sanitized_where_sql = where_clauses.any? ? "WHERE #{where_clauses.map { |clause| ActiveRecord::Base.sanitize_sql_like(clause) }.join(' AND ')}" : ''
|
164
|
+
# sanitized_order_by = order_clause.present? ? ActiveRecord::Base.sanitize_sql_like(order_clause) : ''
|
165
|
+
# sanitized_limit_clause = limit_clause.present? ? ActiveRecord::Base.sanitize_sql_like(limit_clause) : ''
|
166
|
+
|
167
|
+
# <<-SQL.squish
|
168
|
+
# SELECT *
|
169
|
+
# FROM cypher('#{graph_name}', $$
|
170
|
+
# MATCH #{ActiveRecord::Base.sanitize_sql_like(match_clause)}
|
171
|
+
# #{sanitized_where_sql}
|
172
|
+
# RETURN #{ActiveRecord::Base.sanitize_sql_like(return_clause)}
|
173
|
+
# #{sanitized_order_by}
|
174
|
+
# #{sanitized_limit_clause}
|
175
|
+
# $$) AS (#{return_names.map { |name| "#{ActiveRecord::Base.sanitize_sql_like(name)} agtype" }.join(', ')});
|
176
|
+
# SQL
|
177
|
+
# end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
data/lib/rails_age/version.rb
CHANGED
data/lib/rails_age.rb
CHANGED
@@ -6,13 +6,17 @@ module RailsAge
|
|
6
6
|
end
|
7
7
|
|
8
8
|
module ApacheAge
|
9
|
+
require 'apache_age/cypher.rb'
|
10
|
+
require 'apache_age/entities/query_builder'
|
9
11
|
require 'apache_age/entities/class_methods'
|
10
12
|
require 'apache_age/entities/common_methods'
|
11
13
|
require 'apache_age/entities/entity'
|
12
14
|
require 'apache_age/entities/node'
|
13
15
|
require 'apache_age/entities/edge'
|
16
|
+
# require 'apache_age/entities/path'
|
14
17
|
require 'apache_age/node'
|
15
18
|
require 'apache_age/edge'
|
19
|
+
# require 'apache_age/path'
|
16
20
|
require 'apache_age/validators/expected_node_type'
|
17
21
|
require 'apache_age/validators/unique_node'
|
18
22
|
require 'apache_age/validators/unique_edge'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails_age
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.6.
|
4
|
+
version: 0.6.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bill Tihen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-09-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -30,6 +30,20 @@ dependencies:
|
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
32
|
version: '9.0'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: ostruct
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
33
47
|
- !ruby/object:Gem::Dependency
|
34
48
|
name: rspec-rails
|
35
49
|
requirement: !ruby/object:Gem::Requirement
|
@@ -96,14 +110,17 @@ files:
|
|
96
110
|
- config/routes.rb
|
97
111
|
- db/migrate/20240521062349_add_apache_age.rb
|
98
112
|
- db/schema.rb
|
113
|
+
- lib/apache_age/cypher.rb
|
99
114
|
- lib/apache_age/edge.rb
|
100
115
|
- lib/apache_age/entities/class_methods.rb
|
101
116
|
- lib/apache_age/entities/common_methods.rb
|
102
117
|
- lib/apache_age/entities/edge.rb
|
103
118
|
- lib/apache_age/entities/entity.rb
|
104
119
|
- lib/apache_age/entities/node.rb
|
105
|
-
- lib/apache_age/entities/
|
120
|
+
- lib/apache_age/entities/path.rb
|
121
|
+
- lib/apache_age/entities/query_builder.rb
|
106
122
|
- lib/apache_age/node.rb
|
123
|
+
- lib/apache_age/path.rb
|
107
124
|
- lib/apache_age/types/factory.rb
|
108
125
|
- lib/apache_age/validators/expected_node_type.rb
|
109
126
|
- lib/apache_age/validators/node_type_validator.rb
|
@@ -162,14 +179,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
162
179
|
requirements:
|
163
180
|
- - ">="
|
164
181
|
- !ruby/object:Gem::Version
|
165
|
-
version: '3.
|
182
|
+
version: '3.2'
|
166
183
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
167
184
|
requirements:
|
168
185
|
- - ">="
|
169
186
|
- !ruby/object:Gem::Version
|
170
187
|
version: '0'
|
171
188
|
requirements: []
|
172
|
-
rubygems_version: 3.5.
|
189
|
+
rubygems_version: 3.5.18
|
173
190
|
signing_key:
|
174
191
|
specification_version: 4
|
175
192
|
summary: Apache AGE plugin for Rails 7.x
|
@@ -1,53 +0,0 @@
|
|
1
|
-
# module ApacheAge
|
2
|
-
# module Entities
|
3
|
-
# module Vertex
|
4
|
-
# extend ActiveSupport::Concern
|
5
|
-
|
6
|
-
# included do
|
7
|
-
# include ActiveModel::Model
|
8
|
-
# include ActiveModel::Dirty
|
9
|
-
# include ActiveModel::Attributes
|
10
|
-
|
11
|
-
# attribute :id, :integer
|
12
|
-
|
13
|
-
# extend ApacheAge::Entities::ClassMethods
|
14
|
-
# include ApacheAge::Entities::CommonMethods
|
15
|
-
# end
|
16
|
-
|
17
|
-
# def age_type = 'vertex'
|
18
|
-
|
19
|
-
# # AgeSchema::Nodes::Company.create(company_name: 'Bedrock Quarry')
|
20
|
-
# # SELECT *
|
21
|
-
# # FROM cypher('age_schema', $$
|
22
|
-
# # CREATE (company:Company {company_name: 'Bedrock Quarry'})
|
23
|
-
# # RETURN company
|
24
|
-
# # $$) as (Company agtype);
|
25
|
-
# def create_sql
|
26
|
-
# alias_name = age_alias || age_label.downcase
|
27
|
-
# <<-SQL
|
28
|
-
# SELECT *
|
29
|
-
# FROM cypher('#{age_graph}', $$
|
30
|
-
# CREATE (#{alias_name}#{self})
|
31
|
-
# RETURN #{alias_name}
|
32
|
-
# $$) as (#{age_label} agtype);
|
33
|
-
# SQL
|
34
|
-
# end
|
35
|
-
|
36
|
-
# # So far just properties of string type with '' around them
|
37
|
-
# def update_sql
|
38
|
-
# alias_name = age_alias || age_label.downcase
|
39
|
-
# set_caluse =
|
40
|
-
# age_properties.map { |k, v| v ? "#{alias_name}.#{k} = '#{v}'" : "#{alias_name}.#{k} = NULL" }.join(', ')
|
41
|
-
# <<-SQL
|
42
|
-
# SELECT *
|
43
|
-
# FROM cypher('#{age_graph}', $$
|
44
|
-
# MATCH (#{alias_name}:#{age_label})
|
45
|
-
# WHERE id(#{alias_name}) = #{id}
|
46
|
-
# SET #{set_caluse}
|
47
|
-
# RETURN #{alias_name}
|
48
|
-
# $$) as (#{age_label} agtype);
|
49
|
-
# SQL
|
50
|
-
# end
|
51
|
-
# end
|
52
|
-
# end
|
53
|
-
# end
|