activecypher 0.7.3 → 0.8.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/lib/active_cypher/connection_adapters/memgraph_adapter.rb +10 -1
  3. data/lib/active_cypher/connection_adapters/neo4j_adapter.rb +1 -1
  4. data/lib/active_cypher/connection_adapters/persistence_methods.rb +31 -14
  5. data/lib/active_cypher/relation.rb +1 -1
  6. data/lib/active_cypher/version.rb +1 -1
  7. data/lib/activecypher.rb +3 -1
  8. data/lib/cyrel/ast/call_node.rb +39 -0
  9. data/lib/cyrel/ast/clause_adapter.rb +38 -0
  10. data/lib/cyrel/ast/clause_node.rb +10 -0
  11. data/lib/cyrel/ast/compiler.rb +609 -0
  12. data/lib/cyrel/ast/create_node.rb +21 -0
  13. data/lib/cyrel/ast/delete_node.rb +22 -0
  14. data/lib/cyrel/ast/expression_node.rb +10 -0
  15. data/lib/cyrel/ast/foreach_node.rb +23 -0
  16. data/lib/cyrel/ast/limit_node.rb +21 -0
  17. data/lib/cyrel/ast/literal_node.rb +39 -0
  18. data/lib/cyrel/ast/load_csv_node.rb +24 -0
  19. data/lib/cyrel/ast/match_node.rb +23 -0
  20. data/lib/cyrel/ast/merge_node.rb +23 -0
  21. data/lib/cyrel/ast/node.rb +36 -0
  22. data/lib/cyrel/ast/optimized_nodes.rb +117 -0
  23. data/lib/cyrel/ast/order_by_node.rb +21 -0
  24. data/lib/cyrel/ast/pattern_node.rb +10 -0
  25. data/lib/cyrel/ast/query_integrated_compiler.rb +27 -0
  26. data/lib/cyrel/ast/remove_node.rb +21 -0
  27. data/lib/cyrel/ast/return_node.rb +21 -0
  28. data/lib/cyrel/ast/set_node.rb +20 -0
  29. data/lib/cyrel/ast/simple_cache.rb +50 -0
  30. data/lib/cyrel/ast/skip_node.rb +19 -0
  31. data/lib/cyrel/ast/union_node.rb +22 -0
  32. data/lib/cyrel/ast/unwind_node.rb +20 -0
  33. data/lib/cyrel/ast/where_node.rb +20 -0
  34. data/lib/cyrel/ast/with_node.rb +23 -0
  35. data/lib/cyrel/clause/unwind.rb +71 -0
  36. data/lib/cyrel/expression/literal.rb +9 -2
  37. data/lib/cyrel/expression/property_access.rb +1 -1
  38. data/lib/cyrel/pattern/node.rb +11 -1
  39. data/lib/cyrel/pattern/relationship.rb +21 -13
  40. data/lib/cyrel/query.rb +405 -91
  41. data/lib/cyrel.rb +132 -2
  42. metadata +29 -1
@@ -0,0 +1,609 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stringio'
4
+
5
+ module Cyrel
6
+ module AST
7
+ # Compiles AST nodes into Cypher queries with optional thread-safety
8
+ # It's like Google Translate, but for graph databases and with more reliable results
9
+ class Compiler
10
+ attr_reader :output, :parameters
11
+
12
+ def initialize
13
+ @output = StringIO.new
14
+ if defined?(Concurrent)
15
+ @parameters = Concurrent::Hash.new
16
+ @param_counter = Concurrent::AtomicFixnum.new(0)
17
+ else
18
+ @parameters = {}
19
+ @param_counter = 0
20
+ end
21
+ @first_clause = true
22
+ @loop_variables = Set.new # Track loop variables that shouldn't be parameterized
23
+ end
24
+
25
+ # Compile an AST node or array of nodes
26
+ # Returns [cypher_string, parameters_hash]
27
+ def compile(node_or_nodes)
28
+ nodes = Array(node_or_nodes)
29
+
30
+ nodes.each do |node|
31
+ add_clause_separator unless @first_clause
32
+ node.accept(self)
33
+ @first_clause = false
34
+ end
35
+
36
+ [@output.string, @parameters]
37
+ end
38
+
39
+ # Visit a LIMIT node
40
+ # Because sometimes less is more, except in this comment
41
+ def visit_limit_node(node)
42
+ @output << 'LIMIT '
43
+ render_expression(node.expression)
44
+ end
45
+
46
+ # Visit a SKIP node
47
+ # For when you want to jump ahead in your results
48
+ def visit_skip_node(node)
49
+ @output << 'SKIP '
50
+ render_expression(node.amount)
51
+ end
52
+
53
+ # Visit an ORDER BY node
54
+ # Because even chaos needs some structure sometimes
55
+ def visit_order_by_node(node)
56
+ @output << 'ORDER BY '
57
+ @in_order_by = true
58
+
59
+ node.items.each_with_index do |(expr, direction), index|
60
+ @output << ', ' if index.positive?
61
+ render_expression(expr)
62
+ @output << " #{direction.to_s.upcase}" if direction && direction != :asc
63
+ end
64
+
65
+ @in_order_by = false
66
+ end
67
+
68
+ # Visit a WHERE node
69
+ # Because sometimes you need to be selective about your relationships
70
+ def visit_where_node(node)
71
+ return if node.conditions.empty?
72
+
73
+ @output << 'WHERE '
74
+
75
+ if node.conditions.length == 1
76
+ render_expression(node.conditions.first)
77
+ else
78
+ # Combine multiple conditions with AND
79
+ node.conditions.each_with_index do |condition, index|
80
+ @output << ' AND ' if index.positive?
81
+ render_expression(condition)
82
+ end
83
+ end
84
+ end
85
+
86
+ # Visit a RETURN node
87
+ # Where your data comes home to roost
88
+ def visit_return_node(node)
89
+ @output << 'RETURN '
90
+ @output << 'DISTINCT ' if node.distinct
91
+
92
+ node.items.each_with_index do |item, index|
93
+ @output << ', ' if index.positive?
94
+ render_expression(item)
95
+ end
96
+ end
97
+
98
+ # Visit a SET node
99
+ # Where change happens, one property at a time
100
+ def visit_set_node(node)
101
+ return if node.assignments.empty?
102
+
103
+ @output << 'SET '
104
+
105
+ node.assignments.each_with_index do |assignment, index|
106
+ @output << ', ' if index.positive?
107
+ render_assignment(assignment)
108
+ end
109
+ end
110
+
111
+ # Visit a WITH node
112
+ # Because sometimes you need to pass data along for the next part of your journey
113
+ def visit_with_node(node)
114
+ @output << 'WITH '
115
+ @output << 'DISTINCT ' if node.distinct
116
+
117
+ node.items.each_with_index do |item, index|
118
+ @output << ', ' if index.positive?
119
+ render_expression(item)
120
+ end
121
+
122
+ # Add WHERE clause if present
123
+ return unless node.where_conditions && !node.where_conditions.empty?
124
+
125
+ @output << "\nWHERE "
126
+
127
+ if node.where_conditions.length == 1
128
+ render_expression(node.where_conditions.first)
129
+ else
130
+ # Combine multiple conditions with AND
131
+ node.where_conditions.each_with_index do |condition, index|
132
+ @output << ' AND ' if index.positive?
133
+ render_expression(condition)
134
+ end
135
+ end
136
+ end
137
+
138
+ # Visit an UNWIND node
139
+ # Unpacking arrays like unwrapping presents
140
+ def visit_unwind_node(node)
141
+ @output << 'UNWIND '
142
+
143
+ # Render the expression to unwind
144
+ if node.expression.is_a?(Array)
145
+ # Array literal
146
+ @output << format_array_literal(node.expression)
147
+ elsif node.expression.is_a?(Symbol)
148
+ # Parameter reference
149
+ param_key = register_parameter(node.expression)
150
+ @output << "$#{param_key}"
151
+ else
152
+ # Other expressions
153
+ render_expression(node.expression)
154
+ end
155
+
156
+ @output << " AS #{node.alias_name}"
157
+ end
158
+
159
+ # Visit a MATCH clause node
160
+ # Finding nodes in the graph, one pattern at a time
161
+ def visit_match_node(node)
162
+ @output << (node.optional ? 'OPTIONAL MATCH ' : 'MATCH ')
163
+
164
+ # Handle path variable assignment if present
165
+ @output << "#{node.path_variable} = " if node.path_variable
166
+
167
+ # Render the pattern
168
+ render_pattern(node.pattern)
169
+ end
170
+
171
+ # Visit a CREATE clause node
172
+ # Making nodes and relationships appear out of thin air
173
+ def visit_create_node(node)
174
+ @output << 'CREATE '
175
+ render_pattern(node.pattern)
176
+ end
177
+
178
+ # Visit a MERGE clause node
179
+ # Finding or creating, because commitment issues
180
+ def visit_merge_node(node)
181
+ @output << 'MERGE '
182
+ render_pattern(node.pattern)
183
+
184
+ # Handle ON CREATE SET
185
+ if node.on_create
186
+ @output << "\nON CREATE SET "
187
+ render_merge_assignments(node.on_create)
188
+ end
189
+
190
+ # Handle ON MATCH SET
191
+ return unless node.on_match
192
+
193
+ @output << "\nON MATCH SET "
194
+ render_merge_assignments(node.on_match)
195
+ end
196
+
197
+ # Visit a DELETE clause node
198
+ # Making nodes disappear, potentially with their relationships
199
+ def visit_delete_node(node)
200
+ @output << 'DETACH ' if node.detach
201
+ @output << 'DELETE '
202
+
203
+ node.variables.each_with_index do |var, i|
204
+ @output << ', ' if i.positive?
205
+ @output << var.to_s
206
+ end
207
+ end
208
+
209
+ # Visit a REMOVE clause node
210
+ # Tidying up properties and labels
211
+ def visit_remove_node(node)
212
+ @output << 'REMOVE '
213
+
214
+ node.items.each_with_index do |item, i|
215
+ @output << ', ' if i.positive?
216
+
217
+ case item
218
+ when Expression::PropertyAccess
219
+ # Remove property
220
+ render_expression(item)
221
+ when Array
222
+ # Remove label [variable, label]
223
+ variable, label = item
224
+ @output << "#{variable}:#{label}"
225
+ else
226
+ raise "Unknown REMOVE item type: #{item.class}"
227
+ end
228
+ end
229
+ end
230
+
231
+ # Visit a CALL clause node (procedure call)
232
+ # Invoking the stored procedures of the graph database
233
+ def visit_call_node(node)
234
+ @output << "CALL #{node.procedure_name}"
235
+
236
+ if node.arguments.any?
237
+ @output << '('
238
+ node.arguments.each_with_index do |arg, i|
239
+ @output << ', ' if i.positive?
240
+ render_expression(Expression.coerce(arg))
241
+ end
242
+ @output << ')'
243
+ end
244
+
245
+ return unless node.yield_items
246
+
247
+ @output << ' YIELD '
248
+ if node.yield_items.is_a?(Hash)
249
+ # YIELD items with aliases
250
+ node.yield_items.each_with_index do |(item, alias_name), i|
251
+ @output << ', ' if i.positive?
252
+ @output << item.to_s
253
+ @output << " AS #{alias_name}" if alias_name && alias_name != item
254
+ end
255
+ else
256
+ # Simple yield list
257
+ Array(node.yield_items).each_with_index do |item, i|
258
+ @output << ', ' if i.positive?
259
+ @output << item.to_s
260
+ end
261
+ end
262
+ end
263
+
264
+ # Visit a CALL subquery node
265
+ # Queries all the way down
266
+ def visit_call_subquery_node(node)
267
+ @output << "CALL {\n"
268
+
269
+ # Render subquery clauses directly without going through the cache
270
+ # to avoid recursive locking issues
271
+ subquery = node.subquery
272
+ subquery_clauses = subquery.clauses.sort_by { |clause| subquery.send(:clause_order, clause) }
273
+
274
+ subquery_clauses.each_with_index do |clause, index|
275
+ @output << "\n" if index.positive? # Add newline between clauses
276
+
277
+ if clause.is_a?(AST::ClauseAdapter)
278
+ # For AST-based clauses, compile directly without cache
279
+ # Create a proxy object that forwards parameter registration to this compiler
280
+ parameter_proxy = Object.new
281
+ parent_compiler = self
282
+ parameter_proxy.define_singleton_method(:register_parameter) do |value|
283
+ parent_compiler.send(:register_parameter, value)
284
+ end
285
+
286
+ subquery_compiler = QueryIntegratedCompiler.new(parameter_proxy)
287
+ clause_cypher, = subquery_compiler.compile(clause.ast_node)
288
+ @output << clause_cypher.split("\n").map { |line| " #{line}" }.join("\n")
289
+ else
290
+ # For legacy clauses, render normally
291
+ clause_output = clause.render(subquery)
292
+ @output << clause_output.split("\n").map { |line| " #{line}" }.join("\n") unless clause_output.blank?
293
+
294
+ # Merge subquery parameters
295
+ subquery.parameters.each_value do |value|
296
+ register_parameter(value)
297
+ end
298
+ end
299
+ end
300
+
301
+ @output << "\n}"
302
+ end
303
+
304
+ # Visit a UNION node
305
+ # Combining queries like a Cypher mixologist
306
+ def visit_union_node(node)
307
+ # UNION is special - it combines complete queries
308
+ # We need to render each query's clauses directly to avoid recursive locking
309
+ node.queries.each_with_index do |query, index|
310
+ if index.positive?
311
+ @output << "\n"
312
+ @output << 'UNION'
313
+ @output << ' ALL' if node.all
314
+ @output << "\n"
315
+ end
316
+
317
+ # Render query clauses directly without going through cache
318
+ query_clauses = query.clauses.sort_by { |clause| query.send(:clause_order, clause) }
319
+
320
+ query_clauses.each_with_index do |clause, clause_index|
321
+ @output << "\n" if clause_index.positive?
322
+
323
+ if clause.is_a?(AST::ClauseAdapter)
324
+ # For AST-based clauses, compile directly without cache
325
+ # Create a proxy object that forwards parameter registration to this compiler
326
+ parameter_proxy = Object.new
327
+ parent_compiler = self
328
+ parameter_proxy.define_singleton_method(:register_parameter) do |value|
329
+ parent_compiler.send(:register_parameter, value)
330
+ end
331
+
332
+ clause_compiler = QueryIntegratedCompiler.new(parameter_proxy)
333
+ clause_cypher, = clause_compiler.compile(clause.ast_node)
334
+ @output << clause_cypher
335
+ else
336
+ # For legacy clauses, render normally
337
+ clause_output = clause.render(query)
338
+ @output << clause_output unless clause_output.blank?
339
+
340
+ # Merge query parameters
341
+ query.parameters.each_value do |value|
342
+ register_parameter(value)
343
+ end
344
+ end
345
+ end
346
+ end
347
+ end
348
+
349
+ # Visit a FOREACH node
350
+ # Iterating through lists like a database therapist
351
+ def visit_foreach_node(node)
352
+ @output << "FOREACH (#{node.variable} IN "
353
+
354
+ # Handle the expression - could be an array literal or an expression
355
+ if node.expression.is_a?(Array)
356
+ # Array literal - convert to parameter
357
+ param_key = register_parameter(node.expression)
358
+ @output << "$#{param_key}"
359
+ elsif node.expression.is_a?(Symbol)
360
+ # Symbol reference to parameter
361
+ param_key = register_parameter(node.expression)
362
+ @output << "$#{param_key}"
363
+ else
364
+ # Other expressions
365
+ render_expression(node.expression)
366
+ end
367
+
368
+ @output << ' | '
369
+
370
+ # Track the loop variable so it doesn't get parameterized in inner clauses
371
+ old_loop_variables = @loop_variables.dup
372
+ @loop_variables.add(node.variable.to_sym)
373
+
374
+ # Render update clauses without duplication
375
+ node.update_clauses.each_with_index do |clause, index|
376
+ @output << ' ' if index.positive?
377
+
378
+ raise "Unexpected clause type in FOREACH: #{clause.class}" unless clause.is_a?(AST::ClauseAdapter)
379
+
380
+ # For AST-based clauses, compile just the inner content
381
+ # Create a proxy object that forwards parameter registration to this compiler
382
+ # and inherits the loop variable context
383
+ parameter_proxy = Object.new
384
+ parent_compiler = self
385
+ current_loop_variables = @loop_variables.dup
386
+ parameter_proxy.define_singleton_method(:register_parameter) do |value|
387
+ # Check if this is a loop variable that shouldn't be parameterized
388
+ if value.is_a?(Symbol) && current_loop_variables.include?(value)
389
+ value # Return the symbol itself, not a parameter key
390
+ else
391
+ parent_compiler.send(:register_parameter, value)
392
+ end
393
+ end
394
+
395
+ inner_compiler = QueryIntegratedCompiler.new(parameter_proxy)
396
+ # Pass the loop variables context to the inner compiler
397
+ inner_compiler.instance_variable_set(:@loop_variables, @loop_variables.dup)
398
+ clause_cypher, = inner_compiler.compile([clause.ast_node])
399
+ @output << clause_cypher
400
+
401
+ # For other clause types, render directly
402
+ end
403
+
404
+ # Restore previous loop variables context
405
+ @loop_variables = old_loop_variables
406
+
407
+ @output << ')'
408
+ end
409
+
410
+ # Visit a LOAD CSV node
411
+ # Reading CSV files like it's 1999
412
+ def visit_load_csv_node(node)
413
+ @output << 'LOAD CSV'
414
+ @output << ' WITH HEADERS' if node.with_headers
415
+ @output << ' FROM '
416
+
417
+ # URL can be a string or expression
418
+ if node.url.is_a?(String)
419
+ param_key = register_parameter(node.url)
420
+ @output << "$#{param_key}"
421
+ else
422
+ render_expression(node.url)
423
+ end
424
+
425
+ @output << " AS #{node.variable}"
426
+
427
+ return unless node.fieldterminator
428
+
429
+ @output << ' FIELDTERMINATOR '
430
+ param_key = register_parameter(node.fieldterminator)
431
+ @output << "$#{param_key}"
432
+ end
433
+
434
+ # Visit a literal value node
435
+ # The most honest node in the entire tree
436
+ def visit_literal_node(node)
437
+ if node.value.is_a?(Symbol)
438
+ # Check if this symbol is a loop variable
439
+ @output << if @loop_variables.include?(node.value)
440
+ # Loop variables are rendered as-is, not as parameters
441
+ node.value.to_s
442
+ else
443
+ # Symbols are parameter references, not values to be parameterized
444
+ "$#{node.value}"
445
+ end
446
+ else
447
+ # All other literals become parameters for consistency with existing behavior
448
+ param_key = register_parameter(node.value)
449
+ @output << "$#{param_key}"
450
+ end
451
+ end
452
+
453
+ private
454
+
455
+ def add_clause_separator
456
+ @output << "\n"
457
+ end
458
+
459
+ # Render a pattern (node, relationship, or path)
460
+ def render_pattern(pattern)
461
+ # Patterns have their own render methods
462
+ raise "Don't know how to render pattern: #{pattern.inspect}" unless pattern.respond_to?(:render)
463
+
464
+ @output << pattern.render(@query || self)
465
+ end
466
+
467
+ # Render a SET assignment based on its type
468
+ def render_assignment(assignment)
469
+ type, *args = assignment
470
+
471
+ case type
472
+ when :property
473
+ # SET n.prop = value
474
+ prop_access, value = args
475
+ render_expression(prop_access)
476
+ @output << ' = '
477
+ render_expression(value)
478
+ when :variable_properties
479
+ # SET n = {props} or SET n += {props}
480
+ variable, value, operator = args
481
+ @output << variable.to_s
482
+ @output << (operator == :merge ? ' += ' : ' = ')
483
+ render_expression(value)
484
+ when :label
485
+ # SET n:Label
486
+ variable, label = args
487
+ @output << "#{variable}:#{label}"
488
+ else
489
+ raise "Unknown assignment type: #{type}"
490
+ end
491
+ end
492
+
493
+ # Render merge assignments (ON CREATE/MATCH SET)
494
+ def render_merge_assignments(assignments)
495
+ case assignments
496
+ when Array
497
+ assignments.each_with_index do |item, i|
498
+ @output << ', ' if i.positive?
499
+ render_set_item(item)
500
+ end
501
+ when Hash
502
+ assignments.each_with_index do |(key, value), i|
503
+ @output << ', ' if i.positive?
504
+ render_expression(key)
505
+ @output << ' = '
506
+ render_expression(Expression.coerce(value))
507
+ end
508
+ else
509
+ raise "Unknown assignments type: #{assignments.class}"
510
+ end
511
+ end
512
+
513
+ # Render a single SET item (for MERGE ON CREATE/MATCH SET)
514
+ def render_set_item(item)
515
+ case item
516
+ when Array
517
+ # It's a property assignment like [:n, :name, "value"]
518
+ variable, property, value = item
519
+ @output << "#{variable}.#{property} = "
520
+ render_expression(Expression.coerce(value))
521
+ else
522
+ raise "Unknown SET item type: #{item.class}"
523
+ end
524
+ end
525
+
526
+ # Render an expression (could be a literal, parameter, property access, etc.)
527
+ def render_expression(expr)
528
+ case expr
529
+ in Node
530
+ # It's already an AST node
531
+ expr.accept(self)
532
+ in Clause::Return::RawIdentifier
533
+ # Raw identifiers (variable names) are rendered as-is
534
+ @output << expr.identifier
535
+ in Symbol
536
+ # Symbols in ORDER BY context are identifiers, not parameters
537
+ # Loop variables should also not be parameterized
538
+ if @in_order_by || @loop_variables.include?(expr)
539
+ @output << expr.to_s
540
+ else
541
+ # Parameterize the symbol
542
+ param_key = register_parameter(expr)
543
+ @output << "$#{param_key}"
544
+ end
545
+ in Numeric | true | false | nil
546
+ # Wrap in literal node and visit
547
+ LiteralNode.new(expr).accept(self)
548
+ in String
549
+ # Strings in ORDER BY context are column names/aliases, not literals
550
+ if @in_order_by
551
+ @output << expr
552
+ else
553
+ # Strings should be parameterized
554
+ LiteralNode.new(expr).accept(self)
555
+ end
556
+ else
557
+ # Try common methods
558
+ if expr.respond_to?(:render)
559
+ # Has a render method (like PropertyAccess, FunctionCall, etc.)
560
+ @output << expr.render(@query || self)
561
+ elsif expr.respond_to?(:to_ast)
562
+ # Has a to_ast method
563
+ expr.to_ast.accept(self)
564
+ else
565
+ raise "Don't know how to render expression: #{expr.inspect}"
566
+ end
567
+ end
568
+ end
569
+
570
+ # Register a parameter and return its key (thread-safe if Concurrent is available)
571
+ # Because $p1, $p2, $p3 is the naming convention we deserve
572
+ def register_parameter(value)
573
+ existing_key = @parameters.key(value)
574
+ return existing_key if existing_key
575
+
576
+ if defined?(Concurrent) && @parameters.is_a?(Concurrent::Hash)
577
+ # Thread-safe parameter registration
578
+
579
+ counter = @param_counter.increment
580
+ key = :"p#{counter}"
581
+ else
582
+ # Non-concurrent version
583
+
584
+ @param_counter += 1
585
+ key = :"p#{@param_counter}"
586
+ end
587
+ @parameters[key] = value
588
+ key
589
+ end
590
+
591
+ # Format an array literal for Cypher output
592
+ def format_array_literal(array)
593
+ elements = array.map do |element|
594
+ case element
595
+ when String
596
+ "'#{element}'"
597
+ when Symbol
598
+ "'#{element}'"
599
+ when Array
600
+ format_array_literal(element)
601
+ else
602
+ element.to_s
603
+ end
604
+ end
605
+ "[#{elements.join(', ')}]"
606
+ end
607
+ end
608
+ end
609
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyrel
4
+ module AST
5
+ # AST node for CREATE clauses
6
+ # Because sometimes you need to make things exist
7
+ class CreateNode < ClauseNode
8
+ attr_reader :pattern
9
+
10
+ def initialize(pattern)
11
+ @pattern = pattern
12
+ end
13
+
14
+ protected
15
+
16
+ def state
17
+ [@pattern]
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyrel
4
+ module AST
5
+ # AST node for DELETE and DETACH DELETE clauses
6
+ # Because sometimes things need to disappear, with or without their relationships
7
+ class DeleteNode < ClauseNode
8
+ attr_reader :variables, :detach
9
+
10
+ def initialize(variables, detach: false)
11
+ @variables = variables
12
+ @detach = detach
13
+ end
14
+
15
+ protected
16
+
17
+ def state
18
+ [@variables, @detach]
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyrel
4
+ module AST
5
+ # Base class for expression nodes (literals, comparisons, functions, etc.)
6
+ # Where math meets philosophy and neither wins
7
+ class ExpressionNode < Node
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyrel
4
+ module AST
5
+ # AST node for FOREACH clause (extension)
6
+ # For when you need to iterate through a list and make changes
7
+ class ForeachNode < ClauseNode
8
+ attr_reader :variable, :expression, :update_clauses
9
+
10
+ def initialize(variable, expression, update_clauses)
11
+ @variable = variable
12
+ @expression = expression
13
+ @update_clauses = update_clauses
14
+ end
15
+
16
+ protected
17
+
18
+ def state
19
+ [@variable, @expression, @update_clauses]
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyrel
4
+ module AST
5
+ # AST node for LIMIT clause
6
+ # For when you want boundaries, even in your queries
7
+ class LimitNode < ClauseNode
8
+ attr_reader :expression
9
+
10
+ def initialize(expression)
11
+ @expression = expression
12
+ end
13
+
14
+ protected
15
+
16
+ def state
17
+ [@expression]
18
+ end
19
+ end
20
+ end
21
+ end