activecypher 0.7.2 → 0.7.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 738573111aeb926ef359c2bd2643e07d25f83b76d63f7cebf0d31e762c06fabf
4
- data.tar.gz: 8ede3884325f7e86e4ca06dea636e0193d8742fc19b0ed2e92724f3270f31f7f
3
+ metadata.gz: bcfe9f62e0fdcf58b7d4f46fef0b7869bac550fc730f2e93a2d057938cad8a4e
4
+ data.tar.gz: a835e25b6ff10e6e9e4884c2855091e617086e1635931988cc018d9eac06afd8
5
5
  SHA512:
6
- metadata.gz: afc1446c1c2e7195ed3ad241e84bcdaffaf9c634084068088f15137f897ea4955d80448af5b80b225af204a23baa217486b4327160b50b7bd41899d7c6b95df1
7
- data.tar.gz: f5cf3960f8127672e803d9dd9d2246e8245499e2f7208013e2bd97a4e5971fbcd5044aa107bcf1208a21cfc40337d0c89cbc3354706dce06f0bb1df8b62d84a3
6
+ metadata.gz: 4297ab6127aa159da2800dd2024102e8768cfac6604ce390fa35d71d95b393e3226a6d544e535ccc09c14d1632a23009c48df5067a62b6f158fc344b7e1ff21c
7
+ data.tar.gz: b929133db5f0be22b7fbd2e3923e54b0a6b041e748d28dc32bbd6f454ab0547ea48637a945262b480339ccee8e78551b23b64cd6aea8a276c5f3d5e766adbc8d
@@ -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
 
@@ -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}" }
@@ -16,7 +16,8 @@ module ActiveCypher
16
16
  end
17
17
  label_string = labels.map { |l| ":#{l}" }.join
18
18
 
19
- cypher = "CREATE (n#{label_string} $props) RETURN id(n) AS internal_id"
19
+ adapter = model.connection.id_handler
20
+ cypher = "CREATE (n#{label_string} $props) RETURN #{adapter.return_node_id('n')}"
20
21
  data = model.connection.execute_cypher(cypher, { props: props }, 'Create')
21
22
 
22
23
  return false if data.blank? || !data.first.key?(:internal_id)
@@ -42,9 +43,14 @@ module ActiveCypher
42
43
 
43
44
  label_string = labels.map { |l| ":#{l}" }.join
44
45
  set_clauses = changes.keys.map { |property| "n.#{property} = $#{property}" }.join(', ')
45
- params = changes.merge(node_id: model.internal_id)
46
46
 
47
- cypher = "MATCH (n#{label_string}) WHERE id(n) = $node_id SET #{set_clauses} RETURN n"
47
+ adapter = model.connection.id_handler
48
+ # Convert internal_id to its preferred existential format
49
+ # Neo4j wants strings because it's complicated, Memgraph wants integers because it's not
50
+ node_id_param = adapter.id_function == 'elementId' ? model.internal_id.to_s : model.internal_id.to_i
51
+ params = changes.merge(node_id: node_id_param)
52
+
53
+ cypher = "MATCH (n#{label_string}) WHERE #{adapter.node_id_where('n', 'node_id')} SET #{set_clauses} RETURN n"
48
54
  model.connection.execute_cypher(cypher, params, 'Update')
49
55
 
50
56
  model.send(:changes_applied)
@@ -62,14 +68,19 @@ module ActiveCypher
62
68
  end
63
69
  label_string = labels.map { |l| ":#{l}" }.join
64
70
 
71
+ adapter = model.connection.id_handler
72
+ # Convert internal_id to whatever format makes the database feel validated
73
+ # It's like therapy, but for graph databases
74
+ node_id_param = adapter.id_function == 'elementId' ? model.internal_id.to_s : model.internal_id.to_i
75
+
65
76
  cypher = <<~CYPHER
66
77
  MATCH (n#{label_string})
67
- WHERE id(n) = $node_id
78
+ WHERE #{adapter.node_id_where('n', 'node_id')}
68
79
  DETACH DELETE n
69
80
  RETURN count(*) AS deleted
70
81
  CYPHER
71
82
 
72
- result = model.connection.execute_cypher(cypher, { node_id: model.internal_id }, 'Destroy')
83
+ result = model.connection.execute_cypher(cypher, { node_id: node_id_param }, 'Destroy')
73
84
  result.present? && result.first[:deleted].to_i.positive?
74
85
  end
75
86
  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
 
@@ -142,7 +142,7 @@ module ActiveCypher
142
142
 
143
143
  Cyrel
144
144
  .match(Cyrel.node(node_alias, labels: labels))
145
- .return_(node_alias, Cyrel.element_id(node_alias).as(:internal_id))
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.7.3'
5
5
 
6
6
  def self.gem_version
7
7
  Gem::Version.new VERSION
@@ -18,14 +18,25 @@ module Cyrel
18
18
  # --- Common Cypher Functions ---
19
19
 
20
20
  # Use elementId() instead of deprecated id()
21
+ # @deprecated Use {#node_id} instead for adapter-aware ID handling
21
22
  def element_id(node_variable)
22
23
  Expression::FunctionCall.new(:elementId, Clause::Return::RawIdentifier.new(node_variable.to_s))
23
24
  end
24
25
 
26
+ # @deprecated Use {#node_id} instead for adapter-aware ID handling
25
27
  def id(node_variable)
26
28
  Expression::FunctionCall.new(:id, Clause::Return::RawIdentifier.new(node_variable.to_s))
27
29
  end
28
30
 
31
+ # Adapter-aware node ID function
32
+ # Generates a placeholder that will be replaced by the correct ID function
33
+ # at execution time based on the database adapter (Neo4j vs Memgraph)
34
+ # Because databases can't agree on how to name their ID functions,
35
+ # we'll just pretend they're all the same and fix it later
36
+ def node_id(node_variable)
37
+ Expression::FunctionCall.new(:__NODE_ID__, Clause::Return::RawIdentifier.new(node_variable.to_s))
38
+ end
39
+
29
40
  # Because apparently, COUNT(*) isn’t obvious enough.
30
41
  # Handles the 'give me everything and make it snappy' use case.
31
42
  def count(expression, distinct: false)
data/lib/cyrel.rb CHANGED
@@ -35,11 +35,15 @@ module Cyrel
35
35
 
36
36
  # Cyrel DSL helper: returns the element id of a node/relationship.
37
37
  # Example: Cyrel.id(:n)
38
- def id(...) = Functions.element_id(...)
38
+ def id(...) = Functions.id(...)
39
39
 
40
40
  # Cyrel DSL helper: returns the element id of a node/relationship (alias).
41
41
  def element_id(...) = Functions.element_id(...)
42
42
 
43
+ # Cyrel DSL helper: adapter-aware node ID function
44
+ # Example: Cyrel.node_id(:n)
45
+ def node_id(...) = Functions.node_id(...)
46
+
43
47
  # Cyrel DSL helper: Cypher count() aggregation.
44
48
  # Example: Cyrel.count(:n)
45
49
  def count(...) = Functions.count(...)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activecypher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 0.7.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih