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?(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.element_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