activecypher 0.7.3 → 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/connection_adapters/memgraph_adapter.rb +10 -1
- data/lib/active_cypher/connection_adapters/neo4j_adapter.rb +1 -1
- data/lib/active_cypher/connection_adapters/persistence_methods.rb +31 -14
- data/lib/active_cypher/relation.rb +1 -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/pattern/node.rb +11 -1
- data/lib/cyrel/pattern/relationship.rb +21 -13
- data/lib/cyrel/query.rb +405 -91
- data/lib/cyrel.rb +132 -2
- 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
|
@@ -136,7 +136,16 @@ module ActiveCypher
|
|
136
136
|
labels = run('MATCH (n) RETURN DISTINCT labels(n) AS lbl').flat_map { |r| r['lbl'] }
|
137
137
|
|
138
138
|
nodes = labels.map do |lbl|
|
139
|
-
|
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'] }
|
140
149
|
Schema::NodeTypeDef.new(lbl, props, nil)
|
141
150
|
end
|
142
151
|
|
@@ -94,7 +94,7 @@ module ActiveCypher
|
|
94
94
|
connect
|
95
95
|
# Replace adapter-aware placeholder with Neo4j's elementId function
|
96
96
|
cypher = cypher.gsub('__NODE_ID__', 'elementId')
|
97
|
-
|
97
|
+
|
98
98
|
session = connection.session # thin wrapper around Bolt::Session
|
99
99
|
result = session.write_transaction do |tx|
|
100
100
|
logger.debug { "[#{ctx}] #{cypher} #{params.inspect}" }
|
@@ -14,10 +14,18 @@ module ActiveCypher
|
|
14
14
|
else
|
15
15
|
[model.class.label_name.to_s]
|
16
16
|
end
|
17
|
-
label_string = labels.map { |l| ":#{l}" }.join
|
18
17
|
|
19
18
|
adapter = model.connection.id_handler
|
20
|
-
|
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)
|
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
|
28
|
+
|
21
29
|
data = model.connection.execute_cypher(cypher, { props: props }, 'Create')
|
22
30
|
|
23
31
|
return false if data.blank? || !data.first.key?(:internal_id)
|
@@ -41,16 +49,23 @@ module ActiveCypher
|
|
41
49
|
[model.class.label_name.to_s]
|
42
50
|
end
|
43
51
|
|
44
|
-
label_string = labels.map { |l| ":#{l}" }.join
|
45
|
-
set_clauses = changes.keys.map { |property| "n.#{property} = $#{property}" }.join(', ')
|
46
|
-
|
47
52
|
adapter = model.connection.id_handler
|
48
53
|
# Convert internal_id to its preferred existential format
|
49
54
|
# Neo4j wants strings because it's complicated, Memgraph wants integers because it's not
|
50
55
|
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
56
|
|
53
|
-
|
57
|
+
# OPTIMIZED: Use string template for known-safe UPDATE pattern
|
58
|
+
# Labels come from model class (safe), property names from model attributes (safe)
|
59
|
+
label_string = labels.map { |l| ":#{l}" }.join
|
60
|
+
set_clauses = changes.keys.map { |property| "n.#{property} = $#{property}" }.join(', ')
|
61
|
+
|
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)
|
54
69
|
model.connection.execute_cypher(cypher, params, 'Update')
|
55
70
|
|
56
71
|
model.send(:changes_applied)
|
@@ -66,19 +81,21 @@ module ActiveCypher
|
|
66
81
|
else
|
67
82
|
[model.class.label_name]
|
68
83
|
end
|
69
|
-
label_string = labels.map { |l| ":#{l}" }.join
|
70
84
|
|
71
85
|
adapter = model.connection.id_handler
|
72
86
|
# Convert internal_id to whatever format makes the database feel validated
|
73
87
|
# It's like therapy, but for graph databases
|
74
88
|
node_id_param = adapter.id_function == 'elementId' ? model.internal_id.to_s : model.internal_id.to_i
|
75
89
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
90
|
+
# OPTIMIZED: Use string template for known-safe DELETE pattern
|
91
|
+
# Labels come from model class (safe)
|
92
|
+
label_string = labels.map { |l| ":#{l}" }.join
|
93
|
+
|
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
|
82
99
|
|
83
100
|
result = model.connection.execute_cypher(cypher, { node_id: node_id_param }, 'Destroy')
|
84
101
|
result.present? && result.first[:deleted].to_i.positive?
|
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
|