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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b70a7612e29fd973054f654ad63f28b6fe965d993073ffd39aefcdf10798326
4
- data.tar.gz: c23ad33676db0a63bb389bb8edc597eaa4eb6f8cdcfbf44eb32e0eb4c640c1fd
3
+ metadata.gz: 1f1c222ac52b28439402e7b5bdbd0b3245c2530ce75227a9b35ef47917225327
4
+ data.tar.gz: d0cb17f1db87febfdb693f0fc85970b1d4b8e6c7555ae5424be1b04cbcf1eee9
5
5
  SHA512:
6
- metadata.gz: 3688ca19c11ee45562da2a6ccd6d65be740d4ba838802ec8dc52e8453ee80e42fbd771585e31b21ab857b6acb7abc449423debbae9cd2d86a5f9207009cc847d
7
- data.tar.gz: f86b0d54cea7d36aa2d9093d37d0a816ab4fed6485b3697bc31c9a76abda329cae4adb5c33845baccaa5e86153c36aa4996146150b224b2eb7dec927fa43a8f2
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
- "Who can say in this vast, uncaring universe of nodes and relationships?")
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
- assign_attributes(attrs) if attrs
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?
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveCypher
4
- VERSION = '0.9.0'
4
+ VERSION = '0.10.1'
5
5
 
6
6
  def self.gem_version
7
7
  Gem::Version.new VERSION
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activecypher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih