Class: ActiveGraph::Core::Query

Inherits:
Object
  • Object
show all
Includes:
QueryClauses, QueryFindInBatches, Enumerable
Defined in:
lib/active_graph/core/query.rb,
lib/active_graph/core/query_ext.rb

Overview

Allows for generation of cypher queries via ruby method calls (inspired by ActiveRecord / arel syntax)

Can be used to express cypher queries in ruby nicely, or to more easily generate queries programatically.

Also, queries can be passed around an application to progressively build a query across different concerns

See also the following link for full cypher language documentation: docs.neo4j.org/chunked/milestone/cypher-query-lang.html

Defined Under Namespace

Classes: Parameters, PartitionedClauses

Constant Summary collapse

DEFINED_CLAUSES =
{}
METHODS =

DETACH DELETE clause

Returns:

%w[start match optional_match call using where create create_unique merge set on_create_set on_match_set remove unwind delete detach_delete with with_distinct return order skip limit]
BREAK_METHODS =

rubocop:disable Metrics/LineLength

%(with with_distinct call)
CLAUSIFY_CLAUSE =
proc { |method| const_get(method.to_s.split('_').map(&:capitalize).join + 'Clause') }
CLAUSES =
METHODS.map(&CLAUSIFY_CLAUSE)
EMPTY =

Returns a CYPHER query string from the object query representation

Examples:

Query.new.match(p: :Person).where(p: {age: 30})  # => "MATCH (p:Person) WHERE p.age = 30

Returns:

  • (String)

    Resulting cypher query string

' '
NEWLINE =
"\n"

Class Attribute Summary collapse

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from QueryFindInBatches

#find_each, #find_in_batches

Constructor Details

#initialize(options = {}) ⇒ Query

Returns a new instance of Query.



70
71
72
73
74
75
# File 'lib/active_graph/core/query.rb', line 70

def initialize(options = {})
  @options = options
  @clauses = []
  @_params = {}
  @params = Parameters.new
end

Class Attribute Details

.pretty_cypherObject

Returns the value of attribute pretty_cypher.



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

def pretty_cypher
  @pretty_cypher
end

Instance Attribute Details

#clausesObject

Returns the value of attribute clauses.



21
22
23
# File 'lib/active_graph/core/query.rb', line 21

def clauses
  @clauses
end

#proxy_chain_levelObject

For instances where you turn a QueryProxy into a Query and then back to a QueryProxy with `#proxy_as`



21
22
23
# File 'lib/active_graph/core/query_ext.rb', line 21

def proxy_chain_level
  @proxy_chain_level
end

Instance Method Details

#&(other) ⇒ Object



374
375
376
377
378
379
# File 'lib/active_graph/core/query.rb', line 374

def &(other)
  self.class.new.tap do |new_query|
    new_query.options = options.merge(other.options)
    new_query.clauses = clauses + other.clauses
  end.params(other._params)
end

#breakObject

Allows what's been built of the query so far to be frozen and the rest built anew. Can be called multiple times in a string of method calls

Examples:

# Creates a query representing the cypher: MATCH (q:Person), r:Car MATCH (p: Person)-->q
Query.new.match(q: Person).match('r:Car').break.match('(p: Person)-->q')


211
212
213
# File 'lib/active_graph/core/query.rb', line 211

def break
  build_deeper_query(nil)
end

#clause?(method) ⇒ Boolean

Returns:

  • (Boolean)


390
391
392
393
# File 'lib/active_graph/core/query.rb', line 390

def clause?(method)
  clause_class = DEFINED_CLAUSES[method] || CLAUSIFY_CLAUSE.call(method)
  clauses.any? { |clause| clause.is_a?(clause_class) }
end

#contextObject



343
344
345
# File 'lib/active_graph/core/query.rb', line 343

def context
  @options[:context]
end

#copyObject



381
382
383
384
385
386
387
388
# File 'lib/active_graph/core/query.rb', line 381

def copy
  dup.tap do |query|
    to_cypher
    query.instance_variable_set('@params'.freeze, @params.copy)
    query.instance_variable_set('@partitioned_clauses'.freeze, nil)
    query.instance_variable_set('@response'.freeze, nil)
  end
end

#count(var = nil) ⇒ Object



260
261
262
263
# File 'lib/active_graph/core/query.rb', line 260

def count(var = nil)
  v = var.nil? ? '*' : var
  pluck("count(#{v})").first
end

#create(*args) ⇒ Query

CREATE clause

Returns:



# File 'lib/active_graph/core/query.rb', line 137

#create_unique(*args) ⇒ Query

CREATE UNIQUE clause

Returns:



# File 'lib/active_graph/core/query.rb', line 141

#delete(*args) ⇒ Query

DELETE clause

Returns:



# File 'lib/active_graph/core/query.rb', line 157

#detach_delete(*args) ⇒ Query

DETACH DELETE clause

Returns:



165
# File 'lib/active_graph/core/query.rb', line 165

METHODS = %w[start match optional_match call using where create create_unique merge set on_create_set on_match_set remove unwind delete detach_delete with with_distinct return order skip limit]

#eachObject



265
266
267
# File 'lib/active_graph/core/query.rb', line 265

def each
  response.each { |object| yield object }
end

#execBoolean

Executes a query without returning the result

Returns:

  • (Boolean)

    true if successful

Raises:

  • (ActiveGraph::Server::CypherResponse::ResponseError)

    Raises errors from neo4j server



278
279
280
281
282
# File 'lib/active_graph/core/query.rb', line 278

def exec
  response

  true
end

#inspectObject



77
78
79
# File 'lib/active_graph/core/query.rb', line 77

def inspect
  "#<Query CYPHER: #{ANSI::YELLOW}#{to_cypher.inspect}#{ANSI::CLEAR}>"
end

#limit(*args) ⇒ Query

LIMIT clause

Returns:



# File 'lib/active_graph/core/query.rb', line 113

#match(*args) ⇒ Query

MATCH clause

Returns:



# File 'lib/active_graph/core/query.rb', line 85

#match_nodes(hash, optional_match = false) ⇒ Object



245
246
247
248
249
250
251
252
# File 'lib/active_graph/core/query.rb', line 245

def match_nodes(hash, optional_match = false)
  hash.inject(self) do |query, (variable, node_object)|
    neo_id = (node_object.respond_to?(:neo_id) ? node_object.neo_id : node_object)

    match_method = optional_match ? :optional_match : :match
    query.send(match_method, variable).where(variable => {neo_id: neo_id})
  end
end

#merge(*args) ⇒ Query

MERGE clause

Returns:



# File 'lib/active_graph/core/query.rb', line 145

#on_create_set(*args) ⇒ Query

ON CREATE SET clause

Returns:



# File 'lib/active_graph/core/query.rb', line 149

#on_match_set(*args) ⇒ Query

ON MATCH SET clause

Returns:



# File 'lib/active_graph/core/query.rb', line 153

#optional_match(*args) ⇒ Query

OPTIONAL MATCH clause

Returns:



# File 'lib/active_graph/core/query.rb', line 89

#optional_match_nodes(hash) ⇒ Object



254
255
256
# File 'lib/active_graph/core/query.rb', line 254

def optional_match_nodes(hash)
  match_nodes(hash, true)
end

#order(*args) ⇒ Query Also known as: order_by

ORDER BY clause

Returns:



# File 'lib/active_graph/core/query.rb', line 109

#parametersObject



347
348
349
350
# File 'lib/active_graph/core/query.rb', line 347

def parameters
  to_cypher
  merge_params
end

#params(args) ⇒ Object

Allows for the specification of values for params specified in query

Examples:

# Creates a query representing the cypher: MATCH (q: Person {id: $id})
# Calls to params don't affect the cypher query generated, but the params will be
# Passed down when the query is made
Query.new.match('(q: Person {id: $id})').params(id: 12)


222
223
224
# File 'lib/active_graph/core/query.rb', line 222

def params(args)
  copy.tap { |new_query| new_query.instance_variable_get('@params'.freeze).add_params(args) }
end

#partitioned_clausesObject



352
353
354
# File 'lib/active_graph/core/query.rb', line 352

def partitioned_clauses
  @partitioned_clauses ||= PartitionedClauses.new(@clauses)
end

#pluck(*columns) ⇒ Object

Return the specified columns as an array. If one column is specified, a one-dimensional array is returned with the values of that column If two columns are specified, a n-dimensional array is returned with the values of those columns

Examples:

Query.new.match(n: :Person).return(p: :name}.pluck(p: :name) # => Array of names
Query.new.match(n: :Person).return(p: :name}.pluck('p, DISTINCT p.name') # => Array of [node, name] pairs


293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/active_graph/core/query.rb', line 293

def pluck(*columns)
  fail ArgumentError, 'No columns specified for Query#pluck' if columns.size.zero?

  query = return_query(columns)
  columns = query.response.keys

  if columns.size == 1
    column = columns[0]
    query.map { |row| row[column] }
  else
    query.map { |row| columns.map { |column| row[column] } }
  end
end

#pretty_cypherObject



339
340
341
# File 'lib/active_graph/core/query.rb', line 339

def pretty_cypher
  to_cypher(pretty: true)
end


356
357
358
# File 'lib/active_graph/core/query.rb', line 356

def print_cypher
  puts to_cypher(pretty: true).gsub(/\e[^m]+m/, '')
end

#proxy_as(model, var, optional = false) ⇒ ActiveGraph::Node::Query::QueryProxy

Creates a ActiveGraph::Node::Query::QueryProxy object that builds off of a Core::Query object.

Parameters:

  • model (Class)

    An Node model to be used as the start of a new QueryuProxy chain

  • var (Symbol)

    The variable to be used to refer to the object from within the new QueryProxy

  • optional (Boolean) (defaults to: false)

    Indicate whether the new QueryProxy will use MATCH or OPTIONAL MATCH.

Returns:



10
11
12
13
# File 'lib/active_graph/core/query_ext.rb', line 10

def proxy_as(model, var, optional = false)
  # TODO: Discuss whether it's necessary to call `break` on the query or if this should be left to the user.
  ActiveGraph::Node::Query::QueryProxy.new(model, nil, node: var, optional: optional, starting_query: self, chain_level: @proxy_chain_level)
end

#proxy_as_optional(model, var) ⇒ Object

Calls proxy_as with `optional` set true. This doesn't offer anything different from calling `proxy_as` directly but it may be more readable.



16
17
18
# File 'lib/active_graph/core/query_ext.rb', line 16

def proxy_as_optional(model, var)
  proxy_as(model, var, true)
end

#raise_if_cypher_error!(response) ⇒ Object



241
242
243
# File 'lib/active_graph/core/query.rb', line 241

def raise_if_cypher_error!(response)
  response.raise_cypher_error if response.respond_to?(:error?) && response.error?
end

#remove(*args) ⇒ Query

REMOVE clause

Returns:



# File 'lib/active_graph/core/query.rb', line 125

#reorder(*args) ⇒ Object

Clears out previous order clauses and allows only for those specified by args



186
187
188
189
190
191
# File 'lib/active_graph/core/query.rb', line 186

def reorder(*args)
  query = copy

  query.remove_clause_class(OrderClause)
  query.order(*args)
end

#responseObject



235
236
237
238
239
# File 'lib/active_graph/core/query.rb', line 235

def response
  return @response if @response

  @response = ActiveGraph::Base.query(self, wrap: !unwrapped?)
end

#return(*args) ⇒ Query

RETURN clause

Returns:



# File 'lib/active_graph/core/query.rb', line 133

#return_query(columns) ⇒ Object



307
308
309
310
311
312
# File 'lib/active_graph/core/query.rb', line 307

def return_query(columns)
  query = copy
  query.remove_clause_class(ReturnClause)

  query.return(*columns)
end

#set(*args) ⇒ Query

SET clause

Returns:



# File 'lib/active_graph/core/query.rb', line 121

#set_props(*args) ⇒ Object

Works the same as the #set method, but when given a nested array it will set properties rather than setting entire objects

Examples:

# Creates a query representing the cypher: MATCH (n:Person) SET n.age = 19
Query.new.match(n: :Person).set_props(n: {age: 19})


203
204
205
# File 'lib/active_graph/core/query.rb', line 203

def set_props(*args) # rubocop:disable Naming/AccessorMethodName
  build_deeper_query(SetClause, args, set_props: true)
end

#skip(*args) ⇒ Query Also known as: offset

SKIP clause

Returns:



# File 'lib/active_graph/core/query.rb', line 117

#start(*args) ⇒ Query

START clause

Returns:



# File 'lib/active_graph/core/query.rb', line 81

#to_aArray

Class is Enumerable. Each yield is a Hash with the key matching the variable returned and the value being the value for that key from the response

Returns:

  • (Array)

Raises:

  • (ActiveGraph::Server::CypherResponse::ResponseError)

    Raises errors from neo4j server



# File 'lib/active_graph/core/query.rb', line 269

#to_cypher(options = {}) ⇒ Object Also known as: cypher



321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/active_graph/core/query.rb', line 321

def to_cypher(options = {})
  join_string = options[:pretty] ? NEWLINE : EMPTY

  cypher_string = partitioned_clauses.map do |clauses|
    clauses_by_class = clauses.group_by(&:class)

    cypher_parts = CLAUSES.map do |clause_class|
      clause_class.to_cypher(clauses, options[:pretty]) if clauses = clauses_by_class[clause_class]
    end.compact

    cypher_parts.join(join_string).tap(&:strip!)
  end.join(join_string)

  cypher_string = "CYPHER #{@options[:parser]} #{cypher_string}" if @options[:parser]
  cypher_string.tap(&:strip!)
end

#union_cypher(other, options = {}) ⇒ String

Returns a CYPHER query specifying the union of the callee object's query and the argument's query

Examples:

# Generates cypher: MATCH (n:Person) UNION MATCH (o:Person) WHERE o.age = 10
q = ActiveGraph::Core::Query.new.match(o: :Person).where(o: {age: 10})
result = ActiveGraph::Core::Query.new.match(n: :Person).union_cypher(q)

Parameters:

  • other (Query)

    Second half of UNION

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

    Specify true to use UNION ALL

Returns:

  • (String)

    Resulting UNION cypher query string



370
371
372
# File 'lib/active_graph/core/query.rb', line 370

def union_cypher(other, options = {})
  "#{to_cypher} UNION#{options[:all] ? ' ALL' : ''} #{other.to_cypher}"
end

#unwind(*args) ⇒ Query

UNWIND clause

Returns:



# File 'lib/active_graph/core/query.rb', line 129

#unwrappedObject



226
227
228
229
# File 'lib/active_graph/core/query.rb', line 226

def unwrapped
  @_unwrapped_obj = true
  self
end

#unwrapped?Boolean

Returns:

  • (Boolean)


231
232
233
# File 'lib/active_graph/core/query.rb', line 231

def unwrapped?
  !!@_unwrapped_obj
end

#using(*args) ⇒ Query

USING clause

Returns:



# File 'lib/active_graph/core/query.rb', line 93

#where(*args) ⇒ Query

WHERE clause

Returns:



# File 'lib/active_graph/core/query.rb', line 97

#where_not(*args) ⇒ Object

Works the same as the #where method, but the clause is surrounded by a Cypher NOT() function



195
196
197
# File 'lib/active_graph/core/query.rb', line 195

def where_not(*args)
  build_deeper_query(WhereClause, args, not: true)
end

#with(*args) ⇒ Query

WITH clause

Returns:



# File 'lib/active_graph/core/query.rb', line 101

#with_distinct(*args) ⇒ Query

WITH clause with DISTINCT specified

Returns:



# File 'lib/active_graph/core/query.rb', line 105