Class: ActiveGraph::Node::Query::QueryProxy

Inherits:
Object
  • Object
show all
Includes:
Dependent::QueryProxyMethods, QueryProxyEagerLoading, QueryProxyEnumerable, QueryProxyFindInBatches, QueryProxyMethods, QueryProxyMethodsOfMassUpdating
Defined in:
lib/active_graph/node/query/query_proxy.rb,
lib/active_graph/node/query/query_proxy_link.rb

Overview

rubocop:disable Metrics/ClassLength

Defined Under Namespace

Classes: Link

Constant Summary collapse

METHODS =
%w(where where_not rel_where rel_where_not rel_order order skip limit union)

Constants included from QueryProxyMethods

ActiveGraph::Node::Query::QueryProxyMethods::FIRST, ActiveGraph::Node::Query::QueryProxyMethods::LAST

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Dependent::QueryProxyMethods

#each_for_destruction, #unique_nodes

Methods included from QueryProxyEagerLoading

#association_tree_class, #first, #perform_query, #pluck_vars, #propagate_context, #with_associations, #with_associations_tree, #with_associations_tree=

Methods included from QueryProxyFindInBatches

#find_each, #find_in_batches

Methods included from QueryProxyMethodsOfMassUpdating

#add_rels, #delete, #delete_all, #delete_all_rels, #delete_rels_for_nodes, #destroy, #replace_with, #update_all, #update_all_rels

Methods included from QueryProxyMethods

#as, #as_models, #count, #distinct, #empty?, #exists?, #find, #find_or_create_by, #find_or_initialize_by, #first, #first_or_initialize, #first_rel_to, #having_rel, #include?, #last, #limit_value, #match_to, #not_having_rel, #optional, #order_property, #propagate_context, #rel, #rels, #rels_to, #size

Methods included from QueryProxyEnumerable

#==, #each, #each_rel, #each_with_rel, #fetch_result_cache, #pluck, #result, #result_cache?, #result_cache_for

Constructor Details

#initialize(model, association = nil, options = {}) ⇒ QueryProxy

QueryProxy is Node’s Cypher DSL. While the name might imply that it creates queries in a general sense, it is actually referring to ActiveGraph::Core::Query, which is a pure Ruby Cypher DSL provided by the activegraph gem. QueryProxy provides ActiveRecord-like methods for common patterns. When it’s not handling CRUD for relationships and queries, it provides Node’s association chaining (‘student.lessons.teachers.where(age: 30).hobbies`) and enjoys long walks on the beach.

It should not ever be necessary to instantiate a new QueryProxy object directly, it always happens as a result of calling a method that makes use of it.

originated. has_many) that created this object. QueryProxy objects are evaluated lazily.

Parameters:

  • model (Constant)

    The class which included Node (typically a model, hence the name) from which the query

  • association (ActiveGraph::Node::HasN::Association) (defaults to: nil)

    The Node association (an object created by a has_one or

  • options (Hash) (defaults to: {})

    Additional options pertaining to the QueryProxy object. These may include:

Options Hash (options):

  • :node_var (String, Symbol)

    A string or symbol to be used by Cypher within its query string as an identifier

  • :rel_var (String, Symbol)

    Same as above but pertaining to a relationship identifier

  • :rel_length (Range, Integer, Symbol, Hash)

    A Range, a Integer, a Hash or a Symbol to indicate the variable-length/fixed-length qualifier of the relationship. See neo4jrb.readthedocs.org/en/latest/Querying.html#variable-length-relationships.

  • :source_object (ActiveGraph::Node)

    The node instance at the start of the QueryProxy chain

  • :query_proxy (QueryProxy)

    An existing QueryProxy chain upon which this new object should be built



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/active_graph/node/query/query_proxy.rb', line 40

def initialize(model, association = nil, options = {})
  @model = model
  @association = association
  @context = options.delete(:context)
  @options = options
  @associations_spec = []

  instance_vars_from_options!(options)

  @match_type = @optional ? :optional_match : :match

  @rel_var = options[:rel] || _rel_chain_var

  @chain = []
  @params = @query_proxy ? @query_proxy.instance_variable_get('@params') : {}
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_name, *args, **kwargs, &block) ⇒ Object

QueryProxy objects act as a representation of a model at the class level so we pass through calls This allows us to define class functions for reusable query chaining or for end-of-query aggregation/summarizing



263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/active_graph/node/query/query_proxy.rb', line 263

def method_missing(method_name, *args, **kwargs, &block)
  if @model && @model.respond_to?(method_name)
    scoping do
      if RUBY_VERSION < '3' && kwargs.empty?
        @model.public_send(method_name, *args, &block)
      else
        @model.public_send(method_name, *args, **kwargs, &block)
      end
    end
  else
    super
  end
end

Instance Attribute Details

#associationObject (readonly)

The most recent node to start a QueryProxy chain. Will be nil when using QueryProxy chains on class methods.



16
17
18
# File 'lib/active_graph/node/query/query_proxy.rb', line 16

def association
  @association
end

#contextObject

Returns the value of attribute context.



285
286
287
# File 'lib/active_graph/node/query/query_proxy.rb', line 285

def context
  @context
end

#modelObject (readonly)

The most recent node to start a QueryProxy chain. Will be nil when using QueryProxy chains on class methods.



16
17
18
# File 'lib/active_graph/node/query/query_proxy.rb', line 16

def model
  @model
end

#node_varObject (readonly)

The current node identifier on deck, so to speak. It is the object that will be returned by calling each and the last node link in the QueryProxy chain.



66
67
68
# File 'lib/active_graph/node/query/query_proxy.rb', line 66

def node_var
  @node_var
end

#query_proxyObject (readonly)

Returns the value of attribute query_proxy.



62
63
64
# File 'lib/active_graph/node/query/query_proxy.rb', line 62

def query_proxy
  @query_proxy
end

#rel_varObject (readonly)

The relationship identifier most recently used by the QueryProxy chain.



73
74
75
# File 'lib/active_graph/node/query/query_proxy.rb', line 73

def rel_var
  @rel_var
end

#source_objectObject (readonly)

The most recent node to start a QueryProxy chain. Will be nil when using QueryProxy chains on class methods.



16
17
18
# File 'lib/active_graph/node/query/query_proxy.rb', line 16

def source_object
  @source_object
end

#start_objectObject (readonly)

Returns the value of attribute start_object.



62
63
64
# File 'lib/active_graph/node/query/query_proxy.rb', line 62

def start_object
  @start_object
end

#starting_queryObject (readonly)

The most recent node to start a QueryProxy chain. Will be nil when using QueryProxy chains on class methods.



16
17
18
# File 'lib/active_graph/node/query/query_proxy.rb', line 16

def starting_query
  @starting_query
end

Instance Method Details

#<<(other_node) ⇒ Object

To add a relationship for the node for the association on this QueryProxy



188
189
190
191
# File 'lib/active_graph/node/query/query_proxy.rb', line 188

def <<(other_node)
  _create_relation_or_defer(other_node)
  self
end

#[](index) ⇒ Object



214
215
216
217
218
# File 'lib/active_graph/node/query/query_proxy.rb', line 214

def [](index)
  # TODO: Maybe for this and other methods, use array if already loaded, otherwise
  # use OFFSET and LIMIT 1?
  self.to_a[index]
end

#_create_relationship(other_node_or_nodes, properties) ⇒ Object



251
252
253
# File 'lib/active_graph/node/query/query_proxy.rb', line 251

def _create_relationship(other_node_or_nodes, properties)
  association._create_relationship(@start_object, other_node_or_nodes, properties)
end

#_model_label_string(with_labels = true) ⇒ Object

param [TrueClass, FalseClass] with_labels This param is used by certain QueryProxy methods that already have the neo_id and therefore do not need labels. The @association_labels instance var is set during init and used during association chaining to keep labels out of Cypher queries.



134
135
136
137
# File 'lib/active_graph/node/query/query_proxy.rb', line 134

def _model_label_string(with_labels = true)
  return if !@model || (!with_labels || @association_labels == false)
  @model.mapped_label_names.map { |label_name| ":`#{label_name}`" }.join
end

#_nodeify!(*args) ⇒ Object



239
240
241
242
243
244
245
246
247
248
249
# File 'lib/active_graph/node/query/query_proxy.rb', line 239

def _nodeify!(*args)
  other_nodes = [args].flatten!.map! do |arg|
    (arg.is_a?(Integer) || arg.is_a?(String)) ? @model.find_by(id: arg) : arg
  end.compact

  if @model && other_nodes.any? { |other_node| !other_node.class.mapped_label_names.include?(@model.mapped_label_name) }
    fail ArgumentError, "Node must be of the association's class when model is specified"
  end

  other_nodes
end

#base_query(var, with_labels = true) ⇒ Object



121
122
123
124
125
126
127
128
129
# File 'lib/active_graph/node/query/query_proxy.rb', line 121

def base_query(var, with_labels = true)
  if @association
    chain_var = _association_chain_var
    (_association_query_start(chain_var) & _query).break.send(@match_type,
                                                              "(#{chain_var})#{_association_arrow}(#{var}#{_model_label_string})")
  else
    starting_query ? starting_query : _query_model_as(var, with_labels)
  end
end

#branch { ... } ⇒ QueryProxy

Executes the relation chain specified in the block, while keeping the current scope

Examples:

Load all people that have friends

Person.all.branch { friends }.to_a # => Returns a list of `Person`

Load all people that has old friends

Person.all.branch { friends.where('age > 70') }.to_a # => Returns a list of `Person`

Yields:

  • the block that will be evaluated starting from the current scope

Returns:



204
205
206
207
208
209
210
211
212
# File 'lib/active_graph/node/query/query_proxy.rb', line 204

def branch(&block)
  fail LocalJumpError, 'no block given' if block.nil?
  # `as(identity)` is here to make sure we get the right variable
  # There might be a deeper problem of the variable changing when we
  # traverse an association
  as(identity).instance_eval(&block).query.proxy_as(self.model, identity).tap do |new_query_proxy|
    propagate_context(new_query_proxy)
  end
end

#create(other_nodes, properties = {}) ⇒ Object



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/active_graph/node/query/query_proxy.rb', line 220

def create(other_nodes, properties = {})
  fail 'Can only create relationships on associations' if !@association
  other_nodes = _nodeify!(*other_nodes)

  ActiveGraph::Base.transaction do
    other_nodes.each do |other_node|
      if other_node.neo_id
        other_node.try(:delete_reverse_has_one_core_rel, association)
      else
        other_node.save
      end

      @start_object.association_proxy_cache.clear

      _create_relationship(other_node, properties)
    end
  end
end

#identityObject Also known as: node_identity



67
68
69
# File 'lib/active_graph/node/query/query_proxy.rb', line 67

def identity
  @node_var || _result_string(_chain_level + 1)
end

#init_outer_query_var(var) ⇒ Object



108
109
110
# File 'lib/active_graph/node/query/query_proxy.rb', line 108

def init_outer_query_var(var)
  chain.find(&:start_of_subquery?)&.tap { |link| @outer_query_var = link.subquery_var(var) }
end

#inspectObject



57
58
59
60
# File 'lib/active_graph/node/query/query_proxy.rb', line 57

def inspect
  formatted_nodes = ActiveGraph::Node::NodeListFormatter.new(to_a)
  "#<QueryProxy #{@context} #{formatted_nodes.inspect}>"
end


287
288
289
290
291
292
# File 'lib/active_graph/node/query/query_proxy.rb', line 287

def new_link(node_var = nil)
  self.clone.tap do |new_query_proxy|
    new_query_proxy.instance_variable_set('@result_cache', nil)
    new_query_proxy.instance_variable_set('@node_var', node_var) if node_var
  end
end

#optional?Boolean

Returns:



281
282
283
# File 'lib/active_graph/node/query/query_proxy.rb', line 281

def optional?
  @optional == true
end

#params(params) ⇒ Object



80
81
82
# File 'lib/active_graph/node/query/query_proxy.rb', line 80

def params(params)
  new_link.tap { |new_query| new_query._add_params(params) }
end

#queryObject

Like calling #query_as, but for when you don’t care about the variable name



85
86
87
# File 'lib/active_graph/node/query/query_proxy.rb', line 85

def query
  query_as(identity)
end

#query_as(var, with_labels = true) ⇒ Object

Build a ActiveGraph::Core::Query object for the QueryProxy. This is necessary when you want to take an existing QueryProxy chain and work with it from the more powerful (but less friendly) ActiveGraph::Core::Query.

.. code-block

ruby

student.lessons.query_as(:l).with('your cypher here...')

Parameters:

  • var (String, Symbol)

    The identifier to use for node at this link of the QueryProxy chain.



96
97
98
99
100
101
102
103
104
105
106
# File 'lib/active_graph/node/query/query_proxy.rb', line 96

def query_as(var, with_labels = true)
  init_outer_query_var(var)
  var_name = @outer_query_var || var
  query_obj = if chain.first&.start_of_subquery? && !@association && !starting_query
                _query
              else
                base_query(var_name, with_labels).params(@params)
              end

  query_from_chain(chain, query_obj, var_name).tap { |query| query.proxy_chain_level = _chain_level }
end

#query_from_chain(chain, base_query, var) ⇒ Object



112
113
114
115
116
117
118
119
# File 'lib/active_graph/node/query/query_proxy.rb', line 112

def query_from_chain(chain, base_query, var)
  chain.inject(base_query) do |query, link|
    args = link.args(var, rel_var)
    var = link.update_outer_query_var(var)

    args.is_a?(Array) ? query.send(link.clause, *args) : query.send(link.clause, args)
  end
end

#read_attribute_for_serialization(*args) ⇒ Object



255
256
257
# File 'lib/active_graph/node/query/query_proxy.rb', line 255

def read_attribute_for_serialization(*args)
  to_a.map { |o| o.read_attribute_for_serialization(*args) }
end

#rel_identityObject



74
75
76
77
78
# File 'lib/active_graph/node/query/query_proxy.rb', line 74

def rel_identity
  ActiveSupport::Deprecation.warn 'rel_identity is deprecated and may be removed from future releases, use rel_var instead.', caller

  @rel_var
end

#respond_to_missing?(method_name, include_all = false) ⇒ Boolean

Returns:



277
278
279
# File 'lib/active_graph/node/query/query_proxy.rb', line 277

def respond_to_missing?(method_name, include_all = false)
  (@model && @model.respond_to?(method_name, include_all)) || super
end

#scopingObject

Scope all queries to the current scope.

.. code-block

ruby

Comment.where(post_id: 1).scoping do
  Comment.first
end

TODO: unscoped Please check unscoped if you want to remove all previous scopes (including the default_scope) during the execution of a block.



150
151
152
153
154
155
156
# File 'lib/active_graph/node/query/query_proxy.rb', line 150

def scoping
  previous = @model.current_scope
  @model.current_scope = self
  yield
ensure
  @model.current_scope = previous
end

#to_cypher_with_params(columns = [self.identity]) ⇒ String

Returns a string of the cypher query with return objects and params

Parameters:

  • columns (Array) (defaults to: [self.identity])

    array containing symbols of identifiers used in the query

Returns:

  • (String)


182
183
184
185
# File 'lib/active_graph/node/query/query_proxy.rb', line 182

def to_cypher_with_params(columns = [self.identity])
  final_query = query.return_query(columns)
  "#{final_query.to_cypher} | params: #{final_query.send(:merge_params)}"
end

#union(*args) ⇒ Object



164
165
166
167
# File 'lib/active_graph/node/query/query_proxy.rb', line 164

def union(*args)
  hash_args = {proxy: self, subquery_parts: args, first_clause: @chain.blank?}
  build_deeper_query_proxy(:union, hash_args)
end

#unpersisted_start_object?Boolean

Returns:



294
295
296
# File 'lib/active_graph/node/query/query_proxy.rb', line 294

def unpersisted_start_object?
  @start_object && @start_object.new_record?
end