rails_age 0.6.0 → 0.6.2
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 +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
|