module Neo4j module Core module Cypher module MathFunctions def abs(value=nil) _add_math_func(:abs, value) end def sqrt(value=nil) _add_math_func(:sqrt, value) end def round(value=nil) _add_math_func(:round, value) end def sign(value=nil) _add_math_func(:sign, value) end def _add_math_func(name, value=nil) value ||= self.respond_to?(:var_name) ? self.var_name : to_s expressions.delete(self) Property.new(expressions, nil, name).to_function!(value) end end module MathOperator def -(other) ExprOp.new(self, other, '-') end def +(other) ExprOp.new(self, other, '+') end end module Comparable def <(other) ExprOp.new(self, other, '<') end def <=(other) ExprOp.new(self, other, '<=') end def =~(other) ExprOp.new(self, other, '=~') end def >(other) ExprOp.new(self, other, '>') end def >=(other) ExprOp.new(self, other, '>=') end ## Only in 1.9 if RUBY_VERSION > "1.9.0" eval %{ def !=(other) other.is_a?(String) ? ExprOp.new(self, other, "!=") : super end } end def ==(other) if other.is_a?(Fixnum) || other.is_a?(String) || other.is_a?(Regexp) ExprOp.new(self, other, "=") else super end end end module PredicateMethods def all?(&block) self.respond_to?(:iterable) Predicate.new(expressions, :op => 'all', :clause => :where, :input => input, :iterable => iterable, :predicate_block => block) end def extract(&block) Predicate.new(expressions, :op => 'extract', :clause => :return, :input => input, :iterable => iterable, :predicate_block => block) end def filter(&block) Predicate.new(expressions, :op => 'filter', :clause => :return, :input => input, :iterable => iterable, :predicate_block => block) end def any?(&block) Predicate.new(@expressions, :op => 'any', :clause => :where, :input => input, :iterable => iterable, :predicate_block => block) end def none?(&block) Predicate.new(@expressions, :op => 'none', :clause => :where, :input => input, :iterable => iterable, :predicate_block => block) end def single?(&block) Predicate.new(@expressions, :op => 'single', :clause => :where, :input => input, :iterable => iterable, :predicate_block => block) end end module Variable attr_accessor :return_method def distinct self.return_method = {:name => 'distinct', :bracket => false} self end def [](prop_name) Property.new(expressions, self, prop_name) end def as(v) @var_name = v self end def neo_id Property.new(@expressions, self, 'ID').to_function! end def property?(p) p = Property.new(expressions, self, p) p.binary_operator("has") end def exist? p = Property.new(expressions, self, p) p.binary_operator("", " is null") end end module Matchable # This operator means related to, without regard to type or direction. # @param [Symbol, #var_name] other either a node (Symbol, #var_name) # @return [MatchRelLeft, MatchNode] def <=>(other) MatchNode.new(self, other, expressions, :both) end # This operator means outgoing related to # @param [Symbol, #var_name, String] other the relationship # @return [MatchRelLeft, MatchNode] def >(other) MatchRelLeft.new(self, other, expressions, :outgoing) end # This operator means any direction related to # @param (see #>) # @return [MatchRelLeft, MatchNode] def -(other) MatchRelLeft.new(self, other, expressions, :both) end # This operator means incoming related to # @param (see #>) # @return [MatchRelLeft, MatchNode] def <(other) MatchRelLeft.new(self, other, expressions, :incoming) end # Outgoing relationship to other node # @param [Symbol, #var_name] other either a node (Symbol, #var_name) # @return [MatchRelLeft, MatchNode] def >>(other) MatchNode.new(self, other, expressions, :outgoing) end # Incoming relationship to other node # @param [Symbol, #var_name] other either a node (Symbol, #var_name) # @return [MatchRelLeft, MatchNode] def <<(other) MatchNode.new(self, other, expressions, :incoming) end end class Expression attr_reader :expressions attr_accessor :separator, :clause def initialize(expressions, clause) @clause = clause @expressions = expressions insert_last(clause) @separator = "," end def insert_last(clause) i = @expressions.reverse.index { |e| e.clause == clause } if i.nil? @expressions << self else pos = @expressions.size - i @expressions.insert(pos, self) end end def prefixes {:start => "START", :where => " WHERE", :match => " MATCH", :return => " RETURN", :order_by => " ORDER BY", :skip => " SKIP", :limit => " LIMIT"} end def prefix prefixes[clause] end def valid? true end end class Property attr_reader :expressions, :var_name include Comparable include MathOperator include MathFunctions include PredicateMethods def initialize(expressions, var, prop_name) @var = var.respond_to?(:var_name) ? var.var_name : var @expressions = expressions @prop_name = prop_name @var_name = @prop_name ? "#{@var.to_s}.#{@prop_name}" : @var.to_s end def to_function!(var = @var.to_s) @var_name = "#{@prop_name}(#{var})" self end def as(new_name) @var_name = "#{@var_name} AS #{new_name}" end # required by the Predicate Methods Module # @see PredicateMethods def iterable var_name end def input self end def in?(values) binary_operator("", " IN [#{values.map { |x| %Q["#{x}"] }.join(',')}]") end def distinct @var_name = "distinct #{@var_name}" self end def length @prop_name = "length" to_function! self end %w[count sum avg min max collect head last tail].each do |meth_name| define_method(meth_name) do function(meth_name.to_s) end end def function(func_name_pre, func_name_post = "") ExprOp.new(self, nil, func_name_pre, func_name_post) end def binary_operator(op, post_fix = "") ExprOp.new(self, nil, op, post_fix).binary! end end class Start < Expression attr_reader :var_name include Variable include Matchable def initialize(var_name, expressions) @var_name = "#{var_name}#{expressions.size}" super(expressions, :start) end end class StartNode < Start attr_reader :nodes def initialize(nodes, expressions) super("n", expressions) @nodes = nodes.map { |n| n.respond_to?(:neo_id) ? n.neo_id : n } end def to_s "#{var_name}=node(#{nodes.join(',')})" end end class StartRel < Start attr_reader :rels def initialize(rels, expressions) super("r", expressions) @rels = rels.map { |n| n.respond_to?(:neo_id) ? n.neo_id : n } end def to_s "#{var_name}=relationship(#{rels.join(',')})" end end class NodeQuery < Start attr_reader :index_name, :query def initialize(index_class, query, index_type, expressions) super("n", expressions) @index_name = index_class.index_name_for_type(index_type) @query = query end def to_s "#{var_name}=node:#{index_name}(#{query})" end end class NodeLookup < Start attr_reader :index_name, :query def initialize(index_class, key, value, expressions) super("n", expressions) index_type = index_class.index_type(key.to_s) raise "No index on #{index_class} property #{key}" unless index_type @index_name = index_class.index_name_for_type(index_type) @query = %Q[#{key}="#{value}"] end def to_s %Q[#{var_name}=node:#{index_name}(#{query})] end end # The return statement in the cypher query class Return < Expression attr_reader :var_name def initialize(name_or_ref, expressions, opts = {}) super(expressions, :return) @name_or_ref = name_or_ref @name_or_ref.referenced! if @name_or_ref.respond_to?(:referenced!) @var_name = @name_or_ref.respond_to?(:var_name) ? @name_or_ref.var_name : @name_or_ref.to_s opts.each_pair { |k, v| self.send(k, v) } end def return_method @name_or_ref.respond_to?(:return_method) && @name_or_ref.return_method end def as_return_method if return_method[:bracket] "#{return_method[:name]}(#@var_name)" else "#{return_method[:name]} #@var_name" end end # Specifies an ORDER BY cypher query # @param [Property] props the properties which should be sorted # @return self def asc(*props) @order_by ||= OrderBy.new(expressions) @order_by.asc(props) self end # Specifies an ORDER BY cypher query # @param [Property] props the properties which should be sorted # @return self def desc(*props) @order_by ||= OrderBy.new(expressions) @order_by.desc(props) self end # Creates a SKIP cypher clause # @param [Fixnum] val the number of entries to skip # @return self def skip(val) Skip.new(expressions, val) self end # Creates a LIMIT cypher clause # @param [Fixnum] val the number of entries to limit # @return self def limit(val) Limit.new(expressions, val) self end def to_s return_method ? as_return_method : var_name.to_s end end class Skip < Expression def initialize(expressions, value) super(expressions, :skip) @value = value end def to_s @value end end class Limit < Expression def initialize(expressions, value) super(expressions, :limit) @value = value end def to_s @value end end class OrderBy < Expression def initialize(expressions) super(expressions, :order_by) end def asc(props) @asc ||= [] @asc += props end def desc(props) @desc ||= [] @desc += props end def to_s s = [] s << @asc.map { |p| p.var_name }.join(",") if @asc s << @desc.map { |p| p.var_name.to_s + " DESC" }.join(",") if @desc s.join(',') end end class Match < Expression attr_reader :dir, :expressions, :left, :right, :var_name, :dir_op attr_accessor :algorithm, :next, :prev include Variable def initialize(left, right, expressions, dir, dir_op) super(expressions, :match) @var_name = "m#{expressions.size}" @dir = dir @dir_op = dir_op @prev = left if left.is_a?(Match) @left = left @right = right end def nodes Entities.new(@expressions, "nodes", self) end def rels Entities.new(@expressions, "relationships", self) end def length self.return_method = {:name => 'length', :bracket => true} self end def find_match_start c = self while (c.prev) do c = c.prev end c end def left_var_name @left.respond_to?(:var_name) ? @left.var_name : @left.to_s end def right_var_name @right.respond_to?(:var_name) ? @right.var_name : @right.to_s end def right_expr @right.respond_to?(:expr) ? @right.expr : right_var_name end def referenced! @referenced = true end def referenced? !!@referenced end def to_s curr = find_match_start result = (referenced? || curr.referenced?) ? "#{var_name} = " : "" result << (algorithm ? "#{algorithm}(" : "") begin result << curr.expr end while (curr = curr.next) result << ")" if algorithm result end end class MatchRelLeft < Match def initialize(left, right, expressions, dir) super(left, right, expressions, dir, dir == :incoming ? '<-' : '-') end # @param [Symbol,NodeVar,String] other part of the match cypher statement. # @return [MatchRelRight] the right part of an relationship cypher query. def >(other) expressions.delete(self) self.next = MatchRelRight.new(self, other, expressions, :outgoing) end # @see #> # @return (see #>) def <(other) expressions.delete(self) self.next = MatchRelRight.new(self, other, expressions, :incoming) end # @see #> # @return (see #>) def -(other) expressions.delete(self) self.next = MatchRelRight.new(self, other, expressions, :both) end # @return [String] a cypher string for this match. def expr if prev # we have chained more then one relationships in a match expression "#{dir_op}[#{right_expr}]" else # the right is an relationship and could be an expressions, e.g "r?" "(#{left_var_name})#{dir_op}[#{right_expr}]" end end end class MatchRelRight < Match # @param left the left part of the query # @param [Symbol,NodeVar,String] right part of the match cypher statement. def initialize(left, right, expressions, dir) super(left, right, expressions, dir, dir == :outgoing ? '->' : '-') end # @param [Symbol,NodeVar,String] other part of the match cypher statement. # @return [MatchRelLeft] the right part of an relationship cypher query. def >(other) expressions.delete(self) self.next = MatchRelLeft.new(self, other, expressions, :outgoing) end # @see #> # @return (see #>) def <(other) expressions.delete(self) self.next = MatchRelLeft.new(self, other, expressions, :incoming) end # @see #> # @return (see #>) def -(other) expressions.delete(self) self.next = MatchRelLeft.new(self, other, expressions, :both) end # @return [String] a cypher string for this match. def expr "#{dir_op}(#{right_var_name})" end end class MatchNode < Match attr_reader :dir_op def initialize(left, right, expressions, dir) dir_op = case dir when :outgoing then "-->" when :incoming then "<--" when :both then "--" end super(left, right, expressions, dir, dir_op) end # @return [String] a cypher string for this match. def expr if prev # we have chained more then one relationships in a match expression "#{dir_op}(#{right_expr})" else # the right is an relationship and could be an expressions, e.g "r?" "(#{left_var_name})#{dir_op}(#{right_expr})" end end def <<(other) expressions.delete(self) self.next = MatchNode.new(self, other, expressions, :incoming) end def >>(other) expressions.delete(self) self.next = MatchNode.new(self, other, expressions, :outgoing) end # @param [Symbol,NodeVar,String] other part of the match cypher statement. # @return [MatchRelRight] the right part of an relationship cypher query. def >(other) expressions.delete(self) self.next = MatchRelLeft.new(self, other, expressions, :outgoing) end # @see #> # @return (see #>) def <(other) expressions.delete(self) self.next = MatchRelLeft.new(self, other, expressions, :incoming) end # @see #> # @return (see #>) def -(other) expressions.delete(self) self.next = MatchRelLeft.new(self, other, expressions, :both) end end # Represents an unbound node variable used in match statements class NodeVar include Variable include Matchable # @return the name of the variable attr_reader :var_name attr_reader :expressions def initialize(expressions, variables) @var_name = "v#{variables.size}" variables << self @expressions = expressions end # @return [String] a cypher string for this node variable def to_s var_name end end # represent an unbound relationship variable used in match,where,return statement class RelVar include Variable attr_reader :var_name, :expr, :expressions def initialize(expressions, variables, expr) variables << self @expr = expr @expressions = expressions guess = expr ? /([[:alpha:]]*)/.match(expr)[1] : "" @var_name = guess.empty? ? "v#{variables.size}" : guess end def rel_type Property.new(@expressions, self, 'type').to_function! end # @return [String] a cypher string for this relationship variable def to_s var_name end end class ExprOp < Expression attr_reader :left, :right, :op, :neg, :post_fix include MathFunctions def initialize(left, right, op, post_fix = "") super(left.expressions, :where) @op = op @post_fix = post_fix self.expressions.delete(left) self.expressions.delete(right) @left = quote(left) if regexp?(right) @op = "=~" @right = to_regexp(right) else @right = right && quote(right) end @neg = nil end def separator " " end def quote(val) if val.respond_to?(:var_name) val.var_name else val.is_a?(String) ? %Q["#{val}"] : val end end def regexp?(right) @op == "=~" || right.is_a?(Regexp) end def to_regexp(val) %Q[/#{val.respond_to?(:source) ? val.source : val.to_s}/] end def count ExprOp.new(self, nil, 'count') end def &(other) ExprOp.new(self, other, "and") end def |(other) ExprOp.new(self, other, "or") end def -@ @neg = "not" self end def not @neg = "not" self end # Only in 1.9 if RUBY_VERSION > "1.9.0" eval %{ def ! @neg = "not" self end } end def left_to_s left.is_a?(ExprOp) ? "(#{left})" : left end def right_to_s right.is_a?(ExprOp) ? "(#{right})" : right end def binary! @binary = true self end def valid? # puts "valid? @binary=#{@binary} (#@left #@op #@right) in clause #{clause} ret #{@binary ? !!@left : !!@left && !!@right}" # it is only valid in a where clause if it's either binary or it has right and left values @binary ? @left : @left && @right end def to_s if @right neg ? "#{neg}(#{left_to_s} #{op} #{right_to_s})" : "#{left_to_s} #{op} #{right_to_s}" else # binary operator neg ? "#{neg}#{op}(#{left_to_s}#{post_fix})" : "#{op}(#{left_to_s}#{post_fix})" end end end class Where < Expression def initialize(expressions, where_statement = nil) super(expressions, :where) @where_statement = where_statement end def to_s @where_statement.to_s end end class Predicate < Expression attr_accessor :params def initialize(expressions, params) @params = params @identifier = :x params[:input].referenced! if params[:input].respond_to?(:referenced!) super(expressions, params[:clause]) end def identifier(i) @identifier = i self end def to_s input = params[:input] if input.kind_of?(Property) yield_param = Property.new([], @identifier, nil) args = "" else yield_param = NodeVar.new([], []).as(@identifier.to_sym) args = "(#{input.var_name})" end context = Neo4j::Cypher.new(yield_param, ¶ms[:predicate_block]) context.expressions.each { |e| e.clause = nil } if params[:clause] == :return where_or_colon = ':' else where_or_colon = 'WHERE' end predicate_value = context.to_s[1..-1] # skip separator , "#{params[:op]}(#@identifier in #{params[:iterable]}#{args} #{where_or_colon} #{predicate_value})" end end class Entities include PredicateMethods attr_reader :input, :expressions, :iterable def initialize(expressions, iterable, input) @iterable = iterable @input = input @expressions = expressions end end end end end