activecypher 0.8.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e809c6bea007b6a318d5edd2ccdace438cd005be107fe57c46e6502ef3070d5
4
- data.tar.gz: 6579d0009293f0a496b951a705d4752d6e85b1eb94105f1296f53b642ee77638
3
+ metadata.gz: cee43c77d5c77240791bd23a3ea08410ae58034f29f86d4bd0ec35bc65640b43
4
+ data.tar.gz: 9a5947493ac00f596f11a3ae6e5ceba5c1bb145b56a203c38e27f9c011790fdb
5
5
  SHA512:
6
- metadata.gz: ec67f5ba393f56c92c72473cad34afb2b6f05b77e5a0037333537b1c043b81588c08d5e2264cdc994f1e82a7326c50150be2429403d99d0bb69dc7f8c6d0d388
7
- data.tar.gz: f690ef297c01518b540aadc5e258ce5e8ca2370026caff714259a26cc4db8178dc8ba0de2d0b20cf2e10c11d99aa65df5324cdb75376625d9ceaea1c5031fa42
6
+ metadata.gz: 8465c9e97cd9ce106b7a77e252714c72ac0fcb5470b35eccc0315e9277830402157ef07671b3a1cc757f757be0719f7475781a515322113bf23c698c88bd473c
7
+ data.tar.gz: fbb7585d26ac13eda94bad3e72a7511d133d80ba4b4840c3c9001f323d818f6d6c61d45f26916a1e40ef93555bb1a75e6d4240e776f1d31f22412023faf9dbd3
@@ -67,6 +67,31 @@ module ActiveCypher
67
67
  "#{name} with internal_id=#{internal_db_id.inspect} not found. Perhaps it's in another castle, or just being 'graph'-ty."
68
68
  end
69
69
 
70
+ # Find the first node matching the given attributes, or return nil and question your life choices
71
+ # @param attributes [Hash] Attributes to match
72
+ # @return [Object, nil] The first matching record or nil
73
+ # Because apparently typing .where(attrs).limit(1).first was giving people RSI
74
+ def find_by(attributes = {})
75
+ return nil if attributes.blank?
76
+
77
+ where(attributes).limit(1).first
78
+ end
79
+
80
+ # Find the first node matching the given attributes or throw a tantrum
81
+ # @param attributes [Hash] Attributes to match
82
+ # @return [Object] The first matching record
83
+ # @raise [ActiveCypher::RecordNotFound] When no record is found
84
+ # For when nil isn't dramatic enough and you need your code to scream at you
85
+ def find_by!(attributes = {})
86
+ # Format attributes nicely for the error message
87
+ formatted_attrs = attributes.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')
88
+
89
+ find_by(attributes) || raise(ActiveCypher::RecordNotFound,
90
+ "Couldn't find #{name} with #{formatted_attrs}. " \
91
+ "Perhaps it's hiding in another graph, or maybe it never existed. " \
92
+ 'Who can say in this vast, uncaring universe of nodes and relationships?')
93
+ end
94
+
70
95
  # Instantiates and immediately saves a new record. YOLO mode.
71
96
  # @param attrs [Hash] Attributes for the new record
72
97
  # @return [Object] The new, possibly persisted record
@@ -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?
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveCypher
4
- VERSION = '0.8.2'
4
+ VERSION = '0.10.0'
5
5
 
6
6
  def self.gem_version
7
7
  Gem::Version.new VERSION
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'cyrel/expression'
4
- require 'cyrel/expression/property_access'
5
-
6
3
  module Cyrel
7
4
  module Clause
8
5
  # Represents a SET clause in a Cypher query.
@@ -86,10 +86,7 @@ module Cyrel
86
86
 
87
87
  # Add more operators as needed (e.g., IN, STARTS WITH, CONTAINS, ENDS WITH)
88
88
  # These might be better represented as specific Comparison or FunctionCall types.
89
-
90
- # NOTE: `coerce` method moved to the Expression module itself.
91
89
  end
92
- require_relative 'alias' # Explicitly require the Alias class
93
90
 
94
91
  # Creates an aliased version of this expression.
95
92
  # @param alias_name [Symbol, String] The alias to assign.
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../expression'
4
-
5
3
  module Cyrel
6
4
  module Expression
7
5
  # Represents a CASE expression in Cypher.
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Dependencies are autoloaded by Zeitwerk based on constant usage.
4
- # Explicit requires removed.
5
-
6
3
  module Cyrel
7
4
  module Expression
8
5
  # Represents an EXISTS { pattern } predicate in Cypher.
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../expression'
4
- require_relative '../pattern' # Need Path
5
-
6
3
  module Cyrel
7
4
  module Expression
8
5
  # Represents a Pattern Comprehension in Cypher.
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'base'
4
-
5
3
  module Cyrel
6
4
  module Expression
7
5
  # Represents accessing a property on a variable (node or relationship alias).
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Subclasses are autoloaded by Zeitwerk based on constant usage.
4
- # Explicit requires removed.
5
-
6
3
  module Cyrel
7
4
  # Namespace for classes representing expressions in Cypher queries.
8
5
  # Expressions are parts of a query that evaluate to a value or condition.
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../direction'
4
-
5
3
  module Cyrel
6
4
  module Pattern
7
5
  class Relationship
data/lib/cyrel/query.rb CHANGED
@@ -1,11 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Base DSL components
4
- require 'cyrel/parameterizable'
5
- require 'cyrel/logging'
6
-
7
- # Require all clause types for DSL methods
8
-
9
3
  module Cyrel
10
4
  # Error raised when merging queries with conflicting alias definitions.
11
5
  # Because even in graphs, two things can't have the same name without drama.
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.8.2
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -265,7 +265,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
265
265
  - !ruby/object:Gem::Version
266
266
  version: '0'
267
267
  requirements: []
268
- rubygems_version: 3.6.7
268
+ rubygems_version: 3.6.9
269
269
  specification_version: 4
270
270
  summary: OpenCypher Adapter ala ActiveRecord
271
271
  test_files: []