activecypher 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/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 +35 -2
- data/lib/active_cypher/connection_adapters/neo4j_adapter.rb +32 -0
- data/lib/active_cypher/connection_adapters/persistence_methods.rb +38 -10
- 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 +2 -2
- data/lib/active_cypher/relationship.rb +4 -1
- data/lib/active_cypher/version.rb +1 -1
- data/lib/activecypher.rb +3 -1
- data/lib/cyrel/ast/call_node.rb +39 -0
- data/lib/cyrel/ast/clause_adapter.rb +38 -0
- data/lib/cyrel/ast/clause_node.rb +10 -0
- data/lib/cyrel/ast/compiler.rb +609 -0
- data/lib/cyrel/ast/create_node.rb +21 -0
- data/lib/cyrel/ast/delete_node.rb +22 -0
- data/lib/cyrel/ast/expression_node.rb +10 -0
- data/lib/cyrel/ast/foreach_node.rb +23 -0
- data/lib/cyrel/ast/limit_node.rb +21 -0
- data/lib/cyrel/ast/literal_node.rb +39 -0
- data/lib/cyrel/ast/load_csv_node.rb +24 -0
- data/lib/cyrel/ast/match_node.rb +23 -0
- data/lib/cyrel/ast/merge_node.rb +23 -0
- data/lib/cyrel/ast/node.rb +36 -0
- data/lib/cyrel/ast/optimized_nodes.rb +117 -0
- data/lib/cyrel/ast/order_by_node.rb +21 -0
- data/lib/cyrel/ast/pattern_node.rb +10 -0
- data/lib/cyrel/ast/query_integrated_compiler.rb +27 -0
- data/lib/cyrel/ast/remove_node.rb +21 -0
- data/lib/cyrel/ast/return_node.rb +21 -0
- data/lib/cyrel/ast/set_node.rb +20 -0
- data/lib/cyrel/ast/simple_cache.rb +50 -0
- data/lib/cyrel/ast/skip_node.rb +19 -0
- data/lib/cyrel/ast/union_node.rb +22 -0
- data/lib/cyrel/ast/unwind_node.rb +20 -0
- data/lib/cyrel/ast/where_node.rb +20 -0
- data/lib/cyrel/ast/with_node.rb +23 -0
- data/lib/cyrel/clause/unwind.rb +71 -0
- data/lib/cyrel/expression/literal.rb +9 -2
- data/lib/cyrel/expression/property_access.rb +1 -1
- data/lib/cyrel/functions.rb +11 -0
- data/lib/cyrel/pattern/node.rb +11 -1
- data/lib/cyrel/pattern/relationship.rb +21 -13
- data/lib/cyrel/query.rb +405 -91
- data/lib/cyrel.rb +137 -3
- metadata +29 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b73e136ad3f9de8db24567d350140d9679d04d35139d861af4b5821037cc6bdd
|
4
|
+
data.tar.gz: 6e795339764ca8e603cc80db6909a983fdcb3877fa2dfcaec3fcf20149d153b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 803b1091f2227ebe564fad414025e21790710e7e4c60d9b05113c33a1a7d020495c7ce45a876ac9650261f54b60fbca5da7c2d1c0fcdfb8b169918798080feda
|
7
|
+
data.tar.gz: e02f17df064f5ef440503bfedfa050572c5326144403e79e32188528f615afbb11c62fe75e6417b26094c4168ede2414b821681c293c5731de5da6ad2ba8c1ea
|
@@ -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
|
|
@@ -112,7 +136,16 @@ module ActiveCypher
|
|
112
136
|
labels = run('MATCH (n) RETURN DISTINCT labels(n) AS lbl').flat_map { |r| r['lbl'] }
|
113
137
|
|
114
138
|
nodes = labels.map do |lbl|
|
115
|
-
|
139
|
+
# Use Cyrel for secure query building with user-provided label
|
140
|
+
query = Cyrel::Query.new
|
141
|
+
.match(Cyrel.node(:n, lbl))
|
142
|
+
.with(:n)
|
143
|
+
.limit(100)
|
144
|
+
.unwind(Cyrel.function(:keys, :n), :k)
|
145
|
+
.return_('DISTINCT k')
|
146
|
+
|
147
|
+
cypher, params = query.to_cypher
|
148
|
+
props = run(cypher, params).map { |r| r['k'] }
|
116
149
|
Schema::NodeTypeDef.new(lbl, props, nil)
|
117
150
|
end
|
118
151
|
|
@@ -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}" }
|
@@ -14,9 +14,18 @@ module ActiveCypher
|
|
14
14
|
else
|
15
15
|
[model.class.label_name.to_s]
|
16
16
|
end
|
17
|
+
|
18
|
+
adapter = model.connection.id_handler
|
19
|
+
|
20
|
+
# OPTIMIZED: Use string template instead of Cyrel for known-safe CREATE pattern
|
21
|
+
# Labels come from model class (safe), props are parameterized (safe)
|
17
22
|
label_string = labels.map { |l| ":#{l}" }.join
|
23
|
+
cypher = if adapter.id_function == 'elementId'
|
24
|
+
"CREATE (n#{label_string} $props) RETURN elementId(n) AS internal_id"
|
25
|
+
else
|
26
|
+
"CREATE (n#{label_string} $props) RETURN id(n) AS internal_id"
|
27
|
+
end
|
18
28
|
|
19
|
-
cypher = "CREATE (n#{label_string} $props) RETURN id(n) AS internal_id"
|
20
29
|
data = model.connection.execute_cypher(cypher, { props: props }, 'Create')
|
21
30
|
|
22
31
|
return false if data.blank? || !data.first.key?(:internal_id)
|
@@ -40,11 +49,23 @@ module ActiveCypher
|
|
40
49
|
[model.class.label_name.to_s]
|
41
50
|
end
|
42
51
|
|
52
|
+
adapter = model.connection.id_handler
|
53
|
+
# Convert internal_id to its preferred existential format
|
54
|
+
# Neo4j wants strings because it's complicated, Memgraph wants integers because it's not
|
55
|
+
node_id_param = adapter.id_function == 'elementId' ? model.internal_id.to_s : model.internal_id.to_i
|
56
|
+
|
57
|
+
# OPTIMIZED: Use string template for known-safe UPDATE pattern
|
58
|
+
# Labels come from model class (safe), property names from model attributes (safe)
|
43
59
|
label_string = labels.map { |l| ":#{l}" }.join
|
44
60
|
set_clauses = changes.keys.map { |property| "n.#{property} = $#{property}" }.join(', ')
|
45
|
-
params = changes.merge(node_id: model.internal_id)
|
46
61
|
|
47
|
-
cypher =
|
62
|
+
cypher = if adapter.id_function == 'elementId'
|
63
|
+
"MATCH (n#{label_string}) WHERE elementId(n) = $node_id SET #{set_clauses} RETURN n"
|
64
|
+
else
|
65
|
+
"MATCH (n#{label_string}) WHERE id(n) = $node_id SET #{set_clauses} RETURN n"
|
66
|
+
end
|
67
|
+
|
68
|
+
params = changes.merge(node_id: node_id_param)
|
48
69
|
model.connection.execute_cypher(cypher, params, 'Update')
|
49
70
|
|
50
71
|
model.send(:changes_applied)
|
@@ -60,16 +81,23 @@ module ActiveCypher
|
|
60
81
|
else
|
61
82
|
[model.class.label_name]
|
62
83
|
end
|
84
|
+
|
85
|
+
adapter = model.connection.id_handler
|
86
|
+
# Convert internal_id to whatever format makes the database feel validated
|
87
|
+
# It's like therapy, but for graph databases
|
88
|
+
node_id_param = adapter.id_function == 'elementId' ? model.internal_id.to_s : model.internal_id.to_i
|
89
|
+
|
90
|
+
# OPTIMIZED: Use string template for known-safe DELETE pattern
|
91
|
+
# Labels come from model class (safe)
|
63
92
|
label_string = labels.map { |l| ":#{l}" }.join
|
64
93
|
|
65
|
-
cypher =
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
CYPHER
|
94
|
+
cypher = if adapter.id_function == 'elementId'
|
95
|
+
"MATCH (n#{label_string}) WHERE elementId(n) = $node_id DETACH DELETE n RETURN count(*) AS deleted"
|
96
|
+
else
|
97
|
+
"MATCH (n#{label_string}) WHERE id(n) = $node_id DETACH DELETE n RETURN count(*) AS deleted"
|
98
|
+
end
|
71
99
|
|
72
|
-
result = model.connection.execute_cypher(cypher, { node_id:
|
100
|
+
result = model.connection.execute_cypher(cypher, { node_id: node_id_param }, 'Destroy')
|
73
101
|
result.present? && result.first[:deleted].to_i.positive?
|
74
102
|
end
|
75
103
|
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
|
|
@@ -141,8 +141,8 @@ module ActiveCypher
|
|
141
141
|
end
|
142
142
|
|
143
143
|
Cyrel
|
144
|
-
.match(Cyrel.node(node_alias, labels
|
145
|
-
.return_(node_alias, Cyrel.
|
144
|
+
.match(Cyrel.node(node_alias, *labels))
|
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/activecypher.rb
CHANGED
@@ -104,8 +104,10 @@ loader.ignore("#{__dir__}/activecypher.rb")
|
|
104
104
|
loader.ignore("#{__dir__}/cyrel.rb")
|
105
105
|
loader.inflector.inflect(
|
106
106
|
'activecypher' => 'ActiveCypher',
|
107
|
-
'dsl_context' => 'DSLContext'
|
107
|
+
'dsl_context' => 'DSLContext',
|
108
|
+
'ast' => 'AST'
|
108
109
|
)
|
110
|
+
|
109
111
|
loader.push_dir("#{__dir__}/cyrel", namespace: Cyrel)
|
110
112
|
loader.setup
|
111
113
|
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cyrel
|
4
|
+
module AST
|
5
|
+
# AST node for CALL clauses (procedures)
|
6
|
+
# For when you need to call upon the powers that be
|
7
|
+
class CallNode < Node
|
8
|
+
attr_reader :procedure_name, :arguments, :yield_items
|
9
|
+
|
10
|
+
def initialize(procedure_name, arguments: [], yield_items: nil)
|
11
|
+
@procedure_name = procedure_name
|
12
|
+
@arguments = arguments
|
13
|
+
@yield_items = yield_items
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def state
|
19
|
+
[@procedure_name, @arguments, @yield_items]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# AST node for CALL subqueries
|
24
|
+
# For when you need a query within a query (queryception)
|
25
|
+
class CallSubqueryNode < Node
|
26
|
+
attr_reader :subquery
|
27
|
+
|
28
|
+
def initialize(subquery)
|
29
|
+
@subquery = subquery
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
|
34
|
+
def state
|
35
|
+
[@subquery]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cyrel
|
4
|
+
module AST
|
5
|
+
# Adapter that allows AST nodes to work with the existing clause-based system
|
6
|
+
# Now with simple caching for performance
|
7
|
+
class ClauseAdapter < Clause::Base
|
8
|
+
attr_reader :ast_node
|
9
|
+
|
10
|
+
def initialize(ast_node)
|
11
|
+
@ast_node = ast_node
|
12
|
+
@ast_node_hash = ast_node.hash
|
13
|
+
super()
|
14
|
+
end
|
15
|
+
|
16
|
+
def render(query)
|
17
|
+
# Use a simple cache key based on AST node structure
|
18
|
+
cache_key = [@ast_node_hash, @ast_node.class.name].join(':')
|
19
|
+
|
20
|
+
SimpleCache.instance.fetch(cache_key) do
|
21
|
+
# Create a compiler that delegates parameter registration to the query
|
22
|
+
compiler = QueryIntegratedCompiler.new(query)
|
23
|
+
compiler.compile(ast_node)
|
24
|
+
compiler.output.string
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Ruby 3.0+ pattern matching support
|
29
|
+
def deconstruct
|
30
|
+
[ast_node]
|
31
|
+
end
|
32
|
+
|
33
|
+
def deconstruct_keys(_keys)
|
34
|
+
{ ast_node: ast_node, type: ast_node.class }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|