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
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cyrel
|
4
|
+
module AST
|
5
|
+
# Immutable AST node for literal values using Ruby's Data class
|
6
|
+
# Because sometimes you just want to say what you mean, immutably
|
7
|
+
LiteralNode = Data.define(:value) do
|
8
|
+
def accept(visitor)
|
9
|
+
visitor.visit_literal_node(self)
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_ast
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
# Pattern matching support
|
17
|
+
def deconstruct
|
18
|
+
[value]
|
19
|
+
end
|
20
|
+
|
21
|
+
def deconstruct_keys(_keys)
|
22
|
+
{ value: value }
|
23
|
+
end
|
24
|
+
|
25
|
+
# Type inference
|
26
|
+
def inferred_type
|
27
|
+
case value
|
28
|
+
when String then :string
|
29
|
+
when Symbol then :parameter
|
30
|
+
when Integer then :integer
|
31
|
+
when Float then :float
|
32
|
+
when TrueClass, FalseClass then :boolean
|
33
|
+
when NilClass then :null
|
34
|
+
else :unknown
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cyrel
|
4
|
+
module AST
|
5
|
+
# AST node for LOAD CSV clause (extension)
|
6
|
+
# For when you need to import data from the ancient format of CSV
|
7
|
+
class LoadCsvNode < ClauseNode
|
8
|
+
attr_reader :url, :variable, :with_headers, :fieldterminator
|
9
|
+
|
10
|
+
def initialize(url, variable, with_headers: false, fieldterminator: nil)
|
11
|
+
@url = url
|
12
|
+
@variable = variable
|
13
|
+
@with_headers = with_headers
|
14
|
+
@fieldterminator = fieldterminator
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def state
|
20
|
+
[@url, @variable, @with_headers, @fieldterminator]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cyrel
|
4
|
+
module AST
|
5
|
+
# AST node for MATCH and OPTIONAL MATCH clauses
|
6
|
+
# Because finding things in graphs is what we're all about
|
7
|
+
class MatchNode < ClauseNode
|
8
|
+
attr_reader :pattern, :optional, :path_variable
|
9
|
+
|
10
|
+
def initialize(pattern, optional: false, path_variable: nil)
|
11
|
+
@pattern = pattern
|
12
|
+
@optional = optional
|
13
|
+
@path_variable = path_variable
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def state
|
19
|
+
[@pattern, @optional, @path_variable]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cyrel
|
4
|
+
module AST
|
5
|
+
# AST node for MERGE clauses
|
6
|
+
# For when you want to find or create, the Schrödinger's cat of graph operations
|
7
|
+
class MergeNode < ClauseNode
|
8
|
+
attr_reader :pattern, :on_create, :on_match
|
9
|
+
|
10
|
+
def initialize(pattern, on_create: nil, on_match: nil)
|
11
|
+
@pattern = pattern
|
12
|
+
@on_create = on_create
|
13
|
+
@on_match = on_match
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def state
|
19
|
+
[@pattern, @on_create, @on_match]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cyrel
|
4
|
+
module AST
|
5
|
+
# Base class for all AST nodes
|
6
|
+
# Because every tree needs roots, even if they're just pretending to be organized
|
7
|
+
class Node
|
8
|
+
# Accept a visitor for the visitor pattern
|
9
|
+
# It's like accepting guests, but these guests judge your entire structure
|
10
|
+
def accept(visitor)
|
11
|
+
method_name = "visit_#{self.class.name.demodulize.underscore}"
|
12
|
+
if visitor.respond_to?(method_name)
|
13
|
+
visitor.send(method_name, self)
|
14
|
+
else
|
15
|
+
raise NotImplementedError,
|
16
|
+
"Visitor #{visitor.class} doesn't know how to visit #{self.class}. " \
|
17
|
+
"Did you forget to implement #{method_name}?"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Equality for testing and debugging
|
22
|
+
# Because sometimes you need to know if two trees fell in the same forest
|
23
|
+
def ==(other)
|
24
|
+
self.class == other.class && state == other.state
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
# Override in subclasses to define equality state
|
30
|
+
# The essence of what makes this node unique, like a fingerprint but less criminal
|
31
|
+
def state
|
32
|
+
instance_variables.map { |var| instance_variable_get(var) }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cyrel
|
4
|
+
module AST
|
5
|
+
# Optimized nodes using Data for frequently used, simple nodes
|
6
|
+
# These benefit from Data's automatic hash/== and immutability
|
7
|
+
|
8
|
+
# For simple value nodes, Data is perfect
|
9
|
+
module OptimizedNodes
|
10
|
+
# Literal values - these are created frequently and benefit from Data
|
11
|
+
LiteralData = Data.define(:value) do
|
12
|
+
def accept(visitor)
|
13
|
+
visitor.visit_literal_data(self)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Simple expression nodes that benefit from fast equality
|
18
|
+
PropertyAccessData = Data.define(:variable, :property_name) do
|
19
|
+
def accept(visitor)
|
20
|
+
visitor.visit_property_access_data(self)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Skip and Limit are perfect for Data - simple, immutable
|
25
|
+
SkipData = Data.define(:expression) do
|
26
|
+
def accept(visitor)
|
27
|
+
visitor.visit_skip_data(self)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
LimitData = Data.define(:expression) do
|
32
|
+
def accept(visitor)
|
33
|
+
visitor.visit_limit_data(self)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Optimized cache that takes advantage of Data's hash/==
|
39
|
+
class OptimizedCache
|
40
|
+
include Singleton
|
41
|
+
|
42
|
+
def initialize
|
43
|
+
@cache = {}
|
44
|
+
@max_size = 1000
|
45
|
+
@mutex = Mutex.new
|
46
|
+
end
|
47
|
+
|
48
|
+
def fetch(node)
|
49
|
+
# Data objects have reliable hash/== so we can use them directly as keys
|
50
|
+
@mutex.synchronize do
|
51
|
+
if @cache.key?(node)
|
52
|
+
@cache[node]
|
53
|
+
else
|
54
|
+
value = yield
|
55
|
+
store(node, value)
|
56
|
+
value
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def store(node, value)
|
64
|
+
if @cache.size >= @max_size
|
65
|
+
# LRU eviction
|
66
|
+
@cache.shift
|
67
|
+
end
|
68
|
+
@cache[node] = value
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Example of how to use Data nodes effectively
|
73
|
+
class HybridApproach
|
74
|
+
# Use Data for simple, frequently created nodes
|
75
|
+
# Use Classes for complex nodes with behavior
|
76
|
+
|
77
|
+
def self.create_literal(value)
|
78
|
+
# Literals are perfect for Data - immutable, simple
|
79
|
+
OptimizedNodes::LiteralData.new(value)
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.create_match(pattern, optional: false)
|
83
|
+
# Complex nodes with optional parameters stay as classes
|
84
|
+
MatchNode.new(pattern, optional: optional)
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.benchmark_hybrid
|
88
|
+
require 'benchmark'
|
89
|
+
|
90
|
+
n = 10_000
|
91
|
+
puts "\nHybrid Approach Benchmark:"
|
92
|
+
puts '-' * 40
|
93
|
+
|
94
|
+
Benchmark.bm(35) do |x|
|
95
|
+
x.report('Create literals (Class)') do
|
96
|
+
n.times { |i| LiteralNode.new(i) }
|
97
|
+
end
|
98
|
+
|
99
|
+
x.report('Create literals (Data)') do
|
100
|
+
n.times { |i| OptimizedNodes::LiteralData.new(i) }
|
101
|
+
end
|
102
|
+
|
103
|
+
# Cache performance with Data nodes
|
104
|
+
cache = OptimizedCache.instance
|
105
|
+
literals = 100.times.map { |i| OptimizedNodes::LiteralData.new(i) }
|
106
|
+
|
107
|
+
x.report('Cache lookups (Data as key)') do
|
108
|
+
n.times do |i|
|
109
|
+
node = literals[i % 100]
|
110
|
+
cache.fetch(node) { "value_#{i}" }
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cyrel
|
4
|
+
module AST
|
5
|
+
class OrderByNode < ClauseNode
|
6
|
+
attr_reader :items
|
7
|
+
|
8
|
+
def initialize(items)
|
9
|
+
# items is an array of [expression, direction] pairs
|
10
|
+
# e.g., [[expr1, :asc], [expr2, :desc]]
|
11
|
+
@items = items
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
def state
|
17
|
+
[@items]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cyrel
|
4
|
+
module AST
|
5
|
+
# Compiler that integrates with the Query's parameter system
|
6
|
+
# Like a diplomat that speaks both AST and Query fluently
|
7
|
+
class QueryIntegratedCompiler < Compiler
|
8
|
+
attr_reader :query
|
9
|
+
|
10
|
+
def initialize(query)
|
11
|
+
# Store current loop_variables before calling super
|
12
|
+
old_loop_variables = @loop_variables
|
13
|
+
super()
|
14
|
+
@query = query
|
15
|
+
# Restore loop_variables if they were set before initialization
|
16
|
+
@loop_variables = old_loop_variables if old_loop_variables
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
# Override to use the query's parameter registration
|
22
|
+
def register_parameter(value)
|
23
|
+
@query.register_parameter(value)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cyrel
|
4
|
+
module AST
|
5
|
+
# AST node for REMOVE clauses
|
6
|
+
# For when you need to Marie Kondo your graph properties and labels
|
7
|
+
class RemoveNode < ClauseNode
|
8
|
+
attr_reader :items
|
9
|
+
|
10
|
+
def initialize(items)
|
11
|
+
@items = items
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
def state
|
17
|
+
[@items]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cyrel
|
4
|
+
module AST
|
5
|
+
class ReturnNode < ClauseNode
|
6
|
+
attr_reader :items, :distinct
|
7
|
+
|
8
|
+
def initialize(items, distinct: false)
|
9
|
+
# items is an array of expressions/identifiers to return
|
10
|
+
@items = items
|
11
|
+
@distinct = distinct
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
def state
|
17
|
+
[@items, @distinct]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cyrel
|
4
|
+
module AST
|
5
|
+
class SetNode < ClauseNode
|
6
|
+
attr_reader :assignments
|
7
|
+
|
8
|
+
def initialize(assignments)
|
9
|
+
# assignments is an array of processed assignment tuples
|
10
|
+
@assignments = assignments
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def state
|
16
|
+
[@assignments]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
|
5
|
+
module Cyrel
|
6
|
+
module AST
|
7
|
+
# Simple thread-safe compilation cache
|
8
|
+
# Because compiling the same query twice is like watching reruns
|
9
|
+
class SimpleCache
|
10
|
+
include Singleton
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@cache = {}
|
14
|
+
@mutex = Mutex.new
|
15
|
+
@max_size = 1000
|
16
|
+
end
|
17
|
+
|
18
|
+
def fetch(key)
|
19
|
+
@mutex.synchronize do
|
20
|
+
if @cache.key?(key)
|
21
|
+
@cache[key]
|
22
|
+
elsif block_given?
|
23
|
+
value = yield
|
24
|
+
store(key, value)
|
25
|
+
value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def clear!
|
31
|
+
@mutex.synchronize { @cache.clear }
|
32
|
+
end
|
33
|
+
|
34
|
+
def size
|
35
|
+
@mutex.synchronize { @cache.size }
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def store(key, value)
|
41
|
+
# Simple LRU: remove oldest entries when cache is full
|
42
|
+
if @cache.size >= @max_size
|
43
|
+
# Remove half of the oldest entries
|
44
|
+
(@max_size / 2).times { @cache.shift }
|
45
|
+
end
|
46
|
+
@cache[key] = value
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cyrel
|
4
|
+
module AST
|
5
|
+
# AST node for UNION and UNION ALL clauses
|
6
|
+
# Because sometimes you need to combine queries like a database DJ
|
7
|
+
class UnionNode < ClauseNode
|
8
|
+
attr_reader :queries, :all
|
9
|
+
|
10
|
+
def initialize(queries, all: false)
|
11
|
+
@queries = queries
|
12
|
+
@all = all
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def state
|
18
|
+
[@queries, @all]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cyrel
|
4
|
+
module AST
|
5
|
+
class UnwindNode < ClauseNode
|
6
|
+
attr_reader :expression, :alias_name
|
7
|
+
|
8
|
+
def initialize(expression, alias_name)
|
9
|
+
@expression = expression
|
10
|
+
@alias_name = alias_name
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def state
|
16
|
+
[@expression, @alias_name]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cyrel
|
4
|
+
module AST
|
5
|
+
class WhereNode < ClauseNode
|
6
|
+
attr_reader :conditions
|
7
|
+
|
8
|
+
def initialize(conditions)
|
9
|
+
# conditions is an array of expression objects
|
10
|
+
@conditions = conditions
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def state
|
16
|
+
[@conditions]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cyrel
|
4
|
+
module AST
|
5
|
+
class WithNode < ClauseNode
|
6
|
+
attr_reader :items, :distinct, :where_conditions
|
7
|
+
|
8
|
+
def initialize(items, distinct: false, where_conditions: nil)
|
9
|
+
# items is an array of expressions/identifiers to project
|
10
|
+
@items = items
|
11
|
+
@distinct = distinct
|
12
|
+
# where_conditions can be nil or an array of expressions
|
13
|
+
@where_conditions = where_conditions
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def state
|
19
|
+
[@items, @distinct, @where_conditions]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cyrel
|
4
|
+
module Clause
|
5
|
+
# UNWIND clause for expanding lists into rows
|
6
|
+
# Like unpacking a suitcase, but for data and with less wrinkled clothes
|
7
|
+
class Unwind < Base
|
8
|
+
attr_reader :expression, :variable
|
9
|
+
|
10
|
+
def initialize(expression, variable)
|
11
|
+
@expression = expression
|
12
|
+
@variable = variable.to_sym
|
13
|
+
end
|
14
|
+
|
15
|
+
def render(query)
|
16
|
+
cypher_parts = ['UNWIND']
|
17
|
+
|
18
|
+
# Handle the expression
|
19
|
+
expr_str = case @expression
|
20
|
+
when Symbol
|
21
|
+
# It's a parameter
|
22
|
+
param_key = query.register_parameter(@expression)
|
23
|
+
"$#{param_key}"
|
24
|
+
when Array
|
25
|
+
# Literal array
|
26
|
+
"[#{@expression.map { |item| render_array_item(item, query) }.join(', ')}]"
|
27
|
+
when Expression::Base
|
28
|
+
# It's already an expression
|
29
|
+
@expression.render(query)
|
30
|
+
else
|
31
|
+
# Try to coerce it
|
32
|
+
Expression.coerce(@expression).render(query)
|
33
|
+
end
|
34
|
+
|
35
|
+
cypher_parts << expr_str
|
36
|
+
cypher_parts << 'AS'
|
37
|
+
cypher_parts << @variable.to_s
|
38
|
+
|
39
|
+
cypher_parts.join(' ')
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def render_array_item(item, query)
|
45
|
+
case item
|
46
|
+
when Array
|
47
|
+
# Nested array
|
48
|
+
"[#{item.map { |v| render_value(v, query) }.join(', ')}]"
|
49
|
+
else
|
50
|
+
render_value(item, query)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def render_value(value, query)
|
55
|
+
case value
|
56
|
+
when String
|
57
|
+
"'#{value.gsub("'", "\\\\'")}'"
|
58
|
+
when Symbol
|
59
|
+
# Symbols in arrays should be rendered as strings
|
60
|
+
"'#{value}'"
|
61
|
+
when Numeric, TrueClass, FalseClass, NilClass
|
62
|
+
value.inspect
|
63
|
+
else
|
64
|
+
# For more complex values, treat as parameter
|
65
|
+
param_key = query.register_parameter(value)
|
66
|
+
"$#{param_key}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -18,11 +18,18 @@ module Cyrel
|
|
18
18
|
# @param query [Cyrel::Query] The query object for parameter registration.
|
19
19
|
# @return [String] The parameter placeholder string (e.g., "$p1").
|
20
20
|
def render(query)
|
21
|
-
#
|
21
|
+
# nil is special - render as NULL literal, not a parameter
|
22
22
|
return 'NULL' if @value.nil?
|
23
23
|
|
24
24
|
param_key = query.register_parameter(@value)
|
25
|
-
|
25
|
+
|
26
|
+
# If the param_key is the same as the value (for loop variables),
|
27
|
+
# don't add the $ prefix - just render as identifier
|
28
|
+
if param_key == @value && @value.is_a?(Symbol)
|
29
|
+
param_key.to_s
|
30
|
+
else
|
31
|
+
"$#{param_key}"
|
32
|
+
end
|
26
33
|
end
|
27
34
|
|
28
35
|
# Override comparison methods for direct literal comparison if needed,
|
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)
|