activecypher 0.9.0 → 0.10.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/model/querying.rb +4 -3
- data/lib/active_cypher/relationship.rb +106 -0
- 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: cee43c77d5c77240791bd23a3ea08410ae58034f29f86d4bd0ec35bc65640b43
|
4
|
+
data.tar.gz: 9a5947493ac00f596f11a3ae6e5ceba5c1bb145b56a203c38e27f9c011790fdb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8465c9e97cd9ce106b7a77e252714c72ac0fcb5470b35eccc0315e9277830402157ef07671b3a1cc757f757be0719f7475781a515322113bf23c698c88bd473c
|
7
|
+
data.tar.gz: fbb7585d26ac13eda94bad3e72a7511d133d80ba4b4840c3c9001f323d818f6d6c61d45f26916a1e40ef93555bb1a75e6d4240e776f1d31f22412023faf9dbd3
|
@@ -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.
|
@@ -159,6 +159,21 @@ module ActiveCypher
|
|
159
159
|
new(attrs, from_node: from_node, to_node: to_node).tap(&:save)
|
160
160
|
end
|
161
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:)
|
165
|
+
relationship = create(attrs, from_node: from_node, to_node: to_node)
|
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
|
175
|
+
end
|
176
|
+
|
162
177
|
# Instantiate from DB row, marking the instance as persisted.
|
163
178
|
def instantiate(attributes, from_node: nil, to_node: nil)
|
164
179
|
instance = allocate
|
@@ -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
|
# --------------------------------------------------------------
|
@@ -232,6 +323,21 @@ module ActiveCypher
|
|
232
323
|
# --------------------------------------------------------------
|
233
324
|
private
|
234
325
|
|
326
|
+
# Initialize from database attributes, marking as persisted
|
327
|
+
def init_with_attributes(attributes, from_node: nil, to_node: nil)
|
328
|
+
# Initialize the model first to set up attributes
|
329
|
+
initialize({}, from_node: from_node, to_node: to_node)
|
330
|
+
|
331
|
+
# Now we're not a new record
|
332
|
+
@new_record = false
|
333
|
+
|
334
|
+
# Assign the attributes from the database
|
335
|
+
assign_attributes(attributes) if attributes
|
336
|
+
|
337
|
+
# Clear any change tracking
|
338
|
+
clear_changes_information
|
339
|
+
end
|
340
|
+
|
235
341
|
def create_relationship
|
236
342
|
raise 'Source node must be persisted' unless from_node&.persisted?
|
237
343
|
raise 'Target node must be persisted' unless to_node&.persisted?
|