activecypher 0.9.0 → 0.10.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/model/querying.rb +4 -3
- data/lib/active_cypher/relationship.rb +115 -4
- data/lib/active_cypher/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1f1c222ac52b28439402e7b5bdbd0b3245c2530ce75227a9b35ef47917225327
|
4
|
+
data.tar.gz: d0cb17f1db87febfdb693f0fc85970b1d4b8e6c7555ae5424be1b04cbcf1eee9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3a8f60aacaea3ed7b3a4fb53e92b042237392d809304f1d17c59ebdb3c6b8e4ea221c44beaa6868a7e2e80859bbdb6f438c1cff65b2eff186b4dcd2f18c9c43a
|
7
|
+
data.tar.gz: ea5cdba9b038ca87809698de82982613f3a1c6efc33365bf9b01f07216edbb5bfcf5007f07cd5b9f0d360a830ec86c95e9e00b695bdafc42ac1cc242ed7c663e
|
@@ -73,6 +73,7 @@ module ActiveCypher
|
|
73
73
|
# Because apparently typing .where(attrs).limit(1).first was giving people RSI
|
74
74
|
def find_by(attributes = {})
|
75
75
|
return nil if attributes.blank?
|
76
|
+
|
76
77
|
where(attributes).limit(1).first
|
77
78
|
end
|
78
79
|
|
@@ -83,12 +84,12 @@ module ActiveCypher
|
|
83
84
|
# For when nil isn't dramatic enough and you need your code to scream at you
|
84
85
|
def find_by!(attributes = {})
|
85
86
|
# Format attributes nicely for the error message
|
86
|
-
formatted_attrs = attributes.map { |k, v| "#{k}: #{v.inspect}" }.join(
|
87
|
-
|
87
|
+
formatted_attrs = attributes.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')
|
88
|
+
|
88
89
|
find_by(attributes) || raise(ActiveCypher::RecordNotFound,
|
89
90
|
"Couldn't find #{name} with #{formatted_attrs}. " \
|
90
91
|
"Perhaps it's hiding in another graph, or maybe it never existed. " \
|
91
|
-
|
92
|
+
'Who can say in this vast, uncaring universe of nodes and relationships?')
|
92
93
|
end
|
93
94
|
|
94
95
|
# Instantiates and immediately saves a new record. YOLO mode.
|
@@ -155,8 +155,23 @@ module ActiveCypher
|
|
155
155
|
|
156
156
|
# -- factories -----------------------------------------------
|
157
157
|
# Mirrors ActiveRecord.create
|
158
|
-
def create(attrs = {}, from_node:, to_node
|
159
|
-
new(attrs, from_node: from_node, to_node: to_node).tap(&:save)
|
158
|
+
def create(attrs = {}, from_node:, to_node:, **attribute_kwargs)
|
159
|
+
new(attrs, from_node: from_node, to_node: to_node, **attribute_kwargs).tap(&:save)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Bang version of create - raises exception if save fails
|
163
|
+
# For when you want your relationship failures to be as dramatic as your breakups
|
164
|
+
def create!(attrs = {}, from_node:, to_node:, **attribute_kwargs)
|
165
|
+
relationship = create(attrs, from_node: from_node, to_node: to_node, **attribute_kwargs)
|
166
|
+
if relationship.persisted?
|
167
|
+
relationship
|
168
|
+
else
|
169
|
+
error_msgs = relationship.errors.full_messages.join(', ')
|
170
|
+
error_msgs = 'Validation failed' if error_msgs.empty?
|
171
|
+
raise ActiveCypher::RecordNotSaved,
|
172
|
+
"#{name} could not be saved: #{error_msgs}. " \
|
173
|
+
"Perhaps the nodes aren't ready for this kind of commitment?"
|
174
|
+
end
|
160
175
|
end
|
161
176
|
|
162
177
|
# Instantiate from DB row, marking the instance as persisted.
|
@@ -168,6 +183,82 @@ module ActiveCypher
|
|
168
183
|
to_node: to_node)
|
169
184
|
instance
|
170
185
|
end
|
186
|
+
|
187
|
+
# -- Querying methods ----------------------------------------
|
188
|
+
# Find the first relationship matching the given attributes
|
189
|
+
# Like finding a needle in a haystack, if the haystack was made of graph edges
|
190
|
+
def find_by(attributes = {})
|
191
|
+
return nil if attributes.blank?
|
192
|
+
|
193
|
+
rel_type = relationship_type
|
194
|
+
|
195
|
+
# Build WHERE conditions for the attributes
|
196
|
+
conditions = []
|
197
|
+
params = {}
|
198
|
+
|
199
|
+
attributes.each_with_index do |(key, value), index|
|
200
|
+
param_name = :"p#{index + 1}"
|
201
|
+
conditions << "r.#{key} = $#{param_name}"
|
202
|
+
params[param_name] = value
|
203
|
+
end
|
204
|
+
|
205
|
+
where_clause = conditions.join(' AND ')
|
206
|
+
|
207
|
+
# Determine ID function based on adapter type
|
208
|
+
adapter_class = connection.class
|
209
|
+
id_func = adapter_class.const_defined?(:ID_FUNCTION) ? adapter_class::ID_FUNCTION : 'id'
|
210
|
+
|
211
|
+
cypher = <<~CYPHER
|
212
|
+
MATCH ()-[r:#{rel_type}]-()
|
213
|
+
WHERE #{where_clause}
|
214
|
+
RETURN r, #{id_func}(r) as rid, startNode(r) as from_node, endNode(r) as to_node
|
215
|
+
LIMIT 1
|
216
|
+
CYPHER
|
217
|
+
|
218
|
+
result = connection.execute_cypher(cypher, params, 'Find Relationship By')
|
219
|
+
row = result.first
|
220
|
+
|
221
|
+
return nil unless row
|
222
|
+
|
223
|
+
# Extract relationship data and instantiate
|
224
|
+
rel_data = row[:r] || row['r']
|
225
|
+
rid = row[:rid] || row['rid']
|
226
|
+
|
227
|
+
# Extract properties from the relationship data
|
228
|
+
# Memgraph returns relationships wrapped as [type_code, [actual_data]]
|
229
|
+
attrs = {}
|
230
|
+
|
231
|
+
if rel_data.is_a?(Array) && rel_data.length == 2
|
232
|
+
# Extract the actual relationship data from the second element
|
233
|
+
actual_data = rel_data[1]
|
234
|
+
|
235
|
+
if actual_data.is_a?(Array) && actual_data.length >= 5
|
236
|
+
# Format: [rel_id, start_id, end_id, type, properties, ...]
|
237
|
+
props = actual_data[4]
|
238
|
+
attrs = props.is_a?(Hash) ? props : {}
|
239
|
+
end
|
240
|
+
elsif rel_data.is_a?(Hash)
|
241
|
+
attrs = rel_data
|
242
|
+
end
|
243
|
+
|
244
|
+
# Convert string keys to symbols for attributes
|
245
|
+
attrs = attrs.transform_keys(&:to_sym)
|
246
|
+
attrs[:internal_id] = rid if rid
|
247
|
+
|
248
|
+
instantiate(attrs)
|
249
|
+
end
|
250
|
+
|
251
|
+
# Find the first relationship or raise an exception
|
252
|
+
# For when nil just isn't dramatic enough for your data access needs
|
253
|
+
def find_by!(attributes = {})
|
254
|
+
# Format attributes nicely for the error message
|
255
|
+
formatted_attrs = attributes.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')
|
256
|
+
|
257
|
+
find_by(attributes) || raise(ActiveCypher::RecordNotFound,
|
258
|
+
"Couldn't find #{name} with #{formatted_attrs}. " \
|
259
|
+
'Maybe these nodes were never meant to be connected? ' \
|
260
|
+
'Or perhaps their relationship status is... complicated?')
|
261
|
+
end
|
171
262
|
end
|
172
263
|
|
173
264
|
# --------------------------------------------------------------
|
@@ -176,10 +267,15 @@ module ActiveCypher
|
|
176
267
|
attr_accessor :from_node, :to_node
|
177
268
|
attr_reader :new_record
|
178
269
|
|
179
|
-
def initialize(attrs = {}, from_node: nil, to_node: nil)
|
270
|
+
def initialize(attrs = {}, from_node: nil, to_node: nil, **attribute_kwargs)
|
180
271
|
_run(:initialize) do
|
181
272
|
super()
|
182
|
-
|
273
|
+
|
274
|
+
# Merge explicit attrs hash with keyword arguments for attributes.
|
275
|
+
# Note: `attribute_kwargs` takes precedence over `attrs` for keys that exist in both.
|
276
|
+
combined_attrs = attrs.merge(attribute_kwargs)
|
277
|
+
assign_attributes(combined_attrs) if combined_attrs.any?
|
278
|
+
|
183
279
|
@from_node = from_node
|
184
280
|
@to_node = to_node
|
185
281
|
@new_record = true
|
@@ -232,6 +328,21 @@ module ActiveCypher
|
|
232
328
|
# --------------------------------------------------------------
|
233
329
|
private
|
234
330
|
|
331
|
+
# Initialize from database attributes, marking as persisted
|
332
|
+
def init_with_attributes(attributes, from_node: nil, to_node: nil)
|
333
|
+
# Initialize the model first to set up attributes
|
334
|
+
initialize({}, from_node: from_node, to_node: to_node)
|
335
|
+
|
336
|
+
# Now we're not a new record
|
337
|
+
@new_record = false
|
338
|
+
|
339
|
+
# Assign the attributes from the database
|
340
|
+
assign_attributes(attributes) if attributes
|
341
|
+
|
342
|
+
# Clear any change tracking
|
343
|
+
clear_changes_information
|
344
|
+
end
|
345
|
+
|
235
346
|
def create_relationship
|
236
347
|
raise 'Source node must be persisted' unless from_node&.persisted?
|
237
348
|
raise 'Target node must be persisted' unless to_node&.persisted?
|