activecypher 0.13.0 → 0.14.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 +6 -1
- data/lib/active_cypher/migration.rb +123 -6
- data/lib/active_cypher/version.rb +1 -1
- data/lib/cyrel/expression/exists.rb +33 -0
- data/lib/cyrel/pattern/node.rb +12 -2
- data/lib/cyrel.rb +18 -6
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 614049312045dc138ca5e6aa6950333aa16936ed7f271c0dde0939971b8a4d9d
|
|
4
|
+
data.tar.gz: 5fd731b9d85a3b45ceb5a27fb08b86904fd4adebe95aeb928cff7da3a4f556b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: df907144cb86d4ba2bc10e135a71e38979a91faeaa01b1ed231841626c499f19564dbaf327df5445d7338dd7849b09196fafd42155a20e8abe51397d839c3720
|
|
7
|
+
data.tar.gz: c4879e2edc165ea074510a759878ae99a95d12e6f74b7d4f21a0846bfcf6eedbbfe08c9ba213211ca748dbcde3e7fc2f4d8720857452376edba5c225a90e32c9
|
|
@@ -213,8 +213,13 @@ module ActiveCypher
|
|
|
213
213
|
|
|
214
214
|
def protocol_handler_class = ProtocolHandler
|
|
215
215
|
|
|
216
|
+
# Memgraph 3.7+ is expected. Earlier versions are untested and may not work.
|
|
217
|
+
# See: https://memgraph.com/docs for version-specific features.
|
|
218
|
+
MINIMUM_VERSION = '3.7'
|
|
219
|
+
|
|
216
220
|
def validate_connection
|
|
217
|
-
|
|
221
|
+
agent = connection.server_agent.to_s
|
|
222
|
+
raise ActiveCypher::ConnectionError, "Server at #{config[:uri]} is not Memgraph" unless agent.include?('Memgraph')
|
|
218
223
|
|
|
219
224
|
true
|
|
220
225
|
end
|
|
@@ -28,10 +28,26 @@ module ActiveCypher
|
|
|
28
28
|
|
|
29
29
|
# DSL ---------------------------------------------------------------
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
# Create a node property index.
|
|
32
|
+
# @param label [Symbol, String] Node label
|
|
33
|
+
# @param props [Array<Symbol>] Properties to index
|
|
34
|
+
# @param unique [Boolean] Create unique index (Neo4j only)
|
|
35
|
+
# @param if_not_exists [Boolean] Add IF NOT EXISTS clause (Neo4j only)
|
|
36
|
+
# @param name [String] Index name (Neo4j only)
|
|
37
|
+
# @param composite [Boolean] Create composite index (Memgraph 3.2+). Default true for multiple props.
|
|
38
|
+
def create_node_index(label, *props, unique: false, if_not_exists: true, name: nil, composite: nil)
|
|
39
|
+
# Default composite to true when multiple properties provided
|
|
40
|
+
composite = props.size > 1 if composite.nil?
|
|
41
|
+
|
|
32
42
|
cypher = if connection.vendor == :memgraph
|
|
33
|
-
|
|
34
|
-
|
|
43
|
+
if composite && props.size > 1
|
|
44
|
+
# Memgraph 3.2+ composite index: CREATE INDEX ON :Label(prop1, prop2)
|
|
45
|
+
props_list = props.join(', ')
|
|
46
|
+
["CREATE INDEX ON :#{label}(#{props_list})"]
|
|
47
|
+
else
|
|
48
|
+
# Memgraph single property indexes
|
|
49
|
+
props.map { |p| "CREATE INDEX ON :#{label}(#{p})" }
|
|
50
|
+
end
|
|
35
51
|
else
|
|
36
52
|
# Neo4j syntax
|
|
37
53
|
props_clause = props.map { |p| "n.#{p}" }.join(', ')
|
|
@@ -46,10 +62,23 @@ module ActiveCypher
|
|
|
46
62
|
operations.concat(Array(cypher))
|
|
47
63
|
end
|
|
48
64
|
|
|
49
|
-
|
|
65
|
+
# Create a relationship property index.
|
|
66
|
+
# @param rel_type [Symbol, String] Relationship type
|
|
67
|
+
# @param props [Array<Symbol>] Properties to index
|
|
68
|
+
# @param if_not_exists [Boolean] Add IF NOT EXISTS clause (Neo4j only)
|
|
69
|
+
# @param name [String] Index name (Neo4j only)
|
|
70
|
+
# @param composite [Boolean] Create composite index (Memgraph 3.2+). Default true for multiple props.
|
|
71
|
+
def create_rel_index(rel_type, *props, if_not_exists: true, name: nil, composite: nil)
|
|
72
|
+
composite = props.size > 1 if composite.nil?
|
|
73
|
+
|
|
50
74
|
cypher = if connection.vendor == :memgraph
|
|
51
|
-
|
|
52
|
-
|
|
75
|
+
if composite && props.size > 1
|
|
76
|
+
# Memgraph 3.2+ composite edge index
|
|
77
|
+
props_list = props.join(', ')
|
|
78
|
+
["CREATE EDGE INDEX ON :#{rel_type}(#{props_list})"]
|
|
79
|
+
else
|
|
80
|
+
props.map { |p| "CREATE EDGE INDEX ON :#{rel_type}(#{p})" }
|
|
81
|
+
end
|
|
53
82
|
else
|
|
54
83
|
# Neo4j syntax
|
|
55
84
|
props_clause = props.map { |p| "r.#{p}" }.join(', ')
|
|
@@ -99,6 +128,94 @@ module ActiveCypher
|
|
|
99
128
|
operations.concat(Array(cypher))
|
|
100
129
|
end
|
|
101
130
|
|
|
131
|
+
# Create a vector index (Memgraph 3.4+, Neo4j 5.0+).
|
|
132
|
+
# @param name [String] Index name
|
|
133
|
+
# @param label [Symbol, String] Node label
|
|
134
|
+
# @param property [Symbol] Property containing vector embeddings
|
|
135
|
+
# @param dimension [Integer] Vector dimension (required)
|
|
136
|
+
# @param metric [Symbol] Distance metric: :cosine, :euclidean, :dot_product (default: :cosine)
|
|
137
|
+
# @param quantization [Symbol] Quantization type for memory reduction (Memgraph 3.4+): :scalar, nil
|
|
138
|
+
def create_vector_index(name, label, property, dimension:, metric: :cosine, quantization: nil)
|
|
139
|
+
cypher = if connection.vendor == :memgraph
|
|
140
|
+
config = { dimension: dimension, metric: metric.to_s }
|
|
141
|
+
config[:scalar_kind] = 'f32' if quantization == :scalar
|
|
142
|
+
config_str = config.map { |k, v| "#{k}: #{v.is_a?(String) ? "'#{v}'" : v}" }.join(', ')
|
|
143
|
+
"CREATE VECTOR INDEX #{name} ON :#{label}(#{property}) WITH CONFIG { #{config_str} }"
|
|
144
|
+
else
|
|
145
|
+
# Neo4j syntax
|
|
146
|
+
options = { indexConfig: { 'vector.dimensions' => dimension, 'vector.similarity_function' => metric.to_s.upcase } }
|
|
147
|
+
opts_str = options.to_json.gsub('"', "'")
|
|
148
|
+
"CREATE VECTOR INDEX #{name} IF NOT EXISTS FOR (n:#{label}) ON (n.#{property}) OPTIONS #{opts_str}"
|
|
149
|
+
end
|
|
150
|
+
operations << cypher
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Create a vector index on relationships (Memgraph 3.4+, Neo4j 2025+).
|
|
154
|
+
# @param name [String] Index name
|
|
155
|
+
# @param rel_type [Symbol, String] Relationship type
|
|
156
|
+
# @param property [Symbol] Property containing vector embeddings
|
|
157
|
+
# @param dimension [Integer] Vector dimension (required)
|
|
158
|
+
# @param metric [Symbol] Distance metric: :cosine, :euclidean, :dot_product (default: :cosine)
|
|
159
|
+
def create_vector_rel_index(name, rel_type, property, dimension:, metric: :cosine)
|
|
160
|
+
cypher = if connection.vendor == :memgraph
|
|
161
|
+
config_str = "dimension: #{dimension}, metric: '#{metric}'"
|
|
162
|
+
"CREATE VECTOR EDGE INDEX #{name} ON :#{rel_type}(#{property}) WITH CONFIG { #{config_str} }"
|
|
163
|
+
else
|
|
164
|
+
# Neo4j 2025+ syntax
|
|
165
|
+
"CREATE VECTOR INDEX #{name} IF NOT EXISTS FOR ()-[r:#{rel_type}]-() ON (r.#{property}) " \
|
|
166
|
+
"OPTIONS { indexConfig: { `vector.dimensions`: #{dimension}, `vector.similarity_function`: '#{metric}' } }"
|
|
167
|
+
end
|
|
168
|
+
operations << cypher
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Alias for backwards compatibility
|
|
172
|
+
alias create_vector_edge_index create_vector_rel_index
|
|
173
|
+
|
|
174
|
+
# Create a text index on edges (Memgraph 3.6+ only).
|
|
175
|
+
# Neo4j fulltext indexes on relationships use different syntax via create_fulltext_rel_index.
|
|
176
|
+
# @param name [String] Index name
|
|
177
|
+
# @param rel_type [Symbol, String] Relationship type
|
|
178
|
+
# @param props [Array<Symbol>] Properties to index
|
|
179
|
+
def create_text_edge_index(name, rel_type, *props)
|
|
180
|
+
raise NotImplementedError, 'Text edge indexes only supported on Memgraph 3.6+' unless connection.vendor == :memgraph
|
|
181
|
+
|
|
182
|
+
props.each do |p|
|
|
183
|
+
index_name = props.size > 1 ? "#{name}_#{p}" : name.to_s
|
|
184
|
+
operations << "CREATE TEXT EDGE INDEX #{index_name} ON :#{rel_type}(#{p})"
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Create a fulltext index on relationships (Neo4j only).
|
|
189
|
+
# @param name [String] Index name
|
|
190
|
+
# @param rel_type [Symbol, String] Relationship type
|
|
191
|
+
# @param props [Array<Symbol>] Properties to index
|
|
192
|
+
# @param if_not_exists [Boolean] Add IF NOT EXISTS clause
|
|
193
|
+
def create_fulltext_rel_index(name, rel_type, *props, if_not_exists: true)
|
|
194
|
+
raise NotImplementedError, 'Fulltext relationship indexes only supported on Neo4j' unless connection.vendor == :neo4j
|
|
195
|
+
|
|
196
|
+
props_clause = props.map { |p| "r.#{p}" }.join(', ')
|
|
197
|
+
c = +"CREATE FULLTEXT INDEX #{name}"
|
|
198
|
+
c << ' IF NOT EXISTS' if if_not_exists
|
|
199
|
+
c << " FOR ()-[r:#{rel_type}]-() ON EACH [#{props_clause}]"
|
|
200
|
+
operations << c
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Drop all indexes (Memgraph 3.6+ only).
|
|
204
|
+
# Neo4j requires dropping indexes individually.
|
|
205
|
+
def drop_all_indexes
|
|
206
|
+
raise NotImplementedError, 'drop_all_indexes only supported on Memgraph 3.6+' unless connection.vendor == :memgraph
|
|
207
|
+
|
|
208
|
+
operations << 'DROP ALL INDEXES'
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Drop all constraints (Memgraph 3.6+ only).
|
|
212
|
+
# Neo4j requires dropping constraints individually.
|
|
213
|
+
def drop_all_constraints
|
|
214
|
+
raise NotImplementedError, 'drop_all_constraints only supported on Memgraph 3.6+' unless connection.vendor == :memgraph
|
|
215
|
+
|
|
216
|
+
operations << 'DROP ALL CONSTRAINTS'
|
|
217
|
+
end
|
|
218
|
+
|
|
102
219
|
def execute(cypher_string)
|
|
103
220
|
operations << cypher_string.strip
|
|
104
221
|
end
|
|
@@ -35,5 +35,38 @@ module Cyrel
|
|
|
35
35
|
"EXISTS(#{rendered_pattern})"
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
|
+
|
|
39
|
+
# Represents an EXISTS { MATCH ... WHERE ... } subquery predicate (Memgraph 3.5+).
|
|
40
|
+
# Allows full subquery syntax inside EXISTS block.
|
|
41
|
+
#
|
|
42
|
+
# @example
|
|
43
|
+
# Cyrel.exists_block { match(Cyrel.node(:a) > Cyrel.rel(:r) > Cyrel.node(:b)); where(Cyrel.prop(:b, :active) == true) }
|
|
44
|
+
# # => EXISTS { MATCH (a)-[r]->(b) WHERE b.active = $p1 }
|
|
45
|
+
class ExistsBlock < Base
|
|
46
|
+
attr_reader :subquery
|
|
47
|
+
|
|
48
|
+
# @param subquery [Cyrel::Query] A query object representing the subquery.
|
|
49
|
+
def initialize(subquery)
|
|
50
|
+
raise ArgumentError, 'ExistsBlock requires a Cyrel::Query' unless subquery.is_a?(Cyrel::Query)
|
|
51
|
+
|
|
52
|
+
@subquery = subquery
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Renders the EXISTS { ... } expression.
|
|
56
|
+
# @param query [Cyrel::Query] The parent query for parameter merging.
|
|
57
|
+
# @return [String] The Cypher string fragment.
|
|
58
|
+
def render(query)
|
|
59
|
+
inner_cypher, inner_params = @subquery.to_cypher
|
|
60
|
+
|
|
61
|
+
# Merge subquery parameters into the parent query
|
|
62
|
+
# The inner query uses its own parameter keys, we need to register values
|
|
63
|
+
# which will get new keys in the parent query
|
|
64
|
+
inner_params.each_value do |value|
|
|
65
|
+
query.register_parameter(value)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
"EXISTS { #{inner_cypher} }"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
38
71
|
end
|
|
39
72
|
end
|
data/lib/cyrel/pattern/node.rb
CHANGED
|
@@ -12,14 +12,16 @@ module Cyrel
|
|
|
12
12
|
|
|
13
13
|
attribute :alias_name, Cyrel::Types::SymbolType.new
|
|
14
14
|
attribute :labels, array: :string, default: []
|
|
15
|
+
attribute :or_labels, array: :string, default: [] # Memgraph 3.2+: (n:Label1|Label2)
|
|
15
16
|
attribute :properties, Cyrel::Types::HashType.new, default: -> { {} }
|
|
16
17
|
|
|
17
18
|
validates :alias_name, presence: true
|
|
18
19
|
|
|
19
|
-
def initialize(alias_name, labels: nil, properties: {}, **kw)
|
|
20
|
+
def initialize(alias_name, labels: nil, or_labels: nil, properties: {}, **kw)
|
|
20
21
|
super(
|
|
21
22
|
{ alias_name: alias_name,
|
|
22
23
|
labels: Array(labels).compact.flatten,
|
|
24
|
+
or_labels: Array(or_labels).compact.flatten,
|
|
23
25
|
properties: properties }.merge(kw)
|
|
24
26
|
)
|
|
25
27
|
end
|
|
@@ -38,7 +40,14 @@ module Cyrel
|
|
|
38
40
|
|
|
39
41
|
def render(query)
|
|
40
42
|
base = +"(#{alias_name}"
|
|
41
|
-
|
|
43
|
+
|
|
44
|
+
# OR labels take precedence (Memgraph 3.2+: n:Label1|Label2)
|
|
45
|
+
if or_labels.any?
|
|
46
|
+
base << ':' << or_labels.join('|')
|
|
47
|
+
elsif labels.any?
|
|
48
|
+
base << ':' << labels.join(':')
|
|
49
|
+
end
|
|
50
|
+
|
|
42
51
|
unless properties.empty?
|
|
43
52
|
params = properties.with_indifferent_access
|
|
44
53
|
formatted = params.map do |k, v|
|
|
@@ -60,6 +69,7 @@ module Cyrel
|
|
|
60
69
|
def freeze
|
|
61
70
|
super
|
|
62
71
|
labels.freeze
|
|
72
|
+
or_labels.freeze
|
|
63
73
|
properties.freeze
|
|
64
74
|
end
|
|
65
75
|
|
data/lib/cyrel.rb
CHANGED
|
@@ -11,8 +11,9 @@ module Cyrel
|
|
|
11
11
|
|
|
12
12
|
# Cyrel DSL helper: alias for node creation.
|
|
13
13
|
# Example: Cyrel.n(:person, :Person, name: 'Alice')
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
# Example: Cyrel.n(:n, or_labels: [:Person, :Organization]) # Memgraph 3.2+
|
|
15
|
+
def n(alias_name = nil, *labels, or_labels: nil, **properties)
|
|
16
|
+
Pattern::Node.new(alias_name, labels: labels, or_labels: or_labels, properties: properties)
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
# Cyrel DSL helper: creates a CALL clause for a procedure.
|
|
@@ -29,8 +30,9 @@ module Cyrel
|
|
|
29
30
|
|
|
30
31
|
# Cyrel DSL helper: creates a node pattern.
|
|
31
32
|
# Example: Cyrel.node(:n, :Person, name: 'Alice')
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
# Example: Cyrel.node(:n, or_labels: [:Person, :Organization]) # Memgraph 3.2+
|
|
34
|
+
def node(alias_name = nil, *labels, or_labels: nil, **properties)
|
|
35
|
+
Pattern::Node.new(alias_name, labels: labels, or_labels: or_labels, properties: properties)
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
# Cyrel DSL helper: creates a relationship pattern.
|
|
@@ -57,14 +59,14 @@ module Cyrel
|
|
|
57
59
|
@pending_direction = nil
|
|
58
60
|
end
|
|
59
61
|
|
|
60
|
-
def node(alias_name = nil, *labels, **properties)
|
|
62
|
+
def node(alias_name = nil, *labels, or_labels: nil, **properties)
|
|
61
63
|
# If there's a pending direction, we need to add a relationship first
|
|
62
64
|
if @pending_direction && @elements.any? && @elements.last.is_a?(Cyrel::Pattern::Node)
|
|
63
65
|
@elements << Cyrel::Pattern::Relationship.new(types: [], direction: @pending_direction)
|
|
64
66
|
@pending_direction = nil
|
|
65
67
|
end
|
|
66
68
|
|
|
67
|
-
n = Cyrel::Pattern::Node.new(alias_name, labels: labels, properties: properties)
|
|
69
|
+
n = Cyrel::Pattern::Node.new(alias_name, labels: labels, or_labels: or_labels, properties: properties)
|
|
68
70
|
@elements << n
|
|
69
71
|
self
|
|
70
72
|
end
|
|
@@ -250,6 +252,16 @@ module Cyrel
|
|
|
250
252
|
Expression.exists(pattern)
|
|
251
253
|
end
|
|
252
254
|
|
|
255
|
+
# Cyrel DSL helper: creates an EXISTS block with full subquery (Memgraph 3.5+).
|
|
256
|
+
# Example:
|
|
257
|
+
# Cyrel.exists_block { match(Cyrel.node(:a) > Cyrel.rel(:r) > Cyrel.node(:b, :Admin)) }
|
|
258
|
+
# # => EXISTS { MATCH (a)-[r]->(b:Admin) }
|
|
259
|
+
def exists_block(&block)
|
|
260
|
+
subquery = Query.new
|
|
261
|
+
subquery.instance_eval(&block)
|
|
262
|
+
Expression::ExistsBlock.new(subquery)
|
|
263
|
+
end
|
|
264
|
+
|
|
253
265
|
# Cyrel DSL helper: creates a Logical NOT expression.
|
|
254
266
|
# Example: Cyrel.not(expression)
|
|
255
267
|
def not(expression)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activecypher
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.14.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Abdelkader Boudih
|
|
@@ -43,14 +43,14 @@ dependencies:
|
|
|
43
43
|
requirements:
|
|
44
44
|
- - ">="
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: 0.11.
|
|
46
|
+
version: 0.11.1
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: 0.11.
|
|
53
|
+
version: 0.11.1
|
|
54
54
|
- !ruby/object:Gem::Dependency
|
|
55
55
|
name: io-endpoint
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|