activecypher 0.5.0 → 0.6.1
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/transaction.rb +9 -9
- data/lib/active_cypher/connection_adapters/abstract_adapter.rb +28 -0
- data/lib/active_cypher/connection_adapters/abstract_bolt_adapter.rb +1 -1
- data/lib/active_cypher/connection_adapters/memgraph_adapter.rb +44 -0
- data/lib/active_cypher/connection_adapters/neo4j_adapter.rb +26 -0
- data/lib/active_cypher/connection_adapters/registry.rb +96 -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/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/templates/application_graph_node.rb +1 -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 +31 -38
- data/lib/active_cypher/model/core.rb +1 -63
- data/lib/active_cypher/model/destruction.rb +16 -18
- data/lib/active_cypher/model/labelling.rb +45 -0
- data/lib/active_cypher/model/persistence.rb +46 -40
- data/lib/active_cypher/model/querying.rb +47 -27
- data/lib/active_cypher/railtie.rb +40 -5
- 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/functions.rb +3 -1
- data/lib/cyrel/logging.rb +43 -0
- data/lib/cyrel/query.rb +6 -1
- metadata +11 -2
- data/lib/active_cypher/connection_factory.rb +0 -130
@@ -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.
|
@@ -23,29 +21,29 @@ module ActiveCypher
|
|
23
21
|
raise 'Record already destroyed' if destroyed?
|
24
22
|
|
25
23
|
n = :n
|
26
|
-
# Use all labels for database operations
|
27
24
|
labels = self.class.respond_to?(:labels) ? self.class.labels : [self.class.label_name]
|
28
|
-
|
29
|
-
.where(Cyrel.element_id(n).eq(internal_id))
|
30
|
-
.detach_delete(n)
|
31
|
-
.return_('count(*) as deleted')
|
25
|
+
label_string = labels.map { |l| ":#{l}" }.join
|
32
26
|
|
33
|
-
cypher
|
34
|
-
|
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
|
35
33
|
|
36
|
-
|
37
|
-
|
38
|
-
result
|
39
|
-
if result.present? && result.first.present? && (result.first[:deleted] || 0).positive?
|
34
|
+
result = self.class.connection.execute_cypher(cypher, {}, 'Destroy')
|
35
|
+
|
36
|
+
if result.present? && result.first[:deleted].to_i.positive?
|
40
37
|
@destroyed = true
|
41
|
-
freeze
|
38
|
+
freeze
|
42
39
|
true
|
43
40
|
else
|
44
41
|
false
|
45
42
|
end
|
46
43
|
end
|
47
|
-
rescue StandardError
|
48
|
-
|
44
|
+
rescue StandardError => e
|
45
|
+
warn "[Destruction] Destroy failed: #{e.class}: #{e.message}"
|
46
|
+
false
|
49
47
|
end
|
50
48
|
|
51
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.
|
@@ -135,16 +127,31 @@ module ActiveCypher
|
|
135
127
|
# Use all labels for database operations
|
136
128
|
labels = self.class.respond_to?(:labels) ? self.class.labels : [self.class.label_name.to_s]
|
137
129
|
|
138
|
-
#
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
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')
|
143
151
|
|
144
|
-
data = self.class.connection.execute_cypher(cypher, params, 'Create')
|
145
152
|
return false if data.blank? || !data.first.key?(:internal_id)
|
146
153
|
|
147
|
-
self.internal_id = data.first[:internal_id]
|
154
|
+
self.internal_id = data.first[:internal_id]
|
148
155
|
@new_record = false
|
149
156
|
changes_applied
|
150
157
|
true
|
@@ -169,33 +176,32 @@ module ActiveCypher
|
|
169
176
|
changes = changes_to_save
|
170
177
|
return true if changes.empty?
|
171
178
|
|
172
|
-
n = :n
|
173
|
-
|
174
179
|
# Use all labels for database operations
|
175
180
|
labels = self.class.respond_to?(:labels) ? self.class.labels : [self.class.label_name]
|
176
181
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
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(', ')
|
187
195
|
|
188
|
-
cypher
|
189
|
-
|
196
|
+
cypher = "MATCH (n#{label_string}) " \
|
197
|
+
"WHERE id(n) = #{internal_id} " \
|
198
|
+
"SET #{set_clauses} " \
|
199
|
+
'RETURN n'
|
190
200
|
|
191
|
-
|
201
|
+
self.class.connection.execute_cypher(cypher, {}, 'Update')
|
192
202
|
|
193
|
-
|
194
|
-
|
195
|
-
true
|
196
|
-
else
|
197
|
-
false
|
198
|
-
end
|
203
|
+
changes_applied
|
204
|
+
true
|
199
205
|
end
|
200
206
|
end
|
201
207
|
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,35 +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
|
-
# Use all labels if available, otherwise fall back to the primary label
|
49
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
|
50
48
|
|
51
|
-
|
52
|
-
|
53
|
-
.where(Cyrel.element_id(node_alias).eq(internal_db_id))
|
54
|
-
.return_(node_alias, Cyrel.element_id(node_alias).as(:internal_id))
|
55
|
-
.limit(1)
|
56
|
-
query.to_cypher
|
49
|
+
result = connection.execute_cypher(cypher)
|
50
|
+
record = result.first
|
57
51
|
|
58
|
-
|
59
|
-
|
60
|
-
|
52
|
+
if record
|
53
|
+
attrs = _hydrate_attributes_from_memgraph_record(record, node_alias)
|
54
|
+
return instantiate(attrs)
|
55
|
+
end
|
56
|
+
|
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."
|
61
59
|
end
|
62
60
|
|
63
61
|
# Instantiates and immediately saves a new record. YOLO mode.
|
@@ -65,6 +63,28 @@ module ActiveCypher
|
|
65
63
|
# @return [Object] The new, possibly persisted record
|
66
64
|
# Because sometimes you just want to live dangerously.
|
67
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
|
68
88
|
end
|
69
89
|
end
|
70
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
|
|
@@ -47,7 +47,7 @@ module ActiveCypher
|
|
47
47
|
# --------------------------------------------------------------
|
48
48
|
# Attributes
|
49
49
|
# --------------------------------------------------------------
|
50
|
-
attribute :internal_id, :
|
50
|
+
attribute :internal_id, :integer
|
51
51
|
|
52
52
|
# --------------------------------------------------------------
|
53
53
|
# Connection fallback
|
@@ -59,6 +59,11 @@ module ActiveCypher
|
|
59
59
|
# WorksAtRelationship.connection # -> PersonNode.connection
|
60
60
|
#
|
61
61
|
def self.connection
|
62
|
+
# If a node_base_class is set (directly or by convention), always delegate to its connection
|
63
|
+
if (klass = node_base_class)
|
64
|
+
return klass.connection
|
65
|
+
end
|
66
|
+
|
62
67
|
return @connection if defined?(@connection) && @connection
|
63
68
|
|
64
69
|
begin
|
@@ -74,10 +79,51 @@ module ActiveCypher
|
|
74
79
|
class_attribute :_from_class_name, instance_writer: false
|
75
80
|
class_attribute :_to_class_name, instance_writer: false
|
76
81
|
class_attribute :_relationship_type, instance_writer: false
|
82
|
+
class_attribute :_node_base_class, instance_writer: false
|
77
83
|
|
78
84
|
class << self
|
79
85
|
attr_reader :last_internal_id
|
80
86
|
|
87
|
+
# DSL for setting or getting the node base class for connection delegation
|
88
|
+
def node_base_class(klass = nil)
|
89
|
+
if klass.nil?
|
90
|
+
# If not set, try convention: XxxRelationship -> XxxNode
|
91
|
+
return _node_base_class if _node_base_class
|
92
|
+
|
93
|
+
if name&.end_with?('Relationship')
|
94
|
+
node_base_name = name.sub(/Relationship\z/, 'Node')
|
95
|
+
begin
|
96
|
+
node_base_klass = node_base_name.constantize
|
97
|
+
if node_base_klass.respond_to?(:abstract_class?) && node_base_klass.abstract_class?
|
98
|
+
self._node_base_class = node_base_klass
|
99
|
+
return node_base_klass
|
100
|
+
end
|
101
|
+
rescue NameError
|
102
|
+
# Do nothing, fallback to nil
|
103
|
+
end
|
104
|
+
end
|
105
|
+
return _node_base_class
|
106
|
+
end
|
107
|
+
# Only allow setting on abstract relationship base classes
|
108
|
+
raise "Cannot set node_base_class on non-abstract relationship class #{name}" unless abstract_class?
|
109
|
+
unless klass.respond_to?(:abstract_class?) && klass.abstract_class?
|
110
|
+
raise ArgumentError, "node_base_class must be an abstract node base class (got #{klass})"
|
111
|
+
end
|
112
|
+
|
113
|
+
self._node_base_class = klass
|
114
|
+
end
|
115
|
+
|
116
|
+
# Prevent subclasses from overriding node_base_class
|
117
|
+
def inherited(subclass)
|
118
|
+
super
|
119
|
+
return unless _node_base_class
|
120
|
+
|
121
|
+
subclass._node_base_class = _node_base_class
|
122
|
+
def subclass.node_base_class(*)
|
123
|
+
raise "Cannot override node_base_class in subclass #{name}; it is locked to #{_node_base_class}"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
81
127
|
# -- endpoints ------------------------------------------------
|
82
128
|
def from_class(value = nil)
|
83
129
|
return _from_class_name if value.nil?
|
@@ -160,8 +206,10 @@ module ActiveCypher
|
|
160
206
|
raise 'Cannot destroy a new relationship' if new_record?
|
161
207
|
raise 'Relationship already destroyed' if destroyed?
|
162
208
|
|
163
|
-
|
164
|
-
|
209
|
+
adapter = self.class.connection.id_handler
|
210
|
+
|
211
|
+
cypher = "MATCH ()-[r]-() WHERE #{adapter.with_direct_id(internal_id)} DELETE r"
|
212
|
+
params = {}
|
165
213
|
|
166
214
|
self.class.connection.execute_cypher(cypher, params, 'Destroy Relationship')
|
167
215
|
@destroyed = true
|
@@ -186,22 +234,31 @@ module ActiveCypher
|
|
186
234
|
rel_ty = self.class.relationship_type
|
187
235
|
arrow = '->' # outgoing by default
|
188
236
|
|
189
|
-
|
190
|
-
parts
|
191
|
-
|
192
|
-
|
237
|
+
adapter = self.class.connection.id_handler
|
238
|
+
parts = []
|
239
|
+
|
240
|
+
# Build the Cypher query based on the adapter
|
241
|
+
id_clause = adapter.with_direct_node_ids(from_node.internal_id, to_node.internal_id)
|
242
|
+
parts << "MATCH (p), (h) WHERE #{id_clause}"
|
243
|
+
parts << "CREATE (p)-[r:#{rel_ty}]#{arrow}(h)"
|
193
244
|
parts << 'SET r += $props' unless props.empty? # only if we have props
|
194
|
-
parts <<
|
245
|
+
parts << "RETURN #{adapter.return_id}"
|
195
246
|
|
196
247
|
cypher = parts.join(' ')
|
197
|
-
params = {
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
248
|
+
params = { props: props }
|
249
|
+
|
250
|
+
# Execute Cypher query
|
251
|
+
result = self.class.connection.execute_cypher(cypher, params, 'Create Relationship')
|
252
|
+
|
253
|
+
row = result.first
|
202
254
|
|
203
|
-
|
204
|
-
|
255
|
+
# Try different ways to access the ID
|
256
|
+
rid_sym = row && row[:rid]
|
257
|
+
rid_str = row && row['rid']
|
258
|
+
|
259
|
+
rid = rid_sym || rid_str
|
260
|
+
|
261
|
+
raise 'Relationship creation returned no id' if rid.nil?
|
205
262
|
|
206
263
|
self.internal_id = rid
|
207
264
|
self.class.instance_variable_set(:@last_internal_id, rid)
|
@@ -217,11 +274,14 @@ module ActiveCypher
|
|
217
274
|
changes = changes_to_save
|
218
275
|
return true if changes.empty?
|
219
276
|
|
277
|
+
adapter = self.class.connection.id_handler
|
278
|
+
|
220
279
|
cypher = <<~CYPHER
|
221
|
-
MATCH ()-[r]-() WHERE
|
280
|
+
MATCH ()-[r]-() WHERE #{adapter.with_direct_id(internal_id)}
|
222
281
|
SET r += $props
|
223
282
|
CYPHER
|
224
|
-
|
283
|
+
|
284
|
+
params = { props: changes }
|
225
285
|
|
226
286
|
self.class.connection.execute_cypher(cypher, params, 'Update Relationship')
|
227
287
|
changes_applied
|
data/lib/activecypher.rb
CHANGED
@@ -101,7 +101,10 @@ loader.ignore("#{__dir__}/active_cypher/railtie.rb")
|
|
101
101
|
loader.ignore("#{__dir__}/active_cypher/generators")
|
102
102
|
loader.ignore("#{__dir__}/activecypher.rb")
|
103
103
|
loader.ignore("#{__dir__}/cyrel.rb")
|
104
|
-
loader.inflector.inflect(
|
104
|
+
loader.inflector.inflect(
|
105
|
+
'activecypher' => 'ActiveCypher',
|
106
|
+
'dsl_context' => 'DSLContext'
|
107
|
+
)
|
105
108
|
loader.push_dir("#{__dir__}/cyrel", namespace: Cyrel)
|
106
109
|
loader.setup
|
107
110
|
|
data/lib/cyrel/functions.rb
CHANGED
@@ -22,7 +22,9 @@ module Cyrel
|
|
22
22
|
Expression::FunctionCall.new(:elementId, Clause::Return::RawIdentifier.new(node_variable.to_s))
|
23
23
|
end
|
24
24
|
|
25
|
-
|
25
|
+
def id(node_variable)
|
26
|
+
Expression::FunctionCall.new(:id, Clause::Return::RawIdentifier.new(node_variable.to_s))
|
27
|
+
end
|
26
28
|
|
27
29
|
# Because apparently, COUNT(*) isn’t obvious enough.
|
28
30
|
# Handles the 'give me everything and make it snappy' use case.
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'active_support/tagged_logging'
|
5
|
+
|
6
|
+
module Cyrel
|
7
|
+
# Basic logging support for Cyrel.
|
8
|
+
# Debug logging is disabled by default.
|
9
|
+
module Logging
|
10
|
+
LOG_TAG = 'Cyrel'
|
11
|
+
|
12
|
+
class << self
|
13
|
+
# @return [Logger] the configured logger
|
14
|
+
attr_accessor :backend
|
15
|
+
|
16
|
+
def resolve_log_level(log_level_str)
|
17
|
+
Logger.const_get(log_level_str.upcase)
|
18
|
+
rescue StandardError
|
19
|
+
Logger::UNKNOWN
|
20
|
+
end
|
21
|
+
|
22
|
+
def logger
|
23
|
+
self.backend ||= begin
|
24
|
+
log_level = ENV.fetch('CYREL_LOG_LEVEL', 'unknown')
|
25
|
+
logger_base = Logger.new($stdout)
|
26
|
+
logger_base.level = resolve_log_level(log_level)
|
27
|
+
|
28
|
+
# Return a TaggedLogging instance without calling 'tagged!'
|
29
|
+
ActiveSupport::TaggedLogging.new(logger_base)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def logger
|
35
|
+
Logging.logger.tagged(LOG_TAG)
|
36
|
+
end
|
37
|
+
|
38
|
+
def log_debug(msg) = logger.debug { msg }
|
39
|
+
def log_info(msg) = logger.info { msg }
|
40
|
+
def log_warn(msg) = logger.warn { msg }
|
41
|
+
def log_error(msg) = logger.error { msg }
|
42
|
+
end
|
43
|
+
end
|
data/lib/cyrel/query.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Base DSL components
|
3
4
|
require 'cyrel/parameterizable'
|
4
|
-
|
5
|
+
require 'cyrel/logging'
|
5
6
|
|
6
7
|
# Require all clause types for DSL methods
|
7
8
|
|
@@ -19,6 +20,7 @@ module Cyrel
|
|
19
20
|
# # Manages clauses, parameters, and final query generation, because string interpolation is for amateurs.
|
20
21
|
class Query
|
21
22
|
include Parameterizable
|
23
|
+
include Logging
|
22
24
|
attr_reader :parameters, :clauses # Expose clauses for merge logic
|
23
25
|
|
24
26
|
def initialize
|
@@ -59,6 +61,9 @@ module Cyrel
|
|
59
61
|
.reject(&:blank?)
|
60
62
|
.join("\n")
|
61
63
|
|
64
|
+
log_debug("QUERY: #{cypher_string}")
|
65
|
+
log_debug("PARAMS: #{@parameters.inspect}") unless @parameters.empty?
|
66
|
+
|
62
67
|
[cypher_string, @parameters]
|
63
68
|
end
|
64
69
|
end
|