activecypher 0.3.0 → 0.6.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/lib/active_cypher/base.rb +28 -8
  3. data/lib/active_cypher/bolt/driver.rb +6 -16
  4. data/lib/active_cypher/bolt/session.rb +62 -50
  5. data/lib/active_cypher/bolt/transaction.rb +95 -90
  6. data/lib/active_cypher/connection_adapters/abstract_adapter.rb +28 -0
  7. data/lib/active_cypher/connection_adapters/abstract_bolt_adapter.rb +40 -32
  8. data/lib/active_cypher/connection_adapters/memgraph_adapter.rb +44 -0
  9. data/lib/active_cypher/connection_adapters/neo4j_adapter.rb +27 -1
  10. data/lib/active_cypher/connection_adapters/registry.rb +94 -0
  11. data/lib/active_cypher/connection_handler.rb +18 -3
  12. data/lib/active_cypher/connection_pool.rb +5 -23
  13. data/lib/active_cypher/connection_url_resolver.rb +14 -3
  14. data/lib/active_cypher/cypher_config.rb +2 -1
  15. data/lib/active_cypher/fixtures/dsl_context.rb +41 -0
  16. data/lib/active_cypher/fixtures/evaluator.rb +37 -0
  17. data/lib/active_cypher/fixtures/node_builder.rb +53 -0
  18. data/lib/active_cypher/fixtures/parser.rb +23 -0
  19. data/lib/active_cypher/fixtures/registry.rb +43 -0
  20. data/lib/active_cypher/fixtures/rel_builder.rb +96 -0
  21. data/lib/active_cypher/fixtures.rb +177 -0
  22. data/lib/active_cypher/generators/node_generator.rb +32 -3
  23. data/lib/active_cypher/generators/relationship_generator.rb +29 -2
  24. data/lib/active_cypher/generators/templates/application_graph_node.rb +1 -0
  25. data/lib/active_cypher/generators/templates/node.rb.erb +10 -6
  26. data/lib/active_cypher/generators/templates/relationship.rb.erb +7 -6
  27. data/lib/active_cypher/instrumentation.rb +186 -0
  28. data/lib/active_cypher/model/callbacks.rb +5 -13
  29. data/lib/active_cypher/model/connection_handling.rb +37 -52
  30. data/lib/active_cypher/model/connection_owner.rb +41 -33
  31. data/lib/active_cypher/model/core.rb +4 -12
  32. data/lib/active_cypher/model/countable.rb +10 -3
  33. data/lib/active_cypher/model/destruction.rb +23 -18
  34. data/lib/active_cypher/model/labelling.rb +45 -0
  35. data/lib/active_cypher/model/persistence.rb +52 -26
  36. data/lib/active_cypher/model/querying.rb +49 -25
  37. data/lib/active_cypher/railtie.rb +40 -5
  38. data/lib/active_cypher/relation.rb +10 -2
  39. data/lib/active_cypher/relationship.rb +77 -17
  40. data/lib/active_cypher/version.rb +1 -1
  41. data/lib/activecypher.rb +4 -1
  42. data/lib/cyrel/clause/set.rb +20 -10
  43. data/lib/cyrel/expression/property_access.rb +2 -0
  44. data/lib/cyrel/functions.rb +3 -1
  45. data/lib/cyrel/logging.rb +43 -0
  46. data/lib/cyrel/plus.rb +11 -0
  47. data/lib/cyrel/query.rb +7 -1
  48. data/lib/cyrel.rb +77 -18
  49. metadata +13 -2
  50. data/lib/active_cypher/connection_factory.rb +0 -130
@@ -3,48 +3,56 @@
3
3
  module ActiveCypher
4
4
  module Model
5
5
  # Mixin for anything that “owns” a connection (nodes, relationships, maybe
6
- # graph‑level service objects later). 100 % framework‑agnostic.
7
- # @note Because every object wants to feel important by "owning" something, even if it's just a connection.
8
- # Moroccan black magick and Ruby sorcery may be involved in keeping these connections alive.
6
+ # graph‑level service objects later). 100 % framework‑agnostic.
9
7
  module ConnectionOwner
10
8
  extend ActiveSupport::Concern
11
9
  include ActiveCypher::Logging
12
10
  include ActiveCypher::Model::ConnectionHandling
13
11
 
14
- included do
15
- # Every class gets its own adapter slot (overridden by establish_connection)
16
- # Because nothing says "flexibility" like a class variable you'll forget exists.
17
- # This is where the witchcraft happens: sometimes the right connection just appears.
18
- cattr_accessor :connection, instance_accessor: false
19
- end
20
-
21
12
  class_methods do
22
- delegate :current_role, :current_shard,
23
- to: ActiveCypher::RuntimeRegistry
24
-
25
13
  # One handler for all subclasses that include this concern
26
- # Because sharing is caring, except when it comes to connection pools.
27
- # Summoned by Ruby wizardry: this handler is conjured once and shared by all.
28
- @@connection_handler ||= ActiveCypher::ConnectionHandler.new # rubocop:disable Style/ClassVars
29
- def connection_handler = @@connection_handler
30
-
31
- # Temporarily switches the current role and shard for the duration of the block.
32
- # @param role [Symbol, nil] The role to switch to
33
- # @param shard [Symbol] The shard to switch to
34
- # @yield The block to execute with the new context
35
- # @note Because sometimes you just want to pretend you're connected to something else for a while.
36
- # Warning: If you switch too often, you may summon unexpected spirits from the Ruby shadow dimension.
37
- def connected_to(role: nil, shard: :default)
38
- previous_role = current_role
39
- previous_shard = current_shard
40
- ActiveCypher::RuntimeRegistry.current_role = role || previous_role
41
- ActiveCypher::RuntimeRegistry.current_shard = shard || previous_shard
42
- yield
43
- ensure
44
- ActiveCypher::RuntimeRegistry.current_role = previous_role
45
- ActiveCypher::RuntimeRegistry.current_shard = previous_shard
14
+ def connection_handler
15
+ if defined?(@connection_handler) && @connection_handler
16
+ @connection_handler
17
+ elsif superclass.respond_to?(:connection_handler)
18
+ superclass.connection_handler
19
+ else
20
+ @connection_handler = ActiveCypher::ConnectionHandler.new
21
+ end
22
+ end
23
+
24
+ # Returns the adapter class being used by this model
25
+ def adapter_class
26
+ connection&.class
27
+ end
28
+
29
+ # Always dynamically fetch the connection for the current db_key
30
+ def connection
31
+ handler = connection_handler
32
+
33
+ if respond_to?(:connects_to_mappings) && connects_to_mappings.is_a?(Hash)
34
+ db_key = connects_to_mappings[:writing] # Default to :writing mapping
35
+ if db_key && (pool = handler.pool(db_key))
36
+ return pool.connection
37
+ end
38
+ end
39
+
40
+ return superclass.connection if superclass.respond_to?(:connection)
41
+
42
+ raise ActiveCypher::ConnectionNotEstablished,
43
+ "No connection pool found for #{name}, db_key=#{db_key.inspect}"
46
44
  end
47
45
  end
46
+
47
+ # Instance method to access the adapter class
48
+ def adapter_class
49
+ self.class.adapter_class
50
+ end
51
+
52
+ # Instance method to access the connection dynamically
53
+ def connection
54
+ self.class.connection
55
+ end
48
56
  end
49
57
  end
50
58
  end
@@ -1,27 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_model'
4
-
5
3
  module ActiveCypher
6
4
  module Model
7
- # @!parse
8
- # # Core: The module that tries to make your graph model feel like it belongs in a relational world.
9
- # # Includes every concern under the sun, because why have one abstraction when you can have twelve?
10
- # # Most of this works thanks to a little Ruby sorcery, a dash of witchcraft, and—on rare occasions—some unexplained back magick.
5
+ # Core: The module that tries to make your graph model feel like it belongs in a relational world.
6
+ # Includes every concern under the sun, because why have one abstraction when you can have twelve?
7
+ # Most of this works thanks to a little Ruby sorcery, a dash of witchcraft, and—on rare occasions—some unexplained back magick.
11
8
  module Core
12
9
  extend ActiveSupport::Concern
13
10
 
14
11
  included do
15
- include ActiveModel::API
16
- include ActiveModel::Attributes
17
- include ActiveModel::Dirty
18
12
  include ActiveCypher::Associations
19
13
  include ActiveCypher::Scoping
20
- include ActiveModel::Validations
21
14
 
22
- attribute :internal_id, :string
15
+ attribute :internal_id, :integer
23
16
 
24
- cattr_accessor :connection, instance_accessor: false
25
17
  class_attribute :configurations, instance_accessor: false,
26
18
  default: ActiveSupport::HashWithIndifferentAccess.new
27
19
  end
@@ -16,9 +16,16 @@ module ActiveCypher
16
16
  # If this returns the right number, thank the database gods—or maybe just the back magick hiding in the adapter.
17
17
  def count
18
18
  cypher, params =
19
- if respond_to?(:label_name) # ⇒ node class
20
- ["MATCH (n:#{label_name}) RETURN count(n) AS c", {}]
21
- else # relationship class
19
+ if respond_to?(:label_name) # ⇒ node class
20
+ if respond_to?(:labels) && labels.any?
21
+ # Use all custom labels for COUNT operations
22
+ label_string = labels.map { |l| ":#{l}" }.join
23
+ ["MATCH (n#{label_string}) RETURN count(n) AS c", {}]
24
+ else
25
+ # Fall back to primary label
26
+ ["MATCH (n:#{label_name}) RETURN count(n) AS c", {}]
27
+ end
28
+ else # ⇒ relationship class
22
29
  ["MATCH ()-[r:#{relationship_type}]-() RETURN count(r) AS c", {}] # ▲ undirected
23
30
  end
24
31
 
@@ -2,9 +2,8 @@
2
2
 
3
3
  module ActiveCypher
4
4
  module Model
5
- # @!parse
6
- # # Destruction: The module that lets you banish records from existence with a single incantation.
7
- # # Uses a blend of Ruby sorcery, a dash of witchcraft, and—on rare occasions—some back magick when nothing else will do.
5
+ # Destruction: The module that lets you banish records from existence with a single incantation.
6
+ # Uses a blend of Ruby sorcery, a dash of witchcraft, and—on rare occasions—some back magick when nothing else will do.
8
7
  module Destruction
9
8
  extend ActiveSupport::Concern
10
9
 
@@ -13,7 +12,6 @@ module ActiveCypher
13
12
  # Runs a Cypher `DETACH DELETE` query on the node.
14
13
  # Freezes the object to prevent further use, as a kind of ceremonial burial.
15
14
  # Because nothing says "closure" like a frozen Ruby object.
16
- # If this works and you can't explain why, that's probably back magick.
17
15
  #
18
16
  # @raise [RuntimeError] if the record is new or already destroyed.
19
17
  # @return [Boolean] true if the record was successfully destroyed, false if something caught on fire.
@@ -22,23 +20,30 @@ module ActiveCypher
22
20
  raise 'Cannot destroy a new record' if new_record?
23
21
  raise 'Record already destroyed' if destroyed?
24
22
 
25
- n = :n
26
- query = Cyrel.match(Cyrel.node(self.class.label_name).as(n))
27
- .where(Cyrel.id(n).eq(internal_id))
28
- .detach_delete(n)
23
+ n = :n
24
+ labels = self.class.respond_to?(:labels) ? self.class.labels : [self.class.label_name]
25
+ label_string = labels.map { |l| ":#{l}" }.join
29
26
 
30
- cypher = query.to_cypher
31
- params = { id: internal_id }
27
+ cypher = <<~CYPHER
28
+ MATCH (#{n}#{label_string})
29
+ WHERE id(#{n}) = #{internal_id}
30
+ DETACH DELETE #{n}
31
+ RETURN count(*) AS deleted
32
+ CYPHER
32
33
 
33
- # Here lies the true sorcery: one line to erase a node from existence.
34
- # If the database still remembers it, you may need to consult your local witch.
35
- self.class.connection.execute_cypher(cypher, params, 'Destroy')
36
- @destroyed = true
37
- freeze # To make sure you can't Frankenstein it back to life. Lightning not included.
38
- true
34
+ result = self.class.connection.execute_cypher(cypher, {}, 'Destroy')
35
+
36
+ if result.present? && result.first[:deleted].to_i > 0
37
+ @destroyed = true
38
+ freeze
39
+ true
40
+ else
41
+ false
42
+ end
39
43
  end
40
- rescue StandardError
41
- false # Something went wrong. Don’t ask. Just walk away. Or blame the database, that's always fun. If it keeps happening, suspect back magick.
44
+ rescue StandardError => e
45
+ warn "[Destruction] Destroy failed: #{e.class}: #{e.message}"
46
+ false
42
47
  end
43
48
 
44
49
  # Returns true if this object has achieved full existential closure.
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveCypher
4
+ module Model
5
+ module Labelling
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ # Use array instead of set to preserve insertion order of labels
10
+ class_attribute :custom_labels, default: []
11
+ end
12
+
13
+ class_methods do
14
+ # Define a label for the model. Can be called multiple times to add multiple labels.
15
+ # @param label_name [Symbol, String] The label name
16
+ # @return [Array] The collection of custom labels
17
+ def label(label_name)
18
+ label_sym = label_name.to_sym
19
+ self.custom_labels = custom_labels.dup << label_sym unless custom_labels.include?(label_sym)
20
+ custom_labels
21
+ end
22
+
23
+ # Get all labels for this model
24
+ # @return [Array<Symbol>] All labels for this model
25
+ def labels
26
+ custom_labels.empty? ? [default_label] : custom_labels
27
+ end
28
+
29
+ # Returns the primary label for the model
30
+ # @return [Symbol] The primary label
31
+ def label_name
32
+ custom_labels.any? ? custom_labels.first : default_label
33
+ end
34
+
35
+ # Computes the default label for the model based on class name
36
+ # Strips 'Node' or 'Record' suffix, returns as symbol, capitalized
37
+ def default_label
38
+ base = name.split('::').last
39
+ base = base.sub(/(Node|Record)\z/, '')
40
+ base.to_sym
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -14,10 +14,13 @@ module ActiveCypher
14
14
  # If it already exists, we patch up its regrets.
15
15
  # If it fails, we return false, like cowards.
16
16
  #
17
+ # @param validate [Boolean] whether to run validations (default: true)
17
18
  # @return [Boolean] true if saved successfully, false if the database ghosted us.
18
19
  # Because nothing says "robust" like pretending persistence is easy.
19
20
  # If this works and you can't explain why, that's probably back magick.
20
- def save
21
+ def save(validate: true)
22
+ return false if validate && !valid?
23
+
21
24
  # before_/after_create
22
25
  _run(:save) do
23
26
  if new_record?
@@ -112,17 +115,6 @@ module ActiveCypher
112
115
 
113
116
  private
114
117
 
115
- # # Internal method to initialize a record from the database
116
- # # @param attributes [Hash] The attributes from the database
117
- # # @return [self]
118
- # def init_with_attributes(attrs)
119
- # binding.irb
120
- # assign_attributes(**attrs) if attrs
121
- # @new_record = false # Mark as not new when loading from DB
122
- # clear_changes_information
123
- # self
124
- # end
125
-
126
118
  # Creates the record in the database using Cypher.
127
119
  #
128
120
  # @return [Boolean] true if the database accepted your offering.
@@ -131,16 +123,35 @@ module ActiveCypher
131
123
  def create_record
132
124
  props = attributes_for_persistence
133
125
  n = :n
134
- label = self.class.label_name.to_s
135
- node = Cyrel.node(n, labels: [label], properties: props)
136
- query = Cyrel.create(node).return_(Cyrel.element_id(n).as(:internal_id))
137
- cypher, params = query.to_cypher
138
- params ||= {}
139
126
 
140
- data = self.class.connection.execute_cypher(cypher, params, 'Create')
127
+ # Use all labels for database operations
128
+ labels = self.class.respond_to?(:labels) ? self.class.labels : [self.class.label_name.to_s]
129
+
130
+ # For Memgraph, construct direct Cypher query
131
+ label_string = labels.map { |l| ":#{l}" }.join
132
+
133
+ # Handle properties for Cypher query
134
+ props_str = props.map do |k, v|
135
+ value_str = if v.nil?
136
+ 'NULL'
137
+ elsif v.is_a?(String)
138
+ "'#{v.gsub("'", "\\\\'")}'"
139
+ elsif v.is_a?(Numeric) || v.is_a?(TrueClass) || v.is_a?(FalseClass)
140
+ v.to_s
141
+ else
142
+ "'#{v.to_s.gsub("'", "\\\\'")}'"
143
+ end
144
+ "#{k}: #{value_str}"
145
+ end.join(', ')
146
+
147
+ cypher = "CREATE (#{n}#{label_string} {#{props_str}}) " \
148
+ "RETURN id(#{n}) AS internal_id"
149
+
150
+ data = self.class.connection.execute_cypher(cypher, {}, 'Create')
151
+
141
152
  return false if data.blank? || !data.first.key?(:internal_id)
142
153
 
143
- self.internal_id = data.first[:internal_id].to_s
154
+ self.internal_id = data.first[:internal_id]
144
155
  @new_record = false
145
156
  changes_applied
146
157
  true
@@ -165,15 +176,30 @@ module ActiveCypher
165
176
  changes = changes_to_save
166
177
  return true if changes.empty?
167
178
 
168
- n = :n
169
- query = Cyrel.match(Cyrel.node(self.class.label_name).as(n))
170
- .where(Cyrel.id(n).eq(internal_id))
171
- .set(n => changes)
179
+ # Use all labels for database operations
180
+ labels = self.class.respond_to?(:labels) ? self.class.labels : [self.class.label_name]
181
+
182
+ label_string = labels.map { |l| ":#{l}" }.join
183
+ set_clauses = changes.map do |property, value|
184
+ # Handle different value types appropriately
185
+ if value.nil?
186
+ "n.#{property} = NULL"
187
+ elsif value.is_a?(String)
188
+ "n.#{property} = '#{value.gsub("'", "\\\\'")}'"
189
+ elsif value.is_a?(Numeric) || value.is_a?(TrueClass) || value.is_a?(FalseClass)
190
+ "n.#{property} = #{value}"
191
+ else
192
+ "n.#{property} = '#{value.to_s.gsub("'", "\\\\'")}'"
193
+ end
194
+ end.join(', ')
195
+
196
+ cypher = "MATCH (n#{label_string}) " \
197
+ "WHERE id(n) = #{internal_id} " \
198
+ "SET #{set_clauses} " \
199
+ 'RETURN n'
172
200
 
173
- cypher, params = query.to_cypher
174
- params ||= {}
201
+ self.class.connection.execute_cypher(cypher, {}, 'Update')
175
202
 
176
- self.class.connection.execute_cypher(cypher, params, 'Update')
177
203
  changes_applied
178
204
  true
179
205
  end
@@ -2,19 +2,14 @@
2
2
 
3
3
  module ActiveCypher
4
4
  module Model
5
- # @!parse
6
- # # Querying: The module that lets you pretend your graph is just a really weird table.
7
- # # Because what’s more fun than chaining scopes and pretending you’re not writing Cypher by hand?
5
+ # Querying: The module that lets you pretend your graph is just a really weird table.
6
+ # Because what's more fun than chaining scopes and pretending you're not writing Cypher by hand?
8
7
  module Querying
9
8
  extend ActiveSupport::Concern
10
9
 
11
10
  class_methods do
12
- # -- default label -----------------------------------------------
13
- # Returns the symbolic label name for the model, e.g., :user_node
14
- # @return [Symbol] The label name for the model
15
- def label_name = model_name.element.to_sym
11
+ # -- Basic Query Builders ----------------------------------------
16
12
 
17
- # -- basic query builders ----------------------------------------
18
13
  # Return a base Relation, applying the default scope if it exists
19
14
  # @return [Relation] The base relation for the model
20
15
  # Because nothing says "default" like a scope you forgot you set.
@@ -29,31 +24,38 @@ module ActiveCypher
29
24
  # @return [Relation]
30
25
  def where(conditions) = all.where(conditions)
31
26
 
32
- # @param val [Integer] The limit value
33
- # @return [Relation]
34
- def limit(val) = all.limit(val)
27
+ def limit(val) = all.limit(val)
35
28
 
36
- # @return [Relation]
37
- def order(*) = all.order(*)
29
+ def order(*) = all.order(*)
30
+
31
+ # -- find / create ------------------------------------------------
38
32
 
39
- # -- find / create -----------------------------------------------
40
33
  # Find a node by internal DB ID. Returns the record or dies dramatically.
41
- # @param internal_db_id [String] The internal database ID
42
- # @return [Object] The found record
43
- # @raise [ActiveCypher::RecordNotFound] if not found
44
34
  # Because sometimes you want to find a node, and sometimes you want to find existential dread.
45
35
  def find(internal_db_id)
36
+ internal_db_id = internal_db_id.to_i if internal_db_id.respond_to?(:to_i)
46
37
  node_alias = :n
47
38
 
48
- query = Cyrel
49
- .match(Cyrel.node(label_name).as(node_alias))
50
- .where(Cyrel.element_id(node_alias).eq(internal_db_id))
51
- .return_(node_alias, Cyrel.element_id(node_alias).as(:internal_id))
52
- .limit(1)
39
+ labels = respond_to?(:labels) ? self.labels : [label_name]
40
+ Cyrel.match(Cyrel.node(node_alias, labels: labels)).limit(1)
41
+ label_string = labels.map { |l| ":#{l}" }.join
42
+ cypher = <<~CYPHER
43
+ MATCH (#{node_alias}#{label_string})
44
+ WHERE id(#{node_alias}) = #{internal_db_id}
45
+ RETURN #{node_alias}, id(#{node_alias}) AS internal_id
46
+ LIMIT 1
47
+ CYPHER
48
+
49
+ result = connection.execute_cypher(cypher)
50
+ record = result.first
51
+
52
+ if record
53
+ attrs = _hydrate_attributes_from_memgraph_record(record, node_alias)
54
+ return instantiate(attrs)
55
+ end
53
56
 
54
- Relation.new(self, query).first or
55
- raise ActiveCypher::RecordNotFound,
56
- "#{name} with internal_id=#{internal_db_id.inspect} not found"
57
+ raise ActiveCypher::RecordNotFound,
58
+ "#{name} with internal_id=#{internal_db_id.inspect} not found. Perhaps it's in another castle, or just being 'graph'-ty."
57
59
  end
58
60
 
59
61
  # Instantiates and immediately saves a new record. YOLO mode.
@@ -61,6 +63,28 @@ module ActiveCypher
61
63
  # @return [Object] The new, possibly persisted record
62
64
  # Because sometimes you just want to live dangerously.
63
65
  def create(attrs = {}) = new(attrs).tap(&:save)
66
+
67
+ private
68
+
69
+ def _hydrate_attributes_from_memgraph_record(record, node_alias)
70
+ attrs = {}
71
+ node_data = record[node_alias] || record[node_alias.to_s]
72
+
73
+ if node_data.is_a?(Array) && node_data.length >= 2
74
+ properties_container = node_data[1]
75
+ if properties_container.is_a?(Array) && properties_container.length >= 3
76
+ properties = properties_container[2]
77
+ properties.each { |k, v| attrs[k.to_sym] = v } if properties.is_a?(Hash)
78
+ end
79
+ elsif node_data.is_a?(Hash)
80
+ node_data.each { |k, v| attrs[k.to_sym] = v }
81
+ elsif node_data.respond_to?(:properties)
82
+ attrs = node_data.properties.symbolize_keys
83
+ end
84
+
85
+ attrs[:internal_id] = record[:internal_id] || record['internal_id']
86
+ attrs
87
+ end
64
88
  end
65
89
  end
66
90
  end
@@ -16,12 +16,47 @@ module ActiveCypher
16
16
  end
17
17
 
18
18
  initializer 'active_cypher.load_multi_db' do |_app|
19
- configs = ActiveCypher::CypherConfig.for('*') # entire merged hash
20
- %i[writing reading analytics].each do |role|
21
- next unless (cfg = configs[role])
19
+ configs = ActiveCypher::CypherConfig.for('*')
22
20
 
23
- pool = ActiveCypher::ConnectionPool.new(cfg)
24
- ActiveCypher::Base.connection_handler.set(role, :default, pool)
21
+ # First, create pools for all configurations
22
+ connection_pools = {}
23
+ configs.each do |name, cfg|
24
+ connection_pools[name.to_sym] = ActiveCypher::ConnectionPool.new(cfg)
25
+ end
26
+
27
+ # Register all pools under their own names with the database key
28
+ connection_pools.each do |name, pool|
29
+ # Store the pool with db_key only
30
+ ActiveCypher::Base.connection_handler.set(name, pool)
31
+ end
32
+
33
+ # Register default roles (writing, reading)
34
+ # Use primary pool if available, otherwise use the first pool
35
+ default_db_key = :primary
36
+ default_pool = connection_pools[:primary] || connection_pools.values.first
37
+ default_db_key = connection_pools.keys.first unless connection_pools.key?(:primary)
38
+
39
+ if default_pool
40
+ # Store the default pool with db_key only
41
+ ActiveCypher::Base.connection_handler.set(default_db_key, default_pool)
42
+ end
43
+
44
+ # Find all abstract node base classes with connects_to mappings
45
+ ObjectSpace.each_object(Class) do |klass|
46
+ next unless klass < ActiveCypher::Base
47
+ next unless klass.respond_to?(:abstract_class?) && klass.abstract_class?
48
+ next unless klass.respond_to?(:connects_to_mappings) && klass.connects_to_mappings.present?
49
+
50
+ # Register pools for each role in connects_to mapping
51
+ klass.connects_to_mappings.each_value do |conn_name|
52
+ conn_name = conn_name.to_s.to_sym
53
+ pool = connection_pools[conn_name]
54
+ next unless pool
55
+
56
+ # Only create a new pool if one doesn't exist for this role and db_key combination
57
+ # Format: set(db_key, role, shard, pool)
58
+ ActiveCypher::Base.connection_handler.set(conn_name, pool) unless ActiveCypher::Base.connection_handler.pool(conn_name)
59
+ end
25
60
  end
26
61
  end
27
62
 
@@ -129,11 +129,19 @@ module ActiveCypher
129
129
  # Because writing Cypher by hand is for people with too much free time.
130
130
  # @return [Object] The default Cyrel query
131
131
  def default_query
132
- label = model_class.model_name.element
133
132
  node_alias = :n
134
133
 
134
+ # Use all labels if available, otherwise fall back to primary label
135
+ labels = if model_class.respond_to?(:labels)
136
+ model_class.labels
137
+ elsif model_class.respond_to?(:label_name)
138
+ [model_class.label_name]
139
+ else
140
+ [model_class.model_name.element.to_sym]
141
+ end
142
+
135
143
  Cyrel
136
- .match(Cyrel.node(label).as(node_alias))
144
+ .match(Cyrel.node(node_alias, labels: labels))
137
145
  .return_(node_alias, Cyrel.element_id(node_alias).as(:internal_id))
138
146
  end
139
147