activecypher 0.7.2 → 0.7.3
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/lib/active_cypher/associations/collection_proxy.rb +2 -2
- data/lib/active_cypher/associations.rb +12 -12
- data/lib/active_cypher/connection_adapters/memgraph_adapter.rb +25 -1
- data/lib/active_cypher/connection_adapters/neo4j_adapter.rb +32 -0
- data/lib/active_cypher/connection_adapters/persistence_methods.rb +16 -5
- data/lib/active_cypher/connection_url_resolver.rb +8 -2
- data/lib/active_cypher/fixtures/node_builder.rb +3 -2
- data/lib/active_cypher/generators/templates/cypher_databases.yml +3 -3
- data/lib/active_cypher/model/core.rb +5 -1
- data/lib/active_cypher/model/querying.rb +13 -4
- data/lib/active_cypher/relation.rb +1 -1
- data/lib/active_cypher/relationship.rb +4 -1
- data/lib/active_cypher/version.rb +1 -1
- data/lib/cyrel/functions.rb +11 -0
- data/lib/cyrel.rb +5 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bcfe9f62e0fdcf58b7d4f46fef0b7869bac550fc730f2e93a2d057938cad8a4e
|
4
|
+
data.tar.gz: a835e25b6ff10e6e9e4884c2855091e617086e1635931988cc018d9eac06afd8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4297ab6127aa159da2800dd2024102e8768cfac6604ce390fa35d71d95b393e3226a6d544e535ccc09c14d1632a23009c48df5067a62b6f158fc344b7e1ff21c
|
7
|
+
data.tar.gz: b929133db5f0be22b7fbd2e3923e54b0a6b041e748d28dc32bbd6f454ab0547ea48637a945262b480339ccee8e78551b23b64cd6aea8a276c5f3d5e766adbc8d
|
@@ -128,9 +128,9 @@ module ActiveCypher
|
|
128
128
|
arrow = (dir == :both ? :'--' : :out)
|
129
129
|
Cyrel
|
130
130
|
.match(Cyrel.node(from_node.class.label_name).as(:a)
|
131
|
-
.where(Cyrel.
|
131
|
+
.where(Cyrel.node_id(:a).eq(from_node.internal_id)))
|
132
132
|
.match(Cyrel.node(to_node.class.label_name).as(:b)
|
133
|
-
.where(Cyrel.
|
133
|
+
.where(Cyrel.node_id(:b).eq(to_node.internal_id)))
|
134
134
|
.create(Cyrel.node(:a).rel(arrow, reflection[:relationship]).to(:b))
|
135
135
|
.tap { |qry| owner.class.connection.execute_cypher(*qry.to_cypher, 'Create Association') }
|
136
136
|
end
|
@@ -142,7 +142,7 @@ module ActiveCypher
|
|
142
142
|
# Compose query MATCH – WHERE – RETURN
|
143
143
|
query = Cyrel::Query.new
|
144
144
|
.match(path)
|
145
|
-
.where(Cyrel.
|
145
|
+
.where(Cyrel.node_id(a_alias).eq(internal_id))
|
146
146
|
.return_(b_alias)
|
147
147
|
|
148
148
|
base_relation = Relation.new(target_class, query)
|
@@ -186,7 +186,7 @@ module ActiveCypher
|
|
186
186
|
|
187
187
|
query = Cyrel::Query.new
|
188
188
|
.match(path)
|
189
|
-
.where(Cyrel.
|
189
|
+
.where(Cyrel.node_id(a_alias).eq(internal_id))
|
190
190
|
.return_(b_alias)
|
191
191
|
.limit(1)
|
192
192
|
|
@@ -225,8 +225,8 @@ module ActiveCypher
|
|
225
225
|
.rel(cyrel_direction, rel_type)
|
226
226
|
.as(del_rel_alias)
|
227
227
|
.to(del_end_alias))
|
228
|
-
.where(Cyrel.
|
229
|
-
.where(Cyrel.
|
228
|
+
.where(Cyrel.node_id(del_start_alias).eq(del_start_node.internal_id))
|
229
|
+
.where(Cyrel.node_id(del_end_alias).eq(del_end_node.internal_id))
|
230
230
|
.delete(del_rel_alias)
|
231
231
|
|
232
232
|
self.class.connection.execute_cypher(
|
@@ -266,8 +266,8 @@ module ActiveCypher
|
|
266
266
|
create_query = Cyrel
|
267
267
|
.match(Cyrel.node(new_start_node.class.label_name).as(new_start_alias))
|
268
268
|
.match(Cyrel.node(new_end_node.class.label_name).as(new_end_alias))
|
269
|
-
.where(Cyrel.
|
270
|
-
.where(Cyrel.
|
269
|
+
.where(Cyrel.node_id(new_start_alias).eq(new_start_node.internal_id))
|
270
|
+
.where(Cyrel.node_id(new_end_alias).eq(new_end_node.internal_id))
|
271
271
|
.create(Cyrel.node(new_start_alias)
|
272
272
|
.rel(arrow, rel_type)
|
273
273
|
.to(new_end_alias))
|
@@ -322,7 +322,7 @@ module ActiveCypher
|
|
322
322
|
target_node_alias = :target_node
|
323
323
|
|
324
324
|
start_node_pattern = Cyrel.node(self.class.label_name).as(start_node_alias)
|
325
|
-
.where(Cyrel.
|
325
|
+
.where(Cyrel.node_id(start_node_alias).eq(internal_id))
|
326
326
|
target_node_pattern = Cyrel.node(target_class.label_name).as(target_node_alias)
|
327
327
|
|
328
328
|
rel_pattern = case direction
|
@@ -372,10 +372,10 @@ module ActiveCypher
|
|
372
372
|
# Adjust direction for Cyrel pattern if needed
|
373
373
|
cyrel_direction = direction == :in ? :out : direction
|
374
374
|
del_query = Cyrel.match(Cyrel.node(del_start_node.class.label_name)
|
375
|
-
.as(del_start_alias).where(Cyrel.
|
375
|
+
.as(del_start_alias).where(Cyrel.node_id(del_start_alias)
|
376
376
|
.eq(del_start_node.internal_id)))
|
377
377
|
.match(Cyrel.node(del_end_node.class.label_name)
|
378
|
-
.as(del_end_alias).where(Cyrel.
|
378
|
+
.as(del_end_alias).where(Cyrel.node_id(del_end_alias)
|
379
379
|
.eq(del_end_node.internal_id)))
|
380
380
|
.match(Cyrel.node(del_start_alias).rel(cyrel_direction,
|
381
381
|
rel_type).as(del_rel_alias).to(del_end_alias))
|
@@ -411,10 +411,10 @@ module ActiveCypher
|
|
411
411
|
new_start_alias = :a
|
412
412
|
new_end_alias = :b
|
413
413
|
create_query = Cyrel.match(Cyrel.node(new_start_node.class.label_name)
|
414
|
-
.as(new_start_alias).where(Cyrel.
|
414
|
+
.as(new_start_alias).where(Cyrel.node_id(new_start_alias)
|
415
415
|
.eq(new_start_node.internal_id)))
|
416
416
|
.match(Cyrel.node(new_end_node.class.label_name)
|
417
|
-
.as(new_end_alias).where(Cyrel.
|
417
|
+
.as(new_end_alias).where(Cyrel.node_id(new_end_alias)
|
418
418
|
.eq(new_end_node.internal_id)))
|
419
419
|
.create(Cyrel.node(new_start_alias).rel(:out, rel_type).to(new_end_alias))
|
420
420
|
|
@@ -488,7 +488,7 @@ module ActiveCypher
|
|
488
488
|
|
489
489
|
# Start node pattern
|
490
490
|
start_node_pattern = Cyrel.node(self.class.label_name).as(start_node_alias)
|
491
|
-
.where(Cyrel.
|
491
|
+
.where(Cyrel.node_id(start_node_alias).eq(internal_id))
|
492
492
|
|
493
493
|
# Intermediate node pattern (based on through_reflection)
|
494
494
|
intermediate_node_pattern = Cyrel.node(intermediate_class.label_name).as(intermediate_node_alias)
|
@@ -39,6 +39,27 @@ module ActiveCypher
|
|
39
39
|
'id(r) AS rid'
|
40
40
|
end
|
41
41
|
|
42
|
+
# Additional helper methods for nodes
|
43
|
+
def self.node_id_where(alias_name, param_name = nil)
|
44
|
+
if param_name
|
45
|
+
"id(#{alias_name}) = $#{param_name}"
|
46
|
+
else
|
47
|
+
"id(#{alias_name})"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.node_id_equals_value(alias_name, value)
|
52
|
+
"id(#{alias_name}) = #{value}"
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.return_node_id(alias_name, as_name = 'internal_id')
|
56
|
+
"id(#{alias_name}) AS #{as_name}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.id_function
|
60
|
+
'id'
|
61
|
+
end
|
62
|
+
|
42
63
|
# Return self as id_handler for compatibility with tests
|
43
64
|
def id_handler
|
44
65
|
self.class
|
@@ -66,7 +87,10 @@ module ActiveCypher
|
|
66
87
|
# Memgraph defaults to **implicit auto‑commit** transactions
|
67
88
|
# so we simply run the Cypher and return the rows.
|
68
89
|
def execute_cypher(cypher, params = {}, ctx = 'Query')
|
69
|
-
|
90
|
+
# Replace adapter-aware placeholder with Memgraph's id function
|
91
|
+
# Because Memgraph insists on being different and using id() instead of elementId()
|
92
|
+
cypher = cypher.gsub('__NODE_ID__', 'id')
|
93
|
+
rows = run(cypher, params, context: ctx)
|
70
94
|
process_records(rows)
|
71
95
|
end
|
72
96
|
|
@@ -61,8 +61,40 @@ module ActiveCypher
|
|
61
61
|
'elementId(r) AS rid'
|
62
62
|
end
|
63
63
|
|
64
|
+
# Additional helper methods for nodes
|
65
|
+
def self.node_id_where(alias_name, param_name = nil)
|
66
|
+
if param_name
|
67
|
+
"elementId(#{alias_name}) = $#{param_name}"
|
68
|
+
else
|
69
|
+
"elementId(#{alias_name})"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.node_id_equals_value(alias_name, value)
|
74
|
+
# Quote string values for Cypher because Neo4j is paranoid about injection attacks
|
75
|
+
# (As it should be, have you seen what people try to inject these days?)
|
76
|
+
quoted_value = value.is_a?(String) ? "'#{value}'" : value
|
77
|
+
"elementId(#{alias_name}) = #{quoted_value}"
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.return_node_id(alias_name, as_name = 'internal_id')
|
81
|
+
"elementId(#{alias_name}) AS #{as_name}"
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.id_function
|
85
|
+
'elementId'
|
86
|
+
end
|
87
|
+
|
88
|
+
# Return self as id_handler for compatibility
|
89
|
+
def id_handler
|
90
|
+
self.class
|
91
|
+
end
|
92
|
+
|
64
93
|
def execute_cypher(cypher, params = {}, ctx = 'Query')
|
65
94
|
connect
|
95
|
+
# Replace adapter-aware placeholder with Neo4j's elementId function
|
96
|
+
cypher = cypher.gsub('__NODE_ID__', 'elementId')
|
97
|
+
|
66
98
|
session = connection.session # thin wrapper around Bolt::Session
|
67
99
|
result = session.write_transaction do |tx|
|
68
100
|
logger.debug { "[#{ctx}] #{cypher} #{params.inspect}" }
|
@@ -16,7 +16,8 @@ module ActiveCypher
|
|
16
16
|
end
|
17
17
|
label_string = labels.map { |l| ":#{l}" }.join
|
18
18
|
|
19
|
-
|
19
|
+
adapter = model.connection.id_handler
|
20
|
+
cypher = "CREATE (n#{label_string} $props) RETURN #{adapter.return_node_id('n')}"
|
20
21
|
data = model.connection.execute_cypher(cypher, { props: props }, 'Create')
|
21
22
|
|
22
23
|
return false if data.blank? || !data.first.key?(:internal_id)
|
@@ -42,9 +43,14 @@ module ActiveCypher
|
|
42
43
|
|
43
44
|
label_string = labels.map { |l| ":#{l}" }.join
|
44
45
|
set_clauses = changes.keys.map { |property| "n.#{property} = $#{property}" }.join(', ')
|
45
|
-
params = changes.merge(node_id: model.internal_id)
|
46
46
|
|
47
|
-
|
47
|
+
adapter = model.connection.id_handler
|
48
|
+
# Convert internal_id to its preferred existential format
|
49
|
+
# Neo4j wants strings because it's complicated, Memgraph wants integers because it's not
|
50
|
+
node_id_param = adapter.id_function == 'elementId' ? model.internal_id.to_s : model.internal_id.to_i
|
51
|
+
params = changes.merge(node_id: node_id_param)
|
52
|
+
|
53
|
+
cypher = "MATCH (n#{label_string}) WHERE #{adapter.node_id_where('n', 'node_id')} SET #{set_clauses} RETURN n"
|
48
54
|
model.connection.execute_cypher(cypher, params, 'Update')
|
49
55
|
|
50
56
|
model.send(:changes_applied)
|
@@ -62,14 +68,19 @@ module ActiveCypher
|
|
62
68
|
end
|
63
69
|
label_string = labels.map { |l| ":#{l}" }.join
|
64
70
|
|
71
|
+
adapter = model.connection.id_handler
|
72
|
+
# Convert internal_id to whatever format makes the database feel validated
|
73
|
+
# It's like therapy, but for graph databases
|
74
|
+
node_id_param = adapter.id_function == 'elementId' ? model.internal_id.to_s : model.internal_id.to_i
|
75
|
+
|
65
76
|
cypher = <<~CYPHER
|
66
77
|
MATCH (n#{label_string})
|
67
|
-
WHERE
|
78
|
+
WHERE #{adapter.node_id_where('n', 'node_id')}
|
68
79
|
DETACH DELETE n
|
69
80
|
RETURN count(*) AS deleted
|
70
81
|
CYPHER
|
71
82
|
|
72
|
-
result = model.connection.execute_cypher(cypher, { node_id:
|
83
|
+
result = model.connection.execute_cypher(cypher, { node_id: node_id_param }, 'Destroy')
|
73
84
|
result.present? && result.first[:deleted].to_i.positive?
|
74
85
|
end
|
75
86
|
end
|
@@ -9,9 +9,11 @@ module ActiveCypher
|
|
9
9
|
#
|
10
10
|
# Supported URL prefixes:
|
11
11
|
# - neo4j://
|
12
|
+
# - neo4j+s:// (alias for neo4j+ssc://)
|
12
13
|
# - neo4j+ssl://
|
13
14
|
# - neo4j+ssc://
|
14
15
|
# - memgraph://
|
16
|
+
# - memgraph+s:// (alias for memgraph+ssc://)
|
15
17
|
# - memgraph+ssl://
|
16
18
|
# - memgraph+ssc://
|
17
19
|
#
|
@@ -138,10 +140,14 @@ module ActiveCypher
|
|
138
140
|
|
139
141
|
return nil unless SUPPORTED_ADAPTERS.include?(adapter)
|
140
142
|
|
141
|
-
|
143
|
+
# Map 's' to 'ssc' for Neo4j compatibility (self-signed certificates)
|
144
|
+
mapped_parts = parts.map { |mod| mod == 's' ? 'ssc' : mod }
|
145
|
+
modifiers = mapped_parts.select { |mod| %w[ssl ssc].include?(mod) }
|
142
146
|
|
143
147
|
# If there are parts that are neither the adapter nor valid modifiers, the URL is invalid
|
144
|
-
|
148
|
+
# Check against original parts but also accept 's' as valid
|
149
|
+
valid_modifiers = %w[ssl ssc s]
|
150
|
+
remaining_parts = parts.reject { |part| valid_modifiers.include?(part) }
|
145
151
|
return nil if remaining_parts.any?
|
146
152
|
|
147
153
|
[adapter, modifiers]
|
@@ -17,10 +17,11 @@ module ActiveCypher
|
|
17
17
|
label_clause = labels.map { |label| "`#{label}`" }.join(':')
|
18
18
|
|
19
19
|
# Build and fire the CREATE query.
|
20
|
-
#
|
20
|
+
# Ask the adapter how it likes its IDs served - string or integer, sir?
|
21
|
+
adapter = conn.id_handler
|
21
22
|
cypher = <<~CYPHER
|
22
23
|
CREATE (n:#{label_clause} $props)
|
23
|
-
RETURN n,
|
24
|
+
RETURN n, #{adapter.return_node_id('n')}, properties(n) AS props
|
24
25
|
CYPHER
|
25
26
|
|
26
27
|
result = conn.execute_cypher(cypher, props: props)
|
@@ -12,7 +12,11 @@ module ActiveCypher
|
|
12
12
|
include ActiveCypher::Associations
|
13
13
|
include ActiveCypher::Scoping
|
14
14
|
|
15
|
-
|
15
|
+
# internal_id: The ID that proves you exist in the graph's eyes
|
16
|
+
# String type because Neo4j needs UUIDs like "4:abc:xyz" to feel special
|
17
|
+
# while Memgraph just counts sheep like a normal database.
|
18
|
+
# ActiveModel will convert between them, hopefully without existential dread
|
19
|
+
attribute :internal_id, :string
|
16
20
|
|
17
21
|
class_attribute :configurations, instance_accessor: false,
|
18
22
|
default: ActiveSupport::HashWithIndifferentAccess.new
|
@@ -33,16 +33,25 @@ module ActiveCypher
|
|
33
33
|
# Find a node by internal DB ID. Returns the record or dies dramatically.
|
34
34
|
# Because sometimes you want to find a node, and sometimes you want to find existential dread.
|
35
35
|
def find(internal_db_id)
|
36
|
-
internal_db_id = internal_db_id.to_i if internal_db_id.respond_to?(:to_i)
|
37
36
|
node_alias = :n
|
38
37
|
|
39
38
|
labels = respond_to?(:labels) ? self.labels : [label_name]
|
40
|
-
|
39
|
+
adapter = connection.id_handler
|
41
40
|
label_string = labels.map { |l| ":#{l}" }.join
|
41
|
+
|
42
|
+
# Handle ID format based on adapter's preferred flavor of existential crisis
|
43
|
+
# Neo4j insists on string IDs like "4:uuid:wtf" because simple integers are for peasants
|
44
|
+
# Memgraph keeps it real with numeric IDs because it doesn't need to prove anything
|
45
|
+
formatted_id = if adapter.id_function == 'elementId'
|
46
|
+
internal_db_id.to_s # String for Neo4j
|
47
|
+
else
|
48
|
+
internal_db_id.to_i # Numeric for Memgraph
|
49
|
+
end
|
50
|
+
|
42
51
|
cypher = <<~CYPHER
|
43
52
|
MATCH (#{node_alias}#{label_string})
|
44
|
-
WHERE
|
45
|
-
RETURN #{node_alias},
|
53
|
+
WHERE #{adapter.node_id_equals_value(node_alias, formatted_id)}
|
54
|
+
RETURN #{node_alias}, #{adapter.return_node_id(node_alias)}
|
46
55
|
LIMIT 1
|
47
56
|
CYPHER
|
48
57
|
|
@@ -142,7 +142,7 @@ module ActiveCypher
|
|
142
142
|
|
143
143
|
Cyrel
|
144
144
|
.match(Cyrel.node(node_alias, labels: labels))
|
145
|
-
.return_(node_alias, Cyrel.
|
145
|
+
.return_(node_alias, Cyrel.node_id(node_alias).as(:internal_id))
|
146
146
|
end
|
147
147
|
|
148
148
|
# Actually loads the records from the database, shattering the illusion of laziness.
|
@@ -50,7 +50,10 @@ module ActiveCypher
|
|
50
50
|
# --------------------------------------------------------------
|
51
51
|
# Attributes
|
52
52
|
# --------------------------------------------------------------
|
53
|
-
|
53
|
+
# internal_id: Your relationship's social security number, but less secure
|
54
|
+
# String because Neo4j relationships have commitment issues and need complex IDs
|
55
|
+
# Memgraph relationships just want a simple number, like the good old days with MS Access.
|
56
|
+
attribute :internal_id, :string
|
54
57
|
|
55
58
|
# --------------------------------------------------------------
|
56
59
|
# Connection fallback
|
data/lib/cyrel/functions.rb
CHANGED
@@ -18,14 +18,25 @@ module Cyrel
|
|
18
18
|
# --- Common Cypher Functions ---
|
19
19
|
|
20
20
|
# Use elementId() instead of deprecated id()
|
21
|
+
# @deprecated Use {#node_id} instead for adapter-aware ID handling
|
21
22
|
def element_id(node_variable)
|
22
23
|
Expression::FunctionCall.new(:elementId, Clause::Return::RawIdentifier.new(node_variable.to_s))
|
23
24
|
end
|
24
25
|
|
26
|
+
# @deprecated Use {#node_id} instead for adapter-aware ID handling
|
25
27
|
def id(node_variable)
|
26
28
|
Expression::FunctionCall.new(:id, Clause::Return::RawIdentifier.new(node_variable.to_s))
|
27
29
|
end
|
28
30
|
|
31
|
+
# Adapter-aware node ID function
|
32
|
+
# Generates a placeholder that will be replaced by the correct ID function
|
33
|
+
# at execution time based on the database adapter (Neo4j vs Memgraph)
|
34
|
+
# Because databases can't agree on how to name their ID functions,
|
35
|
+
# we'll just pretend they're all the same and fix it later
|
36
|
+
def node_id(node_variable)
|
37
|
+
Expression::FunctionCall.new(:__NODE_ID__, Clause::Return::RawIdentifier.new(node_variable.to_s))
|
38
|
+
end
|
39
|
+
|
29
40
|
# Because apparently, COUNT(*) isn’t obvious enough.
|
30
41
|
# Handles the 'give me everything and make it snappy' use case.
|
31
42
|
def count(expression, distinct: false)
|
data/lib/cyrel.rb
CHANGED
@@ -35,11 +35,15 @@ module Cyrel
|
|
35
35
|
|
36
36
|
# Cyrel DSL helper: returns the element id of a node/relationship.
|
37
37
|
# Example: Cyrel.id(:n)
|
38
|
-
def id(...) = Functions.
|
38
|
+
def id(...) = Functions.id(...)
|
39
39
|
|
40
40
|
# Cyrel DSL helper: returns the element id of a node/relationship (alias).
|
41
41
|
def element_id(...) = Functions.element_id(...)
|
42
42
|
|
43
|
+
# Cyrel DSL helper: adapter-aware node ID function
|
44
|
+
# Example: Cyrel.node_id(:n)
|
45
|
+
def node_id(...) = Functions.node_id(...)
|
46
|
+
|
43
47
|
# Cyrel DSL helper: Cypher count() aggregation.
|
44
48
|
# Example: Cyrel.count(:n)
|
45
49
|
def count(...) = Functions.count(...)
|