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.
- checksums.yaml +4 -4
- data/lib/active_cypher/base.rb +28 -8
- data/lib/active_cypher/bolt/driver.rb +6 -16
- data/lib/active_cypher/bolt/session.rb +62 -50
- data/lib/active_cypher/bolt/transaction.rb +95 -90
- data/lib/active_cypher/connection_adapters/abstract_adapter.rb +28 -0
- data/lib/active_cypher/connection_adapters/abstract_bolt_adapter.rb +40 -32
- data/lib/active_cypher/connection_adapters/memgraph_adapter.rb +44 -0
- data/lib/active_cypher/connection_adapters/neo4j_adapter.rb +27 -1
- data/lib/active_cypher/connection_adapters/registry.rb +94 -0
- data/lib/active_cypher/connection_handler.rb +18 -3
- data/lib/active_cypher/connection_pool.rb +5 -23
- data/lib/active_cypher/connection_url_resolver.rb +14 -3
- data/lib/active_cypher/cypher_config.rb +2 -1
- data/lib/active_cypher/fixtures/dsl_context.rb +41 -0
- data/lib/active_cypher/fixtures/evaluator.rb +37 -0
- data/lib/active_cypher/fixtures/node_builder.rb +53 -0
- data/lib/active_cypher/fixtures/parser.rb +23 -0
- data/lib/active_cypher/fixtures/registry.rb +43 -0
- data/lib/active_cypher/fixtures/rel_builder.rb +96 -0
- data/lib/active_cypher/fixtures.rb +177 -0
- data/lib/active_cypher/generators/node_generator.rb +32 -3
- data/lib/active_cypher/generators/relationship_generator.rb +29 -2
- data/lib/active_cypher/generators/templates/application_graph_node.rb +1 -0
- data/lib/active_cypher/generators/templates/node.rb.erb +10 -6
- data/lib/active_cypher/generators/templates/relationship.rb.erb +7 -6
- data/lib/active_cypher/instrumentation.rb +186 -0
- data/lib/active_cypher/model/callbacks.rb +5 -13
- data/lib/active_cypher/model/connection_handling.rb +37 -52
- data/lib/active_cypher/model/connection_owner.rb +41 -33
- data/lib/active_cypher/model/core.rb +4 -12
- data/lib/active_cypher/model/countable.rb +10 -3
- data/lib/active_cypher/model/destruction.rb +23 -18
- data/lib/active_cypher/model/labelling.rb +45 -0
- data/lib/active_cypher/model/persistence.rb +52 -26
- data/lib/active_cypher/model/querying.rb +49 -25
- data/lib/active_cypher/railtie.rb +40 -5
- data/lib/active_cypher/relation.rb +10 -2
- data/lib/active_cypher/relationship.rb +77 -17
- data/lib/active_cypher/version.rb +1 -1
- data/lib/activecypher.rb +4 -1
- data/lib/cyrel/clause/set.rb +20 -10
- data/lib/cyrel/expression/property_access.rb +2 -0
- data/lib/cyrel/functions.rb +3 -1
- data/lib/cyrel/logging.rb +43 -0
- data/lib/cyrel/plus.rb +11 -0
- data/lib/cyrel/query.rb +7 -1
- data/lib/cyrel.rb +77 -18
- metadata +13 -2
- 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
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
#
|
37
|
-
def
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
#
|
8
|
-
#
|
9
|
-
#
|
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, :
|
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)
|
20
|
-
|
21
|
-
|
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
|
-
#
|
6
|
-
#
|
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
|
26
|
-
|
27
|
-
|
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 =
|
31
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
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
|
-
|
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]
|
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
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
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,
|
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
|
-
#
|
6
|
-
#
|
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
|
-
# --
|
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
|
-
|
33
|
-
# @return [Relation]
|
34
|
-
def limit(val) = all.limit(val)
|
27
|
+
def limit(val) = all.limit(val)
|
35
28
|
|
36
|
-
|
37
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
55
|
-
|
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('*')
|
20
|
-
%i[writing reading analytics].each do |role|
|
21
|
-
next unless (cfg = configs[role])
|
19
|
+
configs = ActiveCypher::CypherConfig.for('*')
|
22
20
|
|
23
|
-
|
24
|
-
|
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(
|
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
|
|