From 0f436423374671e097075ad0588db76dfe52a2d0 Mon Sep 17 00:00:00 2001 From: John Carroll Date: Wed, 27 Dec 2017 17:09:31 -0800 Subject: [PATCH 1/6] wip: added union query clause method --- lib/neo4j-core/query.rb | 9 ++++- lib/neo4j-core/query_clauses.rb | 59 +++++++++++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/lib/neo4j-core/query.rb b/lib/neo4j-core/query.rb index 080e7942..ac2b9b29 100644 --- a/lib/neo4j-core/query.rb +++ b/lib/neo4j-core/query.rb @@ -131,6 +131,10 @@ def inspect # RETURN clause # @return [Query] + # @method union *args + # UNION clause + # @return [Query] + # @method create *args # CREATE clause # @return [Query] @@ -159,7 +163,7 @@ def inspect # DETACH DELETE clause # @return [Query] - 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 return order skip limit] # rubocop:disable Metrics/LineLength + 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 return order skip limit union] # rubocop:disable Metrics/LineLength BREAK_METHODS = %(with call) CLAUSIFY_CLAUSE = proc { |method| const_get(method.to_s.split('_').map(&:capitalize).join + 'Clause') } @@ -170,6 +174,7 @@ def inspect DEFINED_CLAUSES[clause.to_sym] = clause_class define_method(clause) do |*args| + # the args splat will contain method "options", if any were given build_deeper_query(clause_class, args).ergo do |result| BREAK_METHODS.include?(clause) ? result.break : result end @@ -422,7 +427,9 @@ def remove_clause_class(clause_class) def build_deeper_query(clause_class, args = {}, options = {}) copy.tap do |new_query| + # An empty clause (`[nil]`) indicates a "break" to the "generate_partitioning" method new_query.add_clauses [nil] if [nil, WithClause].include?(clause_class) + # @params stores the query's accumulated ".params()" values new_query.add_clauses clause_class.from_args(args, new_query.instance_variable_get('@params'.freeze), options) if clause_class end end diff --git a/lib/neo4j-core/query_clauses.rb b/lib/neo4j-core/query_clauses.rb index 9032e3fc..b87dee42 100644 --- a/lib/neo4j-core/query_clauses.rb +++ b/lib/neo4j-core/query_clauses.rb @@ -25,11 +25,12 @@ def initialize(arg, params, options = {}) @param_vars_added = [] end + # Returns the query clause as a cypher string def value return @value if @value - [String, Symbol, Integer, Hash, NilClass].each do |arg_class| - from_method = "from_#{arg_class.name.downcase}" + [String, Symbol, Integer, Hash, NilClass, Query].each do |arg_class| + from_method = "from_#{arg_class.name.demodulize.downcase}" return @value = send(from_method, @arg) if @arg.is_a?(arg_class) && self.respond_to?(from_method) end @@ -727,6 +728,60 @@ def clause_join end end end + + class UnionClause < Clause + KEYWORD = 'UNION' + + def from_query(value) + from_string(value.to_cypher) + end + + class << self + # Union clauses can only be called with a string or Query argument + def from_args(args, params, options = {}) + from_arg(arg, params, options) + end + + def from_arg(arg, params, options = {}) + new(arg, params, options) if arg.is_a?(Query) || arg.is_a?(String) + end + + def to_cypher(clauses, pretty = false) + clause_string(clauses, pretty) + end + + def clause_string(clauses, pretty) + strings = clause_strings(clauses, pretty) + stripped_string = strings.join(clause_join) + stripped_string.strip! + (pretty && strings.size > 1) ? PRETTY_NEW_LINE + stripped_string : stripped_string + end + + # If `.union()` was called with `all: true` option, insert 'UNION ALL' clause + # otherwise insert 'UNION' clause + def clause_strings(clauses, pretty) + clauses.map do |clause| + clause_keyword = if clause.options && clause.options[:all] + "#{keyword} ALL" + else + keyword + end + + if pretty + "#{clause_color}#{clause_keyword}#{ANSI::CLEAR} #{clause.value}" + PRETTY_NEW_LINE + else + "#{clause_keyword} #{clause.value}" + end + end + end + + def clause_join(options = {}) + ' ' + end + end + end end end end + +union(query, {all: true}) \ No newline at end of file From 93d7bf86103783b65f5a1e457bfcc7df2db6c24c Mon Sep 17 00:00:00 2001 From: John Carroll Date: Wed, 27 Dec 2017 17:13:28 -0800 Subject: [PATCH 2/6] fix: removed note --- lib/neo4j-core/query_clauses.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/neo4j-core/query_clauses.rb b/lib/neo4j-core/query_clauses.rb index b87dee42..a66123ef 100644 --- a/lib/neo4j-core/query_clauses.rb +++ b/lib/neo4j-core/query_clauses.rb @@ -783,5 +783,3 @@ def clause_join(options = {}) end end end - -union(query, {all: true}) \ No newline at end of file From 7089af639903859b9a59eff7a1460cf1db85e04b Mon Sep 17 00:00:00 2001 From: John Carroll Date: Wed, 27 Dec 2017 18:22:54 -0800 Subject: [PATCH 3/6] wip: union query clause --- lib/neo4j-core/query.rb | 14 +++++++++++++- lib/neo4j-core/query_clauses.rb | 9 ++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/neo4j-core/query.rb b/lib/neo4j-core/query.rb index ac2b9b29..44d3971b 100644 --- a/lib/neo4j-core/query.rb +++ b/lib/neo4j-core/query.rb @@ -174,7 +174,7 @@ def inspect DEFINED_CLAUSES[clause.to_sym] = clause_class define_method(clause) do |*args| - # the args splat will contain method "options", if any were given + # the args splat will contain the method options, if any were given build_deeper_query(clause_class, args).ergo do |result| BREAK_METHODS.include?(clause) ? result.break : result end @@ -327,6 +327,18 @@ def return_query(columns) query = copy query.remove_clause_class(ReturnClause) + # If the query object has union clauses, overwrite the return of each union clause + # to return `columns` + clauses_by_class = query.partitioned_clauses.last.group_by(&:class) + union_clauses = clauses_by_class[::Neo4j::Core::QueryClauses::UnionClause] + + if union_clauses.any? + union_clauses.each do |union_clause| + union_clause.arg.remove_clause_class(ReturnClause) + union_clause.arg.return(*columns) + end + end + query.return(*columns) end diff --git a/lib/neo4j-core/query_clauses.rb b/lib/neo4j-core/query_clauses.rb index a66123ef..64ea749f 100644 --- a/lib/neo4j-core/query_clauses.rb +++ b/lib/neo4j-core/query_clauses.rb @@ -732,6 +732,11 @@ def clause_join class UnionClause < Clause KEYWORD = 'UNION' + def initialize(arg, params, options = {}) + fail ArgError unless arg + super + end + def from_query(value) from_string(value.to_cypher) end @@ -739,7 +744,9 @@ def from_query(value) class << self # Union clauses can only be called with a string or Query argument def from_args(args, params, options = {}) - from_arg(arg, params, options) + arg = args.first + + [from_arg(arg, params, options)] end def from_arg(arg, params, options = {}) From 24159f4c4be115c7fb52b535cabb56eab8d21800 Mon Sep 17 00:00:00 2001 From: John Carroll Date: Wed, 27 Dec 2017 21:47:45 -0800 Subject: [PATCH 4/6] add: UnionClause --- lib/neo4j-core/query.rb | 29 +++++++++++++++++++++++------ lib/neo4j-core/query_clauses.rb | 27 +++++++++++++++++++-------- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/lib/neo4j-core/query.rb b/lib/neo4j-core/query.rb index 44d3971b..c2a035ed 100644 --- a/lib/neo4j-core/query.rb +++ b/lib/neo4j-core/query.rb @@ -214,6 +214,11 @@ def break build_deeper_query(nil) end + # UNION ALL cypher clause. Similar to UNION method / clause but doesn't de-duplicate results. See Neo4j docs for more info. + def union_all(*args) + build_deeper_query(UnionClause, args, all: true) + end + # Allows for the specification of values for params specified in query # @example # # Creates a query representing the cypher: MATCH (q: Person {id: {id}}) @@ -327,16 +332,28 @@ def return_query(columns) query = copy query.remove_clause_class(ReturnClause) - # If the query object has union clauses, overwrite the return of each union clause - # to return `columns` - clauses_by_class = query.partitioned_clauses.last.group_by(&:class) + # Check for union clauses + clauses_by_class = query.clauses.group_by(&:class) union_clauses = clauses_by_class[::Neo4j::Core::QueryClauses::UnionClause] - if union_clauses.any? + # If the query object has union clauses, overwrite the return of each union clause + # to return `*columns`. Ignore union clauses which have a string `@arg` + if union_clauses && union_clauses.any? + query.remove_clause_class(UnionClause) + union_clauses.each do |union_clause| - union_clause.arg.remove_clause_class(ReturnClause) - union_clause.arg.return(*columns) + arg = union_clause.arg + + # If `arg` is a Query object, we can overwrite the return. Else, ignore it and hope + # the dev has specified the correct return + if arg.is_a? Query + arg.remove_clause_class(ReturnClause) + union_clause.arg = arg.return(*columns) + union_clause.reset_value! + end end + + query.add_clauses(union_clauses) end query.return(*columns) diff --git a/lib/neo4j-core/query_clauses.rb b/lib/neo4j-core/query_clauses.rb index 64ea749f..a2463e2b 100644 --- a/lib/neo4j-core/query_clauses.rb +++ b/lib/neo4j-core/query_clauses.rb @@ -25,7 +25,7 @@ def initialize(arg, params, options = {}) @param_vars_added = [] end - # Returns the query clause as a cypher string + # Returns the query clause as a cypher string and caches result def value return @value if @value @@ -732,13 +732,24 @@ def clause_join class UnionClause < Clause KEYWORD = 'UNION' - def initialize(arg, params, options = {}) - fail ArgError unless arg - super + # If `value` is a query object, returns value.to_cypher. Formatting optional + def from_query(value, pretty: false) + from_string(value.to_cypher(pretty: pretty)) + end + + # Returns the query clause as a pretty string, if able. + # Cannot format Union Clauses if @arg is a string + def pretty_value + return from_query(@arg, pretty: true) if @arg.is_a? Query + value end - def from_query(value) - from_string(value.to_cypher) + # The query argument stored by the union clause may be mutated if the + # query object result is retreaved by `.pluck()` (see Query#pluck) + # If so, then the cached union clause value should be tossed + def reset_value! + @value = nil + return self end class << self @@ -761,7 +772,7 @@ def clause_string(clauses, pretty) strings = clause_strings(clauses, pretty) stripped_string = strings.join(clause_join) stripped_string.strip! - (pretty && strings.size > 1) ? PRETTY_NEW_LINE + stripped_string : stripped_string + (pretty && strings.size > 1) ? self::PRETTY_NEW_LINE + stripped_string : stripped_string end # If `.union()` was called with `all: true` option, insert 'UNION ALL' clause @@ -775,7 +786,7 @@ def clause_strings(clauses, pretty) end if pretty - "#{clause_color}#{clause_keyword}#{ANSI::CLEAR} #{clause.value}" + PRETTY_NEW_LINE + "#{clause_color}#{clause_keyword}#{ANSI::CLEAR}" + "\n" + clause.pretty_value + "\n" else "#{clause_keyword} #{clause.value}" end From 31aeb022ecd1d6ff1e8561dfc2bddd9043c3c8e5 Mon Sep 17 00:00:00 2001 From: John Carroll Date: Wed, 27 Dec 2017 21:54:09 -0800 Subject: [PATCH 5/6] add: comment --- lib/neo4j-core/query.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/neo4j-core/query.rb b/lib/neo4j-core/query.rb index c2a035ed..535c5cfb 100644 --- a/lib/neo4j-core/query.rb +++ b/lib/neo4j-core/query.rb @@ -163,6 +163,7 @@ def inspect # DETACH DELETE clause # @return [Query] + # This ordering of the METHODS will be used when constructing the final cypher query 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 return order skip limit union] # rubocop:disable Metrics/LineLength BREAK_METHODS = %(with call) From a0bbf243f7f0b4c7f3a2df9941541d6a8a63d1f1 Mon Sep 17 00:00:00 2001 From: John Carroll Date: Wed, 27 Dec 2017 22:04:45 -0800 Subject: [PATCH 6/6] fix: formatting --- lib/neo4j-core/query.rb | 1 - lib/neo4j-core/query_clauses.rb | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/neo4j-core/query.rb b/lib/neo4j-core/query.rb index 535c5cfb..07e08585 100644 --- a/lib/neo4j-core/query.rb +++ b/lib/neo4j-core/query.rb @@ -175,7 +175,6 @@ def inspect DEFINED_CLAUSES[clause.to_sym] = clause_class define_method(clause) do |*args| - # the args splat will contain the method options, if any were given build_deeper_query(clause_class, args).ergo do |result| BREAK_METHODS.include?(clause) ? result.break : result end diff --git a/lib/neo4j-core/query_clauses.rb b/lib/neo4j-core/query_clauses.rb index a2463e2b..c5536bb0 100644 --- a/lib/neo4j-core/query_clauses.rb +++ b/lib/neo4j-core/query_clauses.rb @@ -772,7 +772,6 @@ def clause_string(clauses, pretty) strings = clause_strings(clauses, pretty) stripped_string = strings.join(clause_join) stripped_string.strip! - (pretty && strings.size > 1) ? self::PRETTY_NEW_LINE + stripped_string : stripped_string end # If `.union()` was called with `all: true` option, insert 'UNION ALL' clause @@ -794,7 +793,7 @@ def clause_strings(clauses, pretty) end def clause_join(options = {}) - ' ' + '' end end end