Class: ActiveCypher::Migration

Inherits:
Object
  • Object
show all
Defined in:
lib/active_cypher/migration.rb

Overview

Base class for GraphDB migrations. Provides a small DSL for defining index and constraint operations.

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(connection = ActiveCypher::Base.connection) ⇒ Migration



18
19
20
21
# File 'lib/active_cypher/migration.rb', line 18

def initialize(connection = ActiveCypher::Base.connection)
  @connection = connection
  @operations = []
end

Class Attribute Details

.up_blockObject (readonly)

Returns the value of attribute up_block.



8
9
10
# File 'lib/active_cypher/migration.rb', line 8

def up_block
  @up_block
end

Instance Attribute Details

#connectionObject (readonly)

Returns the value of attribute connection.



16
17
18
# File 'lib/active_cypher/migration.rb', line 16

def connection
  @connection
end

#operationsObject (readonly)

Returns the value of attribute operations.



16
17
18
# File 'lib/active_cypher/migration.rb', line 16

def operations
  @operations
end

Class Method Details

.up(&block) ⇒ Object

Define the migration steps.



11
12
13
# File 'lib/active_cypher/migration.rb', line 11

def up(&block)
  @up_block = block if block_given?
end

Instance Method Details

#create_fulltext_index(name, label, *props, if_not_exists: true) ⇒ Object



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/active_cypher/migration.rb', line 112

def create_fulltext_index(name, label, *props, if_not_exists: true)
  cypher = if connection.vendor == :memgraph
             # Memgraph TEXT INDEX syntax (requires --experimental-enabled='text-search')
             # Memgraph only supports single property per text index, so create one per prop
             props.map.with_index do |p, i|
               index_name = props.size > 1 ? "#{name}_#{p}" : name.to_s
               "CREATE TEXT INDEX #{index_name} ON :#{label}(#{p})"
             end
           else
             # Neo4j syntax
             props_clause = props.map { |p| "n.#{p}" }.join(', ')
             c = +"CREATE FULLTEXT INDEX #{name}"
             c << ' IF NOT EXISTS' if if_not_exists
             c << " FOR (n:#{label}) ON EACH [#{props_clause}]"
             [c]
           end
  operations.concat(Array(cypher))
end

#create_fulltext_rel_index(name, rel_type, *props, if_not_exists: true) ⇒ Object

Create a fulltext index on relationships (Neo4j only).

Raises:

  • (NotImplementedError)


193
194
195
196
197
198
199
200
201
# File 'lib/active_cypher/migration.rb', line 193

def create_fulltext_rel_index(name, rel_type, *props, if_not_exists: true)
  raise NotImplementedError, 'Fulltext relationship indexes only supported on Neo4j' unless connection.vendor == :neo4j

  props_clause = props.map { |p| "r.#{p}" }.join(', ')
  c = +"CREATE FULLTEXT INDEX #{name}"
  c << ' IF NOT EXISTS' if if_not_exists
  c << " FOR ()-[r:#{rel_type}]-() ON EACH [#{props_clause}]"
  operations << c
end

#create_node_index(label, *props, unique: false, if_not_exists: true, name: nil, composite: nil) ⇒ Object

Create a node property index.



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/active_cypher/migration.rb', line 38

def create_node_index(label, *props, unique: false, if_not_exists: true, name: nil, composite: nil)
  # Default composite to true when multiple properties provided
  composite = props.size > 1 if composite.nil?

  cypher = if connection.vendor == :memgraph
             if composite && props.size > 1
               # Memgraph 3.2+ composite index: CREATE INDEX ON :Label(prop1, prop2)
               props_list = props.join(', ')
               ["CREATE INDEX ON :#{label}(#{props_list})"]
             else
               # Memgraph single property indexes
               props.map { |p| "CREATE INDEX ON :#{label}(#{p})" }
             end
           else
             # Neo4j syntax
             props_clause = props.map { |p| "n.#{p}" }.join(', ')
             c = +'CREATE '
             c << 'UNIQUE ' if unique
             c << 'INDEX'
             c << " #{name}" if name
             c << ' IF NOT EXISTS' if if_not_exists
             c << " FOR (n:#{label}) ON (#{props_clause})"
             [c]
           end
  operations.concat(Array(cypher))
end

#create_rel_index(rel_type, *props, if_not_exists: true, name: nil, composite: nil) ⇒ Object

Create a relationship property index.



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/active_cypher/migration.rb', line 71

def create_rel_index(rel_type, *props, if_not_exists: true, name: nil, composite: nil)
  composite = props.size > 1 if composite.nil?

  cypher = if connection.vendor == :memgraph
             if composite && props.size > 1
               # Memgraph 3.2+ composite edge index
               props_list = props.join(', ')
               ["CREATE EDGE INDEX ON :#{rel_type}(#{props_list})"]
             else
               props.map { |p| "CREATE EDGE INDEX ON :#{rel_type}(#{p})" }
             end
           else
             # Neo4j syntax
             props_clause = props.map { |p| "r.#{p}" }.join(', ')
             c = +'CREATE INDEX'
             c << " #{name}" if name
             c << ' IF NOT EXISTS' if if_not_exists
             c << " FOR ()-[r:#{rel_type}]-() ON (#{props_clause})"
             [c]
           end
  operations.concat(Array(cypher))
end

#create_text_edge_index(name, rel_type, *props) ⇒ Object

Create a text index on edges (Memgraph 3.6+ only). Neo4j fulltext indexes on relationships use different syntax via create_fulltext_rel_index.

Raises:

  • (NotImplementedError)


179
180
181
182
183
184
185
186
# File 'lib/active_cypher/migration.rb', line 179

def create_text_edge_index(name, rel_type, *props)
  raise NotImplementedError, 'Text edge indexes only supported on Memgraph 3.6+' unless connection.vendor == :memgraph

  props.each do |p|
    index_name = props.size > 1 ? "#{name}_#{p}" : name.to_s
    operations << "CREATE TEXT EDGE INDEX #{index_name} ON :#{rel_type}(#{p})"
  end
end

#create_uniqueness_constraint(label, *props, if_not_exists: true, name: nil) ⇒ Object



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/active_cypher/migration.rb', line 94

def create_uniqueness_constraint(label, *props, if_not_exists: true, name: nil)
  cypher = if connection.vendor == :memgraph
             # Memgraph syntax: CREATE CONSTRAINT ON (n:Label) ASSERT n.prop IS UNIQUE
             # Note: Memgraph doesn't support IF NOT EXISTS or named constraints
             props_clause = props.map { |p| "n.#{p}" }.join(', ')
             "CREATE CONSTRAINT ON (n:#{label}) ASSERT #{props_clause} IS UNIQUE"
           else
             # Neo4j syntax
             props_clause = props.map { |p| "n.#{p}" }.join(', ')
             c = +'CREATE CONSTRAINT'
             c << " #{name}" if name
             c << ' IF NOT EXISTS' if if_not_exists
             c << " FOR (n:#{label}) REQUIRE (#{props_clause}) IS UNIQUE"
             c
           end
  operations << cypher
end

#create_vector_index(name, label, property, dimension:, metric: :cosine, quantization: nil) ⇒ Object

Create a vector index (Memgraph 3.4+, Neo4j 5.0+).



138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/active_cypher/migration.rb', line 138

def create_vector_index(name, label, property, dimension:, metric: :cosine, quantization: nil)
  cypher = if connection.vendor == :memgraph
             config = { dimension: dimension, metric: metric.to_s }
             config[:scalar_kind] = 'f32' if quantization == :scalar
             config_str = config.map { |k, v| "#{k}: #{v.is_a?(String) ? "'#{v}'" : v}" }.join(', ')
             "CREATE VECTOR INDEX #{name} ON :#{label}(#{property}) WITH CONFIG { #{config_str} }"
           else
             # Neo4j syntax
             options = { indexConfig: { 'vector.dimensions' => dimension, 'vector.similarity_function' => metric.to_s.upcase } }
             opts_str = options.to_json.gsub('"', "'")
             "CREATE VECTOR INDEX #{name} IF NOT EXISTS FOR (n:#{label}) ON (n.#{property}) OPTIONS #{opts_str}"
           end
  operations << cypher
end

#create_vector_rel_index(name, rel_type, property, dimension:, metric: :cosine) ⇒ Object Also known as: create_vector_edge_index

Create a vector index on relationships (Memgraph 3.4+, Neo4j 2025+).



159
160
161
162
163
164
165
166
167
168
169
# File 'lib/active_cypher/migration.rb', line 159

def create_vector_rel_index(name, rel_type, property, dimension:, metric: :cosine)
  cypher = if connection.vendor == :memgraph
             config_str = "dimension: #{dimension}, metric: '#{metric}'"
             "CREATE VECTOR EDGE INDEX #{name} ON :#{rel_type}(#{property}) WITH CONFIG { #{config_str} }"
           else
             # Neo4j 2025+ syntax
             "CREATE VECTOR INDEX #{name} IF NOT EXISTS FOR ()-[r:#{rel_type}]-() ON (r.#{property}) " \
               "OPTIONS { indexConfig: { `vector.dimensions`: #{dimension}, `vector.similarity_function`: '#{metric}' } }"
           end
  operations << cypher
end

#drop_all_constraintsObject

Drop all constraints (Memgraph 3.6+ only). Neo4j requires dropping constraints individually.

Raises:

  • (NotImplementedError)


213
214
215
216
217
# File 'lib/active_cypher/migration.rb', line 213

def drop_all_constraints
  raise NotImplementedError, 'drop_all_constraints only supported on Memgraph 3.6+' unless connection.vendor == :memgraph

  operations << 'DROP ALL CONSTRAINTS'
end

#drop_all_indexesObject

Drop all indexes (Memgraph 3.6+ only). Neo4j requires dropping indexes individually.

Raises:

  • (NotImplementedError)


205
206
207
208
209
# File 'lib/active_cypher/migration.rb', line 205

def drop_all_indexes
  raise NotImplementedError, 'drop_all_indexes only supported on Memgraph 3.6+' unless connection.vendor == :memgraph

  operations << 'DROP ALL INDEXES'
end

#execute(cypher_string) ⇒ Object



219
220
221
# File 'lib/active_cypher/migration.rb', line 219

def execute(cypher_string)
  operations << cypher_string.strip
end

#runObject

Execute the migration.



24
25
26
27
# File 'lib/active_cypher/migration.rb', line 24

def run
  instance_eval(&self.class.up_block) if self.class.up_block
  execute_operations
end