activecypher 0.7.2 → 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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/lib/active_cypher/associations/collection_proxy.rb +2 -2
  3. data/lib/active_cypher/associations.rb +12 -12
  4. data/lib/active_cypher/connection_adapters/memgraph_adapter.rb +35 -2
  5. data/lib/active_cypher/connection_adapters/neo4j_adapter.rb +32 -0
  6. data/lib/active_cypher/connection_adapters/persistence_methods.rb +38 -10
  7. data/lib/active_cypher/connection_url_resolver.rb +8 -2
  8. data/lib/active_cypher/fixtures/node_builder.rb +3 -2
  9. data/lib/active_cypher/generators/templates/cypher_databases.yml +3 -3
  10. data/lib/active_cypher/model/core.rb +5 -1
  11. data/lib/active_cypher/model/querying.rb +13 -4
  12. data/lib/active_cypher/relation.rb +2 -2
  13. data/lib/active_cypher/relationship.rb +4 -1
  14. data/lib/active_cypher/version.rb +1 -1
  15. data/lib/activecypher.rb +3 -1
  16. data/lib/cyrel/ast/call_node.rb +39 -0
  17. data/lib/cyrel/ast/clause_adapter.rb +38 -0
  18. data/lib/cyrel/ast/clause_node.rb +10 -0
  19. data/lib/cyrel/ast/compiler.rb +609 -0
  20. data/lib/cyrel/ast/create_node.rb +21 -0
  21. data/lib/cyrel/ast/delete_node.rb +22 -0
  22. data/lib/cyrel/ast/expression_node.rb +10 -0
  23. data/lib/cyrel/ast/foreach_node.rb +23 -0
  24. data/lib/cyrel/ast/limit_node.rb +21 -0
  25. data/lib/cyrel/ast/literal_node.rb +39 -0
  26. data/lib/cyrel/ast/load_csv_node.rb +24 -0
  27. data/lib/cyrel/ast/match_node.rb +23 -0
  28. data/lib/cyrel/ast/merge_node.rb +23 -0
  29. data/lib/cyrel/ast/node.rb +36 -0
  30. data/lib/cyrel/ast/optimized_nodes.rb +117 -0
  31. data/lib/cyrel/ast/order_by_node.rb +21 -0
  32. data/lib/cyrel/ast/pattern_node.rb +10 -0
  33. data/lib/cyrel/ast/query_integrated_compiler.rb +27 -0
  34. data/lib/cyrel/ast/remove_node.rb +21 -0
  35. data/lib/cyrel/ast/return_node.rb +21 -0
  36. data/lib/cyrel/ast/set_node.rb +20 -0
  37. data/lib/cyrel/ast/simple_cache.rb +50 -0
  38. data/lib/cyrel/ast/skip_node.rb +19 -0
  39. data/lib/cyrel/ast/union_node.rb +22 -0
  40. data/lib/cyrel/ast/unwind_node.rb +20 -0
  41. data/lib/cyrel/ast/where_node.rb +20 -0
  42. data/lib/cyrel/ast/with_node.rb +23 -0
  43. data/lib/cyrel/clause/unwind.rb +71 -0
  44. data/lib/cyrel/expression/literal.rb +9 -2
  45. data/lib/cyrel/expression/property_access.rb +1 -1
  46. data/lib/cyrel/functions.rb +11 -0
  47. data/lib/cyrel/pattern/node.rb +11 -1
  48. data/lib/cyrel/pattern/relationship.rb +21 -13
  49. data/lib/cyrel/query.rb +405 -91
  50. data/lib/cyrel.rb +137 -3
  51. metadata +29 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 738573111aeb926ef359c2bd2643e07d25f83b76d63f7cebf0d31e762c06fabf
4
- data.tar.gz: 8ede3884325f7e86e4ca06dea636e0193d8742fc19b0ed2e92724f3270f31f7f
3
+ metadata.gz: b73e136ad3f9de8db24567d350140d9679d04d35139d861af4b5821037cc6bdd
4
+ data.tar.gz: 6e795339764ca8e603cc80db6909a983fdcb3877fa2dfcaec3fcf20149d153b1
5
5
  SHA512:
6
- metadata.gz: afc1446c1c2e7195ed3ad241e84bcdaffaf9c634084068088f15137f897ea4955d80448af5b80b225af204a23baa217486b4327160b50b7bd41899d7c6b95df1
7
- data.tar.gz: f5cf3960f8127672e803d9dd9d2246e8245499e2f7208013e2bd97a4e5971fbcd5044aa107bcf1208a21cfc40337d0c89cbc3354706dce06f0bb1df8b62d84a3
6
+ metadata.gz: 803b1091f2227ebe564fad414025e21790710e7e4c60d9b05113c33a1a7d020495c7ce45a876ac9650261f54b60fbca5da7c2d1c0fcdfb8b169918798080feda
7
+ data.tar.gz: e02f17df064f5ef440503bfedfa050572c5326144403e79e32188528f615afbb11c62fe75e6417b26094c4168ede2414b821681c293c5731de5da6ad2ba8c1ea
@@ -128,9 +128,9 @@ module ActiveCypher
128
128
  arrow = (dir == :both ? :'--' : :out)
129
129
  Cyrel
130
130
  .match(Cyrel.node(from_node.class.label_name).as(:a)
131
- .where(Cyrel.id(:a).eq(from_node.internal_id)))
131
+ .where(Cyrel.node_id(:a).eq(from_node.internal_id)))
132
132
  .match(Cyrel.node(to_node.class.label_name).as(:b)
133
- .where(Cyrel.id(:b).eq(to_node.internal_id)))
133
+ .where(Cyrel.node_id(:b).eq(to_node.internal_id)))
134
134
  .create(Cyrel.node(:a).rel(arrow, reflection[:relationship]).to(:b))
135
135
  .tap { |qry| owner.class.connection.execute_cypher(*qry.to_cypher, 'Create Association') }
136
136
  end
@@ -142,7 +142,7 @@ module ActiveCypher
142
142
  # Compose query MATCH – WHERE – RETURN
143
143
  query = Cyrel::Query.new
144
144
  .match(path)
145
- .where(Cyrel.id(a_alias).eq(internal_id))
145
+ .where(Cyrel.node_id(a_alias).eq(internal_id))
146
146
  .return_(b_alias)
147
147
 
148
148
  base_relation = Relation.new(target_class, query)
@@ -186,7 +186,7 @@ module ActiveCypher
186
186
 
187
187
  query = Cyrel::Query.new
188
188
  .match(path)
189
- .where(Cyrel.id(a_alias).eq(internal_id))
189
+ .where(Cyrel.node_id(a_alias).eq(internal_id))
190
190
  .return_(b_alias)
191
191
  .limit(1)
192
192
 
@@ -225,8 +225,8 @@ module ActiveCypher
225
225
  .rel(cyrel_direction, rel_type)
226
226
  .as(del_rel_alias)
227
227
  .to(del_end_alias))
228
- .where(Cyrel.id(del_start_alias).eq(del_start_node.internal_id))
229
- .where(Cyrel.id(del_end_alias).eq(del_end_node.internal_id))
228
+ .where(Cyrel.node_id(del_start_alias).eq(del_start_node.internal_id))
229
+ .where(Cyrel.node_id(del_end_alias).eq(del_end_node.internal_id))
230
230
  .delete(del_rel_alias)
231
231
 
232
232
  self.class.connection.execute_cypher(
@@ -266,8 +266,8 @@ module ActiveCypher
266
266
  create_query = Cyrel
267
267
  .match(Cyrel.node(new_start_node.class.label_name).as(new_start_alias))
268
268
  .match(Cyrel.node(new_end_node.class.label_name).as(new_end_alias))
269
- .where(Cyrel.id(new_start_alias).eq(new_start_node.internal_id))
270
- .where(Cyrel.id(new_end_alias).eq(new_end_node.internal_id))
269
+ .where(Cyrel.node_id(new_start_alias).eq(new_start_node.internal_id))
270
+ .where(Cyrel.node_id(new_end_alias).eq(new_end_node.internal_id))
271
271
  .create(Cyrel.node(new_start_alias)
272
272
  .rel(arrow, rel_type)
273
273
  .to(new_end_alias))
@@ -322,7 +322,7 @@ module ActiveCypher
322
322
  target_node_alias = :target_node
323
323
 
324
324
  start_node_pattern = Cyrel.node(self.class.label_name).as(start_node_alias)
325
- .where(Cyrel.id(start_node_alias).eq(internal_id))
325
+ .where(Cyrel.node_id(start_node_alias).eq(internal_id))
326
326
  target_node_pattern = Cyrel.node(target_class.label_name).as(target_node_alias)
327
327
 
328
328
  rel_pattern = case direction
@@ -372,10 +372,10 @@ module ActiveCypher
372
372
  # Adjust direction for Cyrel pattern if needed
373
373
  cyrel_direction = direction == :in ? :out : direction
374
374
  del_query = Cyrel.match(Cyrel.node(del_start_node.class.label_name)
375
- .as(del_start_alias).where(Cyrel.id(del_start_alias)
375
+ .as(del_start_alias).where(Cyrel.node_id(del_start_alias)
376
376
  .eq(del_start_node.internal_id)))
377
377
  .match(Cyrel.node(del_end_node.class.label_name)
378
- .as(del_end_alias).where(Cyrel.id(del_end_alias)
378
+ .as(del_end_alias).where(Cyrel.node_id(del_end_alias)
379
379
  .eq(del_end_node.internal_id)))
380
380
  .match(Cyrel.node(del_start_alias).rel(cyrel_direction,
381
381
  rel_type).as(del_rel_alias).to(del_end_alias))
@@ -411,10 +411,10 @@ module ActiveCypher
411
411
  new_start_alias = :a
412
412
  new_end_alias = :b
413
413
  create_query = Cyrel.match(Cyrel.node(new_start_node.class.label_name)
414
- .as(new_start_alias).where(Cyrel.id(new_start_alias)
414
+ .as(new_start_alias).where(Cyrel.node_id(new_start_alias)
415
415
  .eq(new_start_node.internal_id)))
416
416
  .match(Cyrel.node(new_end_node.class.label_name)
417
- .as(new_end_alias).where(Cyrel.id(new_end_alias)
417
+ .as(new_end_alias).where(Cyrel.node_id(new_end_alias)
418
418
  .eq(new_end_node.internal_id)))
419
419
  .create(Cyrel.node(new_start_alias).rel(:out, rel_type).to(new_end_alias))
420
420
 
@@ -488,7 +488,7 @@ module ActiveCypher
488
488
 
489
489
  # Start node pattern
490
490
  start_node_pattern = Cyrel.node(self.class.label_name).as(start_node_alias)
491
- .where(Cyrel.id(start_node_alias).eq(internal_id))
491
+ .where(Cyrel.node_id(start_node_alias).eq(internal_id))
492
492
 
493
493
  # Intermediate node pattern (based on through_reflection)
494
494
  intermediate_node_pattern = Cyrel.node(intermediate_class.label_name).as(intermediate_node_alias)
@@ -39,6 +39,27 @@ module ActiveCypher
39
39
  'id(r) AS rid'
40
40
  end
41
41
 
42
+ # Additional helper methods for nodes
43
+ def self.node_id_where(alias_name, param_name = nil)
44
+ if param_name
45
+ "id(#{alias_name}) = $#{param_name}"
46
+ else
47
+ "id(#{alias_name})"
48
+ end
49
+ end
50
+
51
+ def self.node_id_equals_value(alias_name, value)
52
+ "id(#{alias_name}) = #{value}"
53
+ end
54
+
55
+ def self.return_node_id(alias_name, as_name = 'internal_id')
56
+ "id(#{alias_name}) AS #{as_name}"
57
+ end
58
+
59
+ def self.id_function
60
+ 'id'
61
+ end
62
+
42
63
  # Return self as id_handler for compatibility with tests
43
64
  def id_handler
44
65
  self.class
@@ -66,7 +87,10 @@ module ActiveCypher
66
87
  # Memgraph defaults to **implicit auto‑commit** transactions
67
88
  # so we simply run the Cypher and return the rows.
68
89
  def execute_cypher(cypher, params = {}, ctx = 'Query')
69
- rows = run(cypher.gsub(/\belementId\(/i, 'id('), params, context: ctx)
90
+ # Replace adapter-aware placeholder with Memgraph's id function
91
+ # Because Memgraph insists on being different and using id() instead of elementId()
92
+ cypher = cypher.gsub('__NODE_ID__', 'id')
93
+ rows = run(cypher, params, context: ctx)
70
94
  process_records(rows)
71
95
  end
72
96
 
@@ -112,7 +136,16 @@ module ActiveCypher
112
136
  labels = run('MATCH (n) RETURN DISTINCT labels(n) AS lbl').flat_map { |r| r['lbl'] }
113
137
 
114
138
  nodes = labels.map do |lbl|
115
- props = run("MATCH (n:`#{lbl}`) WITH n LIMIT 100 UNWIND keys(n) AS k RETURN DISTINCT k").map { |r| r['k'] }
139
+ # Use Cyrel for secure query building with user-provided label
140
+ query = Cyrel::Query.new
141
+ .match(Cyrel.node(:n, lbl))
142
+ .with(:n)
143
+ .limit(100)
144
+ .unwind(Cyrel.function(:keys, :n), :k)
145
+ .return_('DISTINCT k')
146
+
147
+ cypher, params = query.to_cypher
148
+ props = run(cypher, params).map { |r| r['k'] }
116
149
  Schema::NodeTypeDef.new(lbl, props, nil)
117
150
  end
118
151
 
@@ -61,8 +61,40 @@ module ActiveCypher
61
61
  'elementId(r) AS rid'
62
62
  end
63
63
 
64
+ # Additional helper methods for nodes
65
+ def self.node_id_where(alias_name, param_name = nil)
66
+ if param_name
67
+ "elementId(#{alias_name}) = $#{param_name}"
68
+ else
69
+ "elementId(#{alias_name})"
70
+ end
71
+ end
72
+
73
+ def self.node_id_equals_value(alias_name, value)
74
+ # Quote string values for Cypher because Neo4j is paranoid about injection attacks
75
+ # (As it should be, have you seen what people try to inject these days?)
76
+ quoted_value = value.is_a?(String) ? "'#{value}'" : value
77
+ "elementId(#{alias_name}) = #{quoted_value}"
78
+ end
79
+
80
+ def self.return_node_id(alias_name, as_name = 'internal_id')
81
+ "elementId(#{alias_name}) AS #{as_name}"
82
+ end
83
+
84
+ def self.id_function
85
+ 'elementId'
86
+ end
87
+
88
+ # Return self as id_handler for compatibility
89
+ def id_handler
90
+ self.class
91
+ end
92
+
64
93
  def execute_cypher(cypher, params = {}, ctx = 'Query')
65
94
  connect
95
+ # Replace adapter-aware placeholder with Neo4j's elementId function
96
+ cypher = cypher.gsub('__NODE_ID__', 'elementId')
97
+
66
98
  session = connection.session # thin wrapper around Bolt::Session
67
99
  result = session.write_transaction do |tx|
68
100
  logger.debug { "[#{ctx}] #{cypher} #{params.inspect}" }
@@ -14,9 +14,18 @@ module ActiveCypher
14
14
  else
15
15
  [model.class.label_name.to_s]
16
16
  end
17
+
18
+ adapter = model.connection.id_handler
19
+
20
+ # OPTIMIZED: Use string template instead of Cyrel for known-safe CREATE pattern
21
+ # Labels come from model class (safe), props are parameterized (safe)
17
22
  label_string = labels.map { |l| ":#{l}" }.join
23
+ cypher = if adapter.id_function == 'elementId'
24
+ "CREATE (n#{label_string} $props) RETURN elementId(n) AS internal_id"
25
+ else
26
+ "CREATE (n#{label_string} $props) RETURN id(n) AS internal_id"
27
+ end
18
28
 
19
- cypher = "CREATE (n#{label_string} $props) RETURN id(n) AS internal_id"
20
29
  data = model.connection.execute_cypher(cypher, { props: props }, 'Create')
21
30
 
22
31
  return false if data.blank? || !data.first.key?(:internal_id)
@@ -40,11 +49,23 @@ module ActiveCypher
40
49
  [model.class.label_name.to_s]
41
50
  end
42
51
 
52
+ adapter = model.connection.id_handler
53
+ # Convert internal_id to its preferred existential format
54
+ # Neo4j wants strings because it's complicated, Memgraph wants integers because it's not
55
+ node_id_param = adapter.id_function == 'elementId' ? model.internal_id.to_s : model.internal_id.to_i
56
+
57
+ # OPTIMIZED: Use string template for known-safe UPDATE pattern
58
+ # Labels come from model class (safe), property names from model attributes (safe)
43
59
  label_string = labels.map { |l| ":#{l}" }.join
44
60
  set_clauses = changes.keys.map { |property| "n.#{property} = $#{property}" }.join(', ')
45
- params = changes.merge(node_id: model.internal_id)
46
61
 
47
- cypher = "MATCH (n#{label_string}) WHERE id(n) = $node_id SET #{set_clauses} RETURN n"
62
+ cypher = if adapter.id_function == 'elementId'
63
+ "MATCH (n#{label_string}) WHERE elementId(n) = $node_id SET #{set_clauses} RETURN n"
64
+ else
65
+ "MATCH (n#{label_string}) WHERE id(n) = $node_id SET #{set_clauses} RETURN n"
66
+ end
67
+
68
+ params = changes.merge(node_id: node_id_param)
48
69
  model.connection.execute_cypher(cypher, params, 'Update')
49
70
 
50
71
  model.send(:changes_applied)
@@ -60,16 +81,23 @@ module ActiveCypher
60
81
  else
61
82
  [model.class.label_name]
62
83
  end
84
+
85
+ adapter = model.connection.id_handler
86
+ # Convert internal_id to whatever format makes the database feel validated
87
+ # It's like therapy, but for graph databases
88
+ node_id_param = adapter.id_function == 'elementId' ? model.internal_id.to_s : model.internal_id.to_i
89
+
90
+ # OPTIMIZED: Use string template for known-safe DELETE pattern
91
+ # Labels come from model class (safe)
63
92
  label_string = labels.map { |l| ":#{l}" }.join
64
93
 
65
- cypher = <<~CYPHER
66
- MATCH (n#{label_string})
67
- WHERE id(n) = $node_id
68
- DETACH DELETE n
69
- RETURN count(*) AS deleted
70
- CYPHER
94
+ cypher = if adapter.id_function == 'elementId'
95
+ "MATCH (n#{label_string}) WHERE elementId(n) = $node_id DETACH DELETE n RETURN count(*) AS deleted"
96
+ else
97
+ "MATCH (n#{label_string}) WHERE id(n) = $node_id DETACH DELETE n RETURN count(*) AS deleted"
98
+ end
71
99
 
72
- result = model.connection.execute_cypher(cypher, { node_id: model.internal_id }, 'Destroy')
100
+ result = model.connection.execute_cypher(cypher, { node_id: node_id_param }, 'Destroy')
73
101
  result.present? && result.first[:deleted].to_i.positive?
74
102
  end
75
103
  end
@@ -9,9 +9,11 @@ module ActiveCypher
9
9
  #
10
10
  # Supported URL prefixes:
11
11
  # - neo4j://
12
+ # - neo4j+s:// (alias for neo4j+ssc://)
12
13
  # - neo4j+ssl://
13
14
  # - neo4j+ssc://
14
15
  # - memgraph://
16
+ # - memgraph+s:// (alias for memgraph+ssc://)
15
17
  # - memgraph+ssl://
16
18
  # - memgraph+ssc://
17
19
  #
@@ -138,10 +140,14 @@ module ActiveCypher
138
140
 
139
141
  return nil unless SUPPORTED_ADAPTERS.include?(adapter)
140
142
 
141
- modifiers = parts.select { |mod| %w[ssl ssc].include?(mod) }
143
+ # Map 's' to 'ssc' for Neo4j compatibility (self-signed certificates)
144
+ mapped_parts = parts.map { |mod| mod == 's' ? 'ssc' : mod }
145
+ modifiers = mapped_parts.select { |mod| %w[ssl ssc].include?(mod) }
142
146
 
143
147
  # If there are parts that are neither the adapter nor valid modifiers, the URL is invalid
144
- remaining_parts = parts - modifiers
148
+ # Check against original parts but also accept 's' as valid
149
+ valid_modifiers = %w[ssl ssc s]
150
+ remaining_parts = parts.reject { |part| valid_modifiers.include?(part) }
145
151
  return nil if remaining_parts.any?
146
152
 
147
153
  [adapter, modifiers]
@@ -17,10 +17,11 @@ module ActiveCypher
17
17
  label_clause = labels.map { |label| "`#{label}`" }.join(':')
18
18
 
19
19
  # Build and fire the CREATE query.
20
- # We use id(n) because elementId(n) lies to us with strings.
20
+ # Ask the adapter how it likes its IDs served - string or integer, sir?
21
+ adapter = conn.id_handler
21
22
  cypher = <<~CYPHER
22
23
  CREATE (n:#{label_clause} $props)
23
- RETURN n, id(n) AS internal_id, properties(n) AS props
24
+ RETURN n, #{adapter.return_node_id('n')}, properties(n) AS props
24
25
  CYPHER
25
26
 
26
27
  result = conn.execute_cypher(cypher, props: props)
@@ -1,11 +1,11 @@
1
1
  development:
2
2
  primary:
3
- url: ENV["GRAPH_URL"]
3
+ url: <%= ENV["GRAPHDB_URL"] %>
4
4
 
5
5
  test:
6
6
  primary:
7
- url: ENV["GRAPH_URL"]
7
+ url: <%= ENV["GRAPHDB_URL"] %>
8
8
 
9
9
  production:
10
10
  primary:
11
- url: ENV["GRAPH_URL"]
11
+ url: <%= ENV["GRAPHDB_URL"] %>
@@ -12,7 +12,11 @@ module ActiveCypher
12
12
  include ActiveCypher::Associations
13
13
  include ActiveCypher::Scoping
14
14
 
15
- attribute :internal_id, :integer
15
+ # internal_id: The ID that proves you exist in the graph's eyes
16
+ # String type because Neo4j needs UUIDs like "4:abc:xyz" to feel special
17
+ # while Memgraph just counts sheep like a normal database.
18
+ # ActiveModel will convert between them, hopefully without existential dread
19
+ attribute :internal_id, :string
16
20
 
17
21
  class_attribute :configurations, instance_accessor: false,
18
22
  default: ActiveSupport::HashWithIndifferentAccess.new
@@ -33,16 +33,25 @@ module ActiveCypher
33
33
  # Find a node by internal DB ID. Returns the record or dies dramatically.
34
34
  # Because sometimes you want to find a node, and sometimes you want to find existential dread.
35
35
  def find(internal_db_id)
36
- internal_db_id = internal_db_id.to_i if internal_db_id.respond_to?(:to_i)
37
36
  node_alias = :n
38
37
 
39
38
  labels = respond_to?(:labels) ? self.labels : [label_name]
40
- Cyrel.match(Cyrel.node(node_alias, labels: labels)).limit(1)
39
+ adapter = connection.id_handler
41
40
  label_string = labels.map { |l| ":#{l}" }.join
41
+
42
+ # Handle ID format based on adapter's preferred flavor of existential crisis
43
+ # Neo4j insists on string IDs like "4:uuid:wtf" because simple integers are for peasants
44
+ # Memgraph keeps it real with numeric IDs because it doesn't need to prove anything
45
+ formatted_id = if adapter.id_function == 'elementId'
46
+ internal_db_id.to_s # String for Neo4j
47
+ else
48
+ internal_db_id.to_i # Numeric for Memgraph
49
+ end
50
+
42
51
  cypher = <<~CYPHER
43
52
  MATCH (#{node_alias}#{label_string})
44
- WHERE id(#{node_alias}) = #{internal_db_id}
45
- RETURN #{node_alias}, id(#{node_alias}) AS internal_id
53
+ WHERE #{adapter.node_id_equals_value(node_alias, formatted_id)}
54
+ RETURN #{node_alias}, #{adapter.return_node_id(node_alias)}
46
55
  LIMIT 1
47
56
  CYPHER
48
57
 
@@ -141,8 +141,8 @@ module ActiveCypher
141
141
  end
142
142
 
143
143
  Cyrel
144
- .match(Cyrel.node(node_alias, labels: labels))
145
- .return_(node_alias, Cyrel.element_id(node_alias).as(:internal_id))
144
+ .match(Cyrel.node(node_alias, *labels))
145
+ .return_(node_alias, Cyrel.node_id(node_alias).as(:internal_id))
146
146
  end
147
147
 
148
148
  # Actually loads the records from the database, shattering the illusion of laziness.
@@ -50,7 +50,10 @@ module ActiveCypher
50
50
  # --------------------------------------------------------------
51
51
  # Attributes
52
52
  # --------------------------------------------------------------
53
- attribute :internal_id, :integer
53
+ # internal_id: Your relationship's social security number, but less secure
54
+ # String because Neo4j relationships have commitment issues and need complex IDs
55
+ # Memgraph relationships just want a simple number, like the good old days with MS Access.
56
+ attribute :internal_id, :string
54
57
 
55
58
  # --------------------------------------------------------------
56
59
  # Connection fallback
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveCypher
4
- VERSION = '0.7.2'
4
+ VERSION = '0.8.0'
5
5
 
6
6
  def self.gem_version
7
7
  Gem::Version.new VERSION
data/lib/activecypher.rb CHANGED
@@ -104,8 +104,10 @@ loader.ignore("#{__dir__}/activecypher.rb")
104
104
  loader.ignore("#{__dir__}/cyrel.rb")
105
105
  loader.inflector.inflect(
106
106
  'activecypher' => 'ActiveCypher',
107
- 'dsl_context' => 'DSLContext'
107
+ 'dsl_context' => 'DSLContext',
108
+ 'ast' => 'AST'
108
109
  )
110
+
109
111
  loader.push_dir("#{__dir__}/cyrel", namespace: Cyrel)
110
112
  loader.setup
111
113
 
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyrel
4
+ module AST
5
+ # AST node for CALL clauses (procedures)
6
+ # For when you need to call upon the powers that be
7
+ class CallNode < Node
8
+ attr_reader :procedure_name, :arguments, :yield_items
9
+
10
+ def initialize(procedure_name, arguments: [], yield_items: nil)
11
+ @procedure_name = procedure_name
12
+ @arguments = arguments
13
+ @yield_items = yield_items
14
+ end
15
+
16
+ protected
17
+
18
+ def state
19
+ [@procedure_name, @arguments, @yield_items]
20
+ end
21
+ end
22
+
23
+ # AST node for CALL subqueries
24
+ # For when you need a query within a query (queryception)
25
+ class CallSubqueryNode < Node
26
+ attr_reader :subquery
27
+
28
+ def initialize(subquery)
29
+ @subquery = subquery
30
+ end
31
+
32
+ protected
33
+
34
+ def state
35
+ [@subquery]
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyrel
4
+ module AST
5
+ # Adapter that allows AST nodes to work with the existing clause-based system
6
+ # Now with simple caching for performance
7
+ class ClauseAdapter < Clause::Base
8
+ attr_reader :ast_node
9
+
10
+ def initialize(ast_node)
11
+ @ast_node = ast_node
12
+ @ast_node_hash = ast_node.hash
13
+ super()
14
+ end
15
+
16
+ def render(query)
17
+ # Use a simple cache key based on AST node structure
18
+ cache_key = [@ast_node_hash, @ast_node.class.name].join(':')
19
+
20
+ SimpleCache.instance.fetch(cache_key) do
21
+ # Create a compiler that delegates parameter registration to the query
22
+ compiler = QueryIntegratedCompiler.new(query)
23
+ compiler.compile(ast_node)
24
+ compiler.output.string
25
+ end
26
+ end
27
+
28
+ # Ruby 3.0+ pattern matching support
29
+ def deconstruct
30
+ [ast_node]
31
+ end
32
+
33
+ def deconstruct_keys(_keys)
34
+ { ast_node: ast_node, type: ast_node.class }
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyrel
4
+ module AST
5
+ # Base class for clause nodes (MATCH, WHERE, RETURN, etc.)
6
+ # The building blocks of your query, like LEGO but with more existential dread
7
+ class ClauseNode < Node
8
+ end
9
+ end
10
+ end