Class: Cyrel::Node

Inherits:
Object
  • Object
show all
Defined in:
lib/cyrel/node.rb

Overview

The base class for building Cypher queries.

Instance Method Summary collapse

Constructor Details

#initialize(label, as: nil) ⇒ Node

Returns a new instance of Node.

Parameters:

  • label (String)

    The label of the node.

  • as (Symbol, nil) (defaults to: nil)

    An optional alias for the node.



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/cyrel/node.rb', line 8

def initialize(label, as: nil)
  @label = label
  @alias = as || label.to_s.underscore.to_sym
  @conditions = {}
  @raw_conditions = []
  @related_node_conditions = {}
  @optional_match = false
  @return_fields = []
  @order_clauses = []
  @skip_value = nil
  @limit_value = nil
  @with_clause = nil
  @where_after_with = nil
  @remove_props = []
  @detach_delete = false
  @path_variable = nil
end

Instance Method Details

#allObject

Match all nodes of this type



50
51
52
# File 'lib/cyrel/node.rb', line 50

def all
  self
end

#as_path(path_variable) ⇒ Object



125
126
127
128
# File 'lib/cyrel/node.rb', line 125

def as_path(path_variable)
  @path_variable = path_variable
  self
end

#callObject

Invokes a subquery block. Like a Matryoshka of complexity. Because why write one query when you can write two for double the confusion?



149
150
151
152
153
154
155
156
157
158
# File 'lib/cyrel/node.rb', line 149

def call(&)
  if block_given?
    subquery = self.class.new(@label, as: @alias)
    subquery.instance_eval(&)
    @call_subquery = subquery
  else
    # This is for standalone call
  end
  self
end

#create(properties) ⇒ Object



83
84
85
86
# File 'lib/cyrel/node.rb', line 83

def create(properties)
  @create_properties = properties
  self
end

#detach_deleteObject

Schedules the node for DEATH, WITH DETACHMENT. Cold, clean, and emotionally unavailable. Just like your ex.



100
101
102
103
# File 'lib/cyrel/node.rb', line 100

def detach_delete
  @detach_delete = true
  self
end

#limit(amount) ⇒ Object



115
116
117
118
# File 'lib/cyrel/node.rb', line 115

def limit(amount)
  @limit_value = amount
  self
end

#match(pattern) ⇒ Object

Defines the pattern to MATCH. Not to be confused with your desperate search for compatibility on dating apps.



73
74
75
76
# File 'lib/cyrel/node.rb', line 73

def match(pattern)
  @match_pattern = pattern
  self
end

#merge(properties) ⇒ Object



88
89
90
91
# File 'lib/cyrel/node.rb', line 88

def merge(properties)
  @merge_properties = properties
  self
end

#node(label, as: nil) ⇒ Object

Specifies a related node.

Parameters:

  • label (String)

    The label of the related node.

  • as (Symbol, nil) (defaults to: nil)

    An optional alias for the related node.



178
179
180
181
182
# File 'lib/cyrel/node.rb', line 178

def node(label, as: nil)
  @related_node_label = label
  @related_node_alias = as || label.to_s.underscore.to_sym
  self
end

#optional_outgoing(relationship) ⇒ Object

Specifies an optional outgoing relationship. It’s not ghosting, it’s just… optional commitment.



169
170
171
172
173
# File 'lib/cyrel/node.rb', line 169

def optional_outgoing(relationship)
  @outgoing_relationship = relationship
  @optional_match = true
  self
end

#order_by(field, direction = :asc) ⇒ Object



105
106
107
108
# File 'lib/cyrel/node.rb', line 105

def order_by(field, direction = :asc)
  @order_clauses << { field: field, direction: direction }
  self
end

#outgoing(relationship) ⇒ Object

Specifies an outgoing relationship.

Parameters:

  • relationship (Symbol)

    The type of relationship.



162
163
164
165
# File 'lib/cyrel/node.rb', line 162

def outgoing(relationship)
  @outgoing_relationship = relationship
  self
end

#remove(*properties) ⇒ Object



93
94
95
96
# File 'lib/cyrel/node.rb', line 93

def remove(*properties)
  @remove_props.concat(properties)
  self
end

#return(*fields) ⇒ Object

Specifies what to return in the query. Also raises an exception if you try to be too clever—because your cleverness is not welcome here.

Parameters:

  • fields (Array<Symbol, String>)

    The fields to return.



57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/cyrel/node.rb', line 57

def return(*fields)
  fields.each do |field|
    # Skip validation for path variables and variables from subqueries
    next if field.is_a?(String) && (field == @path_variable.to_s || (field.match(/^\w+$/) && defined_in_query?(field)))

    if field.is_a?(Symbol) || (field.is_a?(String) && !field.include?('.') && !field.include?(' as ') &&
                              !field.include?('(') && !field.match(/\[.+\]/))
      raise StandardError, 'Ambiguous name. Please use a string with alias.'
    end
  end
  @return_fields = fields
  self
end

#set(properties) ⇒ Object



78
79
80
81
# File 'lib/cyrel/node.rb', line 78

def set(properties)
  @set_properties = properties
  self
end

#skip(amount) ⇒ Object



110
111
112
113
# File 'lib/cyrel/node.rb', line 110

def skip(amount)
  @skip_value = amount
  self
end

#to_cypherString

This method assembles the final Cypher query like Dr. Frankenstein assembling a monster: a bit of this, a stitch of that, and screaming at lightning until it runs. If this explodes, blame the architecture, not the architect.

Returns:

  • (String)

    The Cypher query string.



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/cyrel/node.rb', line 188

def to_cypher
  parts = []

  # CREATE or MERGE clauses
  if @create_properties
    formatted_props = format_properties(@create_properties)
    parts << "CREATE (#{@alias}:#{@label} {#{formatted_props}})"
  elsif @merge_properties
    formatted_props = format_properties(@merge_properties)
    parts << "MERGE (#{@alias}:#{@label} {#{formatted_props}})"
  else
    # Build MATCH clause
    match_clause = build_match_clause
    parts << match_clause if match_clause
  end

  # Add CALL subquery if present
  if @call_subquery
    subquery_cypher = @call_subquery.to_cypher
    # Format for subquery in CALL
    subquery_cypher = subquery_cypher.gsub(/^MATCH /, '')
    parts << "CALL { MATCH #{subquery_cypher} }"
  end

  # WITH clause
  parts << "WITH #{@with_clause}" if @with_clause
  parts << "WHERE #{@where_after_with}" if @where_after_with && @with_clause

  # SET clause for property updates
  if @set_properties
    set_parts = @set_properties.map { |k, v| "#{@alias}.#{k} = #{format_value(v)}" }
    parts << "SET #{set_parts.join(', ')}"
  end

  # REMOVE clause
  if @remove_props.any?
    remove_parts = @remove_props.map { |prop| "#{@alias}.#{prop}" }
    parts << "REMOVE #{remove_parts.join(', ')}"
  end

  # DETACH DELETE clause
  parts << "DETACH DELETE #{@alias}" if @detach_delete

  # RETURN clause
  if @return_fields.any?
    return_parts = @return_fields.map do |field|
      if field.is_a?(Symbol)
        "#{@alias}.#{field}"
      elsif field.is_a?(String)
        # Check if it's a path variable
        if @path_variable && field == @path_variable.to_s
          field
        # Check if it might be a variable from a subquery
        elsif field.match(/^\w+$/) && defined_in_query?(field)
          field
        # Handle function calls (prevent node alias prefix on CASE/function keywords)
        elsif field.start_with?('CASE ') || field.match(/^\w+\s*\(/)
          field.gsub(' as ', ' AS ')
        # Handle pattern comprehensions
        elsif field.match(/\[.+\]/)
          field.gsub(' as ', ' AS ')
        # Handle field with alias syntax
        elsif field.include?(' as ')
          modified_field = field.gsub(' as ', ' AS ')
          if modified_field.include?('.')
            modified_field
          else
            "#{@alias}.#{modified_field}"
          end
        # Handle field without dot notation
        elsif !field.include?('.')
          "#{@alias}.#{field}"
        else
          field
        end
      else
        field
      end
    end
    parts << "RETURN #{return_parts.join(', ')}"
  end

  # ORDER BY clause
  if @order_clauses.any?
    order_parts = @order_clauses.map do |clause|
      "#{@alias}.#{clause[:field]} #{clause[:direction].to_s.upcase}"
    end
    parts << "ORDER BY #{order_parts.join(', ')}"
  end

  # SKIP and LIMIT
  parts << "SKIP #{@skip_value}" if @skip_value
  parts << "LIMIT #{@limit_value}" if @limit_value

  parts.join(' ')
end

#where(conditions) ⇒ Object

Adds conditions to the query.

Parameters:

  • conditions (Hash, String)

    A hash of conditions or a raw condition string to add to the query.



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/cyrel/node.rb', line 28

def where(conditions)
  if conditions.is_a?(String)
    # Process string-based conditions
    if @with_clause && !@where_after_with.nil?
      @where_after_with = "#{@where_after_with} AND #{conditions}"
    elsif @with_clause
      @where_after_with = conditions
    else
      @raw_conditions << conditions
    end
  else
    conditions = conditions.transform_keys(&:to_s)
    if @outgoing_relationship && @related_node_label
      @related_node_conditions.merge!(conditions)
    else
      @conditions.merge!(conditions)
    end
  end
  self
end

#where_existsObject

Builds a WHERE EXISTS subquery. A fancy way to say, “Does this thing even exist?”—the same question your self-esteem asks daily.



132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/cyrel/node.rb', line 132

def where_exists(&)
  subquery = self.class.new(@label, as: @alias)
  subquery.instance_eval(&)
  pattern = "(#{@alias})"

  if subquery.instance_variable_get(:@outgoing_relationship)
    rel = subquery.instance_variable_get(:@outgoing_relationship)
    rel_node_label = subquery.instance_variable_get(:@related_node_label)
    pattern += "-[:#{rel}]->(:#{rel_node_label})"
  end

  @raw_conditions << "EXISTS(#{pattern})"
  self
end

#with(clause) ⇒ Object



120
121
122
123
# File 'lib/cyrel/node.rb', line 120

def with(clause)
  @with_clause = clause
  self
end