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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 96c24fcfa519e4e44a2e8c8d1e84ae0b975d0d73d25492b025f4968e148a2090
4
- data.tar.gz: f9ef843ec7859048e7284731122be13ec519f9d44066471db70db1d4d326198f
3
+ metadata.gz: 614049312045dc138ca5e6aa6950333aa16936ed7f271c0dde0939971b8a4d9d
4
+ data.tar.gz: 5fd731b9d85a3b45ceb5a27fb08b86904fd4adebe95aeb928cff7da3a4f556b5
5
5
  SHA512:
6
- metadata.gz: 0afa400aeab7bfc05a7842cf4361f064b052ec52c8d2fc402da767da882277e8ad1b2c14a2b3c6396c3d34cc436caa45cb43978727d4cd02da80e589af197b10
7
- data.tar.gz: e76af4465bdfb80f6b388137e849b2b2705e1f9fd8ae5afd81369f19d40c144819cc5eba6f106677a37932b7a0bbab864951fef2e5fb2450aca982e81971065b
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
- raise ActiveCypher::ConnectionError, "Server at #{config[:uri]} is not Memgraph" unless connection.server_agent.to_s.include?('Memgraph')
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
- def create_node_index(label, *props, unique: false, if_not_exists: true, name: nil)
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
- # Memgraph syntax: CREATE INDEX ON :Label(prop)
34
- props.map { |p| "CREATE INDEX ON :#{label}(#{p})" }
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
- def create_rel_index(rel_type, *props, if_not_exists: true, name: nil)
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
- # Memgraph syntax: CREATE EDGE INDEX ON :REL_TYPE(prop)
52
- props.map { |p| "CREATE EDGE INDEX ON :#{rel_type}(#{p})" }
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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveCypher
4
- VERSION = '0.13.0'
4
+ VERSION = '0.14.0'
5
5
 
6
6
  def self.gem_version
7
7
  Gem::Version.new VERSION
@@ -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
@@ -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
- base << ':' << labels.join(':') unless labels.empty?
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
- def n(alias_name = nil, *labels, **properties)
15
- Pattern::Node.new(alias_name, labels: labels, properties: properties)
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
- def node(alias_name = nil, *labels, **properties)
33
- Pattern::Node.new(alias_name, labels: labels, properties: properties)
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.13.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.0
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.0
53
+ version: 0.11.1
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: io-endpoint
56
56
  requirement: !ruby/object:Gem::Requirement