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

Returns a new instance of 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).

Parameters:

  • name (String)

    Index name

  • rel_type (Symbol, String)

    Relationship type

  • props (Array<Symbol>)

    Properties to index

  • if_not_exists (Boolean) (defaults to: true)

    Add IF NOT EXISTS clause

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.

Parameters:

  • label (Symbol, String)

    Node label

  • props (Array<Symbol>)

    Properties to index

  • unique (Boolean) (defaults to: false)

    Create unique index (Neo4j only)

  • if_not_exists (Boolean) (defaults to: true)

    Add IF NOT EXISTS clause (Neo4j only)

  • name (String) (defaults to: nil)

    Index name (Neo4j only)

  • composite (Boolean) (defaults to: nil)

    Create composite index (Memgraph 3.2+). Default true for multiple props.



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.

Parameters:

  • rel_type (Symbol, String)

    Relationship type

  • props (Array<Symbol>)

    Properties to index

  • if_not_exists (Boolean) (defaults to: true)

    Add IF NOT EXISTS clause (Neo4j only)

  • name (String) (defaults to: nil)

    Index name (Neo4j only)

  • composite (Boolean) (defaults to: nil)

    Create composite index (Memgraph 3.2+). Default true for multiple props.



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.

Parameters:

  • name (String)

    Index name

  • rel_type (Symbol, String)

    Relationship type

  • props (Array<Symbol>)

    Properties to 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+).

Parameters:

  • name (String)

    Index name

  • label (Symbol, String)

    Node label

  • property (Symbol)

    Property containing vector embeddings

  • dimension (Integer)

    Vector dimension (required)

  • metric (Symbol) (defaults to: :cosine)

    Distance metric: :cosine, :euclidean, :dot_product (default: :cosine)

  • quantization (Symbol) (defaults to: nil)

    Quantization type for memory reduction (Memgraph 3.4+): :scalar, nil



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+).

Parameters:

  • name (String)

    Index name

  • rel_type (Symbol, String)

    Relationship type

  • property (Symbol)

    Property containing vector embeddings

  • dimension (Integer)

    Vector dimension (required)

  • metric (Symbol) (defaults to: :cosine)

    Distance metric: :cosine, :euclidean, :dot_product (default: :cosine)



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