├── .coveralls.yml ├── .gitignore ├── .overcommit.yml ├── .pullreview.yml ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .travis.yml ├── .yardopts ├── CHANGELOG.md ├── Dockerfile ├── Gemfile ├── Guardfile ├── ISSUE_TEMPLATE.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── Rakefile ├── bin └── rake ├── code_of_conduct.md ├── lib ├── neo4j-core.rb └── neo4j │ ├── ansi.rb │ ├── core.rb │ ├── core │ ├── config.rb │ ├── cypher_session.rb │ ├── cypher_session │ │ ├── adaptors.rb │ │ ├── adaptors │ │ │ ├── bolt.rb │ │ │ ├── bolt │ │ │ │ ├── chunk_writer_io.rb │ │ │ │ └── pack_stream.rb │ │ │ ├── driver.rb │ │ │ ├── embedded.rb │ │ │ ├── has_uri.rb │ │ │ ├── http.rb │ │ │ └── schema.rb │ │ ├── responses.rb │ │ ├── responses │ │ │ ├── bolt.rb │ │ │ ├── driver.rb │ │ │ ├── embedded.rb │ │ │ └── http.rb │ │ ├── result.rb │ │ ├── transactions.rb │ │ └── transactions │ │ │ ├── bolt.rb │ │ │ ├── driver.rb │ │ │ ├── embedded.rb │ │ │ └── http.rb │ ├── helpers.rb │ ├── instrumentable.rb │ ├── label.rb │ ├── logging.rb │ ├── node.rb │ ├── path.rb │ ├── query.rb │ ├── query_clauses.rb │ ├── query_find_in_batches.rb │ ├── rake_tasks_deprecation.rake │ ├── relationship.rb │ ├── version.rb │ └── wrappable.rb │ └── transaction.rb ├── neo4j-core.gemspec └── spec ├── fixtures ├── adirectory │ └── .githold ├── neo4j.properties └── notadirectory ├── helpers.rb ├── lib ├── yard_rspec.rb └── yard_rspec │ └── templates │ └── default │ └── method_details │ ├── html │ └── specs.erb │ ├── setup.rb │ └── text │ └── specs.erb ├── neo4j └── core │ ├── cypher_session │ ├── adaptors │ │ ├── bolt │ │ │ └── pack_stream_spec.rb │ │ ├── bolt_spec.rb │ │ ├── driver_spec.rb │ │ ├── embedded_spec.rb │ │ ├── has_uri_spec.rb │ │ └── http_spec.rb │ └── cypher_error_spec.rb │ ├── cypher_session_spec.rb │ ├── query_parameters_spec.rb │ ├── query_spec.rb │ ├── shared_examples │ └── adaptor.rb │ ├── transaction_spec.rb │ └── wrappable_spec.rb ├── neo4j_spec_helpers.rb └── spec_helper.rb /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore netbeans specific stuff 2 | nbproject 3 | 4 | # Can we agree to install binstubs as `bundle install --binstubs=b`? 5 | b/ 6 | .yardoc 7 | 8 | # ignore Rubymine 9 | .idea 10 | 11 | doc 12 | pkg/ 13 | 14 | .rvmrc 15 | .bundle 16 | tmp 17 | db/ 18 | 19 | /neo4j/ 20 | 21 | # jedit 22 | *~ 23 | 24 | target 25 | 26 | # vim 27 | .*.sw[a-z] 28 | 29 | Gemfile.lock 30 | coverage 31 | .DS_Store 32 | .ruby-version 33 | .ruby-gemset 34 | 35 | # dotenv 36 | .env 37 | .ruby_gemset 38 | .ruby_version -------------------------------------------------------------------------------- /.overcommit.yml: -------------------------------------------------------------------------------- 1 | 2 | PreCommit: 3 | Rubocop: 4 | enabled: true 5 | on_warn: fail # Treat all warnings as failures 6 | 7 | TrailingWhitespace: 8 | enabled: true 9 | exclude: 10 | - '**/db/structure.sql' # Ignore trailing whitespace in generated files 11 | - 'ISSUE_TEMPLATE.md' 12 | - 'PULL_REQUEST_TEMPLATE.md' 13 | 14 | PostCheckout: 15 | ALL: # Special hook name that customizes all hooks of this type 16 | quiet: true # Change all post-checkout hooks to only display output on failure 17 | 18 | IndexTags: 19 | enabled: true # Generate a tags file with `ctags` each time HEAD changes 20 | -------------------------------------------------------------------------------- /.pullreview.yml: -------------------------------------------------------------------------------- 1 | --- 2 | rules: 3 | ignore: 4 | - assignment_in_conditional 5 | - extra_blank_line_detected 6 | - prefer_ruby_19_new_lambda_syntax 7 | - shadowing_outer_local_variable 8 | - space_inside_closing_curly_brace_missing 9 | - space_inside_opening_curly_brace_missing 10 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | 4 | # Should remove when this issue has been resolved: 5 | # https://github.com/bbatsov/rubocop/issues/1544 6 | Lint/ShadowingOuterLocalVariable: 7 | Enabled: false 8 | 9 | 10 | 11 | #--------------------------- 12 | # Style configuration 13 | #--------------------------- 14 | 15 | AllCops: 16 | DisplayCopNames: true 17 | DisplayStyleGuide: true 18 | 19 | Style/HashSyntax: 20 | Enabled: true 21 | EnforcedStyle: ruby19 22 | 23 | Layout/SpaceInsideHashLiteralBraces: 24 | Enabled: true 25 | EnforcedStyle: no_space 26 | 27 | Layout/SpaceAroundOperators: 28 | AllowForAlignment: true 29 | 30 | # Offense count: 7 31 | # Cop supports --auto-correct. 32 | # Configuration parameters: EnforcedStyle, SupportedStyles, ConsistentQuotesInMultiline. 33 | # SupportedStyles: single_quotes, double_quotes 34 | Style/StringLiterals: 35 | Enabled: true 36 | 37 | # I think that these are fine if used carefully 38 | Style/MultilineBlockChain: 39 | Enabled: false 40 | 41 | 42 | # I think this one is broken... 43 | Naming/FileName: 44 | Enabled: false 45 | 46 | Style/SignalException: 47 | EnforcedStyle: semantic 48 | 49 | #--------------------------- 50 | # Don't intend to fix these: 51 | #--------------------------- 52 | 53 | # Cop supports --auto-correct. 54 | # Reason: Double spaces can be useful for grouping code 55 | Layout/EmptyLines: 56 | Enabled: false 57 | 58 | # Cop supports --auto-correct. 59 | # Reason: I have very big opinions on this one. See: 60 | # https://github.com/bbatsov/ruby-style-guide/issues/329 61 | # https://github.com/bbatsov/ruby-style-guide/pull/325 62 | Style/NegatedIf: 63 | Enabled: false 64 | 65 | # Cop supports --auto-correct. 66 | # Reason: I'm fine either way on this, but could maybe be convinced that this should be enforced 67 | Style/Not: 68 | Enabled: false 69 | 70 | # Cop supports --auto-correct. 71 | # Reason: I'm fine with this 72 | Style/PerlBackrefs: 73 | Enabled: false 74 | 75 | # Configuration parameters: Methods. 76 | # Reason: We should be able to specify full variable names, even if it's only one line 77 | Style/SingleLineBlockParams: 78 | Enabled: false 79 | 80 | # Offense count: 1 81 | # Reason: Switched `extend self` to `module_function` in id_property.rb but that caused errors 82 | Style/ModuleFunction: 83 | Enabled: false 84 | 85 | # Configuration parameters: AllowSafeAssignment. 86 | # Reason: I'm a proud user of assignment in conditionals. 87 | Lint/AssignmentInCondition: 88 | Enabled: false 89 | 90 | # Reason: I'm proud to be part of the double negative Ruby tradition 91 | Style/DoubleNegation: 92 | Enabled: false 93 | 94 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2018-05-21 21:54:22 -0400 using RuboCop version 0.56.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 1 10 | # Configuration parameters: Include. 11 | # Include: **/*.gemfile, **/Gemfile, **/gems.rb 12 | Bundler/DuplicatedGem: 13 | Exclude: 14 | - 'Gemfile' 15 | 16 | # Offense count: 2 17 | Lint/AmbiguousBlockAssociation: 18 | Exclude: 19 | - 'spec/neo4j/core/shared_examples/adaptor.rb' 20 | 21 | # Offense count: 1 22 | Lint/HandleExceptions: 23 | Exclude: 24 | - 'spec/lib/yard_rspec.rb' 25 | 26 | # Offense count: 1 27 | Lint/UselessAssignment: 28 | Exclude: 29 | - 'spec/lib/yard_rspec.rb' 30 | 31 | # Offense count: 15 32 | Metrics/AbcSize: 33 | Max: 17 34 | 35 | # Offense count: 32 36 | # Configuration parameters: CountComments, ExcludedMethods. 37 | Metrics/BlockLength: 38 | Max: 723 39 | 40 | # Offense count: 5 41 | # Configuration parameters: CountComments. 42 | Metrics/ClassLength: 43 | Max: 189 44 | 45 | # Offense count: 20 46 | # Configuration parameters: CountComments. 47 | Metrics/MethodLength: 48 | Max: 14 49 | 50 | # Offense count: 1 51 | Naming/MemoizedInstanceVariableName: 52 | Exclude: 53 | - 'lib/neo4j/core/cypher_session/adaptors/bolt.rb' 54 | 55 | # Offense count: 3 56 | # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. 57 | # AllowedNames: io, id, to, by, on, in, at 58 | Naming/UncommunicativeMethodParamName: 59 | Exclude: 60 | - 'lib/neo4j/core/cypher_session/adaptors.rb' 61 | - 'lib/neo4j/core/cypher_session/adaptors/bolt/pack_stream.rb' 62 | - 'spec/neo4j/core/shared_examples/adaptor.rb' 63 | 64 | # Offense count: 55 65 | Style/Documentation: 66 | Enabled: false 67 | 68 | # Offense count: 1 69 | Style/EvalWithLocation: 70 | Exclude: 71 | - 'spec/neo4j/core/query_spec.rb' 72 | 73 | # Offense count: 1 74 | # Configuration parameters: MinBodyLength. 75 | Style/GuardClause: 76 | Exclude: 77 | - 'lib/neo4j/core/cypher_session/adaptors/bolt.rb' 78 | 79 | # Offense count: 2 80 | Style/IdenticalConditionalBranches: 81 | Exclude: 82 | - 'lib/neo4j/core/cypher_session/transactions.rb' 83 | 84 | # Offense count: 55 85 | # Cop supports --auto-correct. 86 | Style/MutableConstant: 87 | Exclude: 88 | - 'Rakefile' 89 | - 'lib/neo4j/ansi.rb' 90 | - 'lib/neo4j/core/cypher_session/adaptors.rb' 91 | - 'lib/neo4j/core/cypher_session/adaptors/bolt.rb' 92 | - 'lib/neo4j/core/cypher_session/adaptors/bolt/pack_stream.rb' 93 | - 'lib/neo4j/core/cypher_session/adaptors/embedded.rb' 94 | - 'lib/neo4j/core/cypher_session/adaptors/http.rb' 95 | - 'lib/neo4j/core/cypher_session/responses.rb' 96 | - 'lib/neo4j/core/query.rb' 97 | - 'lib/neo4j/core/query_clauses.rb' 98 | - 'lib/neo4j/core/version.rb' 99 | - 'spec/neo4j/core/query_spec.rb' 100 | 101 | # Offense count: 2 102 | # Cop supports --auto-correct. 103 | # Configuration parameters: EnforcedStyle. 104 | # SupportedStyles: implicit, explicit 105 | Style/RescueStandardError: 106 | Exclude: 107 | - 'Rakefile' 108 | - 'lib/neo4j/core/cypher_session/adaptors.rb' 109 | 110 | # Offense count: 373 111 | # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. 112 | # URISchemes: http, https 113 | Metrics/LineLength: 114 | Max: 173 115 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_script: 2 | - "travis_retry bin/rake neo4j:install[$NEO4J_VERSION] --trace" 3 | - "bin/rake neo4j:config[development,7474] --trace" 4 | - "if [ -f ./db/neo4j/development/conf/neo4j-wrapper.conf ]; then WRAPPER=-wrapper; fi" 5 | - "echo 'dbms.memory.pagecache.size=600m' >> ./db/neo4j/development/conf/neo4j.conf" 6 | - "echo 'dbms.memory.heap.max_size=600' >> ./db/neo4j/development/conf/neo4j$WRAPPER.conf" 7 | - "echo 'dbms.memory.heap.initial_size=600' >> ./db/neo4j/development/conf/neo4j$WRAPPER.conf" 8 | - "bin/rake neo4j:start --trace" 9 | - "while [ $((curl localhost:7474/ > /dev/null 2>&1); echo $?) -ne 0 ]; do sleep 1; done" 10 | script: 11 | - "bundle exec rspec $RSPEC_OPTS" 12 | language: ruby 13 | cache: bundler 14 | sudo: false 15 | jdk: openjdk8 16 | rvm: 17 | - 2.4.5 18 | - 2.1.10 19 | - jruby-9.2.5.0 20 | env: 21 | global: 22 | - JRUBY_OPTS="--debug -J-Xmx1024m -Xcompile.invokedynamic=false -J-XX:+TieredCompilation -J-XX:TieredStopAtLevel=1 -J-noverify -Xcompile.mode=OFF" 23 | - NEO4J_URL="http://localhost:7474" 24 | - NEO4J_BOLT_URL="bolt://localhost:7472" 25 | matrix: 26 | - NEO4J_VERSION=community-3.3.1 27 | matrix: 28 | include: 29 | - script: "bundle exec rubocop" 30 | rvm: 2.4.5 31 | jdk: 32 | before_script: 33 | env: "RUBOCOP=true" 34 | 35 | # Older versions of Neo4j with latest version of Ruby 36 | - rvm: 2.4.5 37 | env: NEO4J_VERSION=community-3.2.8 38 | - rvm: 2.4.5 39 | env: NEO4J_VERSION=community-3.1.7 40 | - rvm: 2.4.5 41 | env: NEO4J_VERSION=community-2.3.11 42 | 43 | # Older versions of Neo4j with latest version of jRuby 44 | - rvm: jruby-9.2.5.0 45 | env: NEO4J_VERSION=community-3.2.8 46 | - rvm: jruby-9.2.5.0 47 | env: NEO4J_VERSION=community-3.1.7 48 | - rvm: jruby-9.2.5.0 49 | env: NEO4J_VERSION=community-2.3.11 50 | 51 | # NEW_NEO4J_SESSIONS 52 | - rvm: jruby-9.2.5.0 53 | env: RSPEC_OPTS="--tag new_cypher_session" NEO4J_VERSION=community-2.3.11 NEW_NEO4J_SESSIONS=true 54 | 55 | # Enterprise 56 | - rvm: jruby-9.2.5.0 57 | env: NEO4J_VERSION=enterprise-3.5.1 58 | 59 | after_failure: 60 | - cat ./db/neo4j/development/logs/neo4j.log 61 | - cat ./db/neo4j/development/logs/debug.log 62 | - cat ./db/neo4j/development/conf/neo4j.conf 63 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --title 'Neo4j::Core API Documentation' 2 | --no-private -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:latest 2 | 3 | RUN apt-get update && \ 4 | DEBIAN_FRONTEND=noninteractive apt-get install -y locales && \ 5 | apt-get install -y build-essential libjson-c-dev && \ 6 | apt-get clean -y 7 | RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen 8 | 9 | RUN mkdir /usr/src/app 10 | WORKDIR /usr/src/app/ 11 | 12 | COPY Gemfile* ./ 13 | COPY neo4j-core.gemspec ./ 14 | COPY lib/neo4j-core/version.rb ./lib/neo4j-core/ 15 | RUN bundle 16 | 17 | ADD . ./ 18 | 19 | ENV LANG en_US.UTF-8 20 | ENV LANGUAGE en_US.UTF-8 21 | ENV LC_ALL en_US.UTF-8 22 | 23 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | # gem 'neo4j-advanced', '>= 1.8.1', '< 2.0', :require => false 6 | # gem 'neo4j-enterprise', '>= 1.8.1', '< 2.0', :require => false 7 | 8 | gem 'tins', '< 1.7' if RUBY_VERSION.to_f < 2.0 9 | 10 | group 'development' do 11 | if RUBY_PLATFORM =~ /java/ 12 | gem 'neo4j-ruby-driver', path: '../neo4j-ruby-driver' if ENV['USE_LOCAL_DRIVER'] 13 | else 14 | gem 'guard-rspec', require: false 15 | end 16 | if RUBY_VERSION.to_f < 2.0 17 | gem 'overcommit', '< 0.35.0' 18 | gem 'term-ansicolor', '< 1.4' 19 | else 20 | gem 'overcommit' 21 | end 22 | end 23 | 24 | group 'test' do 25 | gem 'activesupport' 26 | gem 'coveralls', require: false 27 | gem 'dotenv' 28 | gem 'rspec' 29 | gem 'rspec-its' 30 | gem 'simplecov-html', require: false 31 | end 32 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | ## Uncomment and set this to only include directories you want to watch 5 | # directories %w(app lib config test spec feature) 6 | 7 | ## Uncomment to clear the screen before every task 8 | # clearing :on 9 | 10 | ## Guard internally checks for changes in the Guardfile and exits. 11 | ## If you want Guard to automatically start up again, run guard in a 12 | ## shell loop, e.g.: 13 | ## 14 | ## $ while bundle exec guard; do echo "Restarting Guard..."; done 15 | ## 16 | ## Note: if you are using the `directories` clause above and you are not 17 | ## watching the project directory ('.'), the you will want to move the Guardfile 18 | ## to a watched dir and symlink it back, e.g. 19 | # 20 | # $ mkdir config 21 | # $ mv Guardfile config/ 22 | # $ ln -s config/Guardfile . 23 | # 24 | # and, you'll have to watch "config/Guardfile" instead of "Guardfile" 25 | 26 | guard :rubocop, cli: '--auto-correct --display-cop-names' do 27 | watch(/.+\.rb$/) 28 | watch(%r{(?:.+/)?\.rubocop.*\.yml$}) { |m| File.dirname(m[0]) } 29 | end 30 | 31 | guard :rspec, cmd: 'bundle exec rspec' do 32 | watch(%r{^spec/.+_spec\.rb$}) 33 | watch(%r{^lib/(.+)\.rb}) { |m| "spec/lib/#{m[1]}_spec.rb" } 34 | watch('spec/spec_helper.rb') { 'spec' } 35 | end 36 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Additional information which could be helpful if relevent to your issue: 8 | 9 | ### Code example (inline, gist, or repo) 10 | 11 | 12 | 13 | ### Runtime information: 14 | 15 | Neo4j database version: 16 | `neo4j` gem version: 17 | `neo4j-core` gem version: 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Andreas Ronge 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # 2 | 3 | This pull introduces/changes: 4 | * 5 | * 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Neo4j-core 2 | 3 | [![Actively Maintained](https://img.shields.io/badge/Maintenance%20Level-Actively%20Maintained-green.svg)](https://gist.github.com/cheerfulstoic/d107229326a01ff0f333a1d3476e068d) 4 | [![Code Climate](https://codeclimate.com/github/neo4jrb/neo4j-core.svg)](https://codeclimate.com/github/neo4jrb/neo4j-core) 5 | [![Build Status](https://travis-ci.org/neo4jrb/neo4j-core.svg)](https://travis-ci.org/neo4jrb/neo4j-core) 6 | [![Coverage Status](https://coveralls.io/repos/neo4jrb/neo4j-core/badge.svg?branch=master)](https://coveralls.io/r/neo4jrb/neo4j-core?branch=master) 7 | [![PullReview stats](https://www.pullreview.com/github/neo4jrb/neo4j-core/badges/master.svg?)](https://www.pullreview.com/github/neo4jrb/neo4j-core/reviews/master) 8 | 9 | A simple Ruby wrapper around the Neo4j graph database that works with the server and embedded Neo4j API. This gem can be used both from JRuby and normal MRI. 10 | It can be used standalone without the neo4j gem. 11 | 12 | ## Basic usage 13 | 14 | ### Executing Cypher queries 15 | 16 | To make a basic connection to Neo4j to execute Cypher queries, first choose an adaptor. Adaptors for HTTP, Bolt, and Embedded mode (jRuby only) are available. You can create an adaptor like: 17 | 18 | require 'neo4j/core/cypher_session/adaptors/http' 19 | http_adaptor = Neo4j::Core::CypherSession::Adaptors::HTTP.new('http://neo4j:pass@localhost:7474', options) 20 | 21 | # or 22 | 23 | require 'neo4j/core/cypher_session/adaptors/bolt' 24 | bolt_adaptor = Neo4j::Core::CypherSession::Adaptors::Bolt.new('bolt://neo4j:pass@localhost:7687', options) 25 | 26 | # or 27 | 28 | require 'neo4j/core/cypher_session/adaptors/embedded' 29 | neo4j_adaptor = Neo4j::Core::CypherSession::Adaptors::Embedded.new('/file/path/to/graph.db', options) 30 | 31 | The options are specific to each adaptor. See below for details. 32 | 33 | Once you have an adaptor you can create a session like so: 34 | 35 | neo4j_session = Neo4j::Core::CypherSession.new(http_adaptor) 36 | 37 | From there you can make queries with a Cypher string: 38 | 39 | # Basic query 40 | neo4j_session.query('MATCH (n) RETURN n LIMIT 10') 41 | 42 | # Query with parameters 43 | neo4j_session.query('MATCH (n) RETURN n LIMIT {limit}', limit: 10) 44 | 45 | Or via the `Neo4j::Core::Query` class 46 | 47 | query_obj = Neo4j::Core::Query.new.match(:n).return(:n).limit(10) 48 | 49 | neo4j_session.query(query_obj) 50 | 51 | Making multiple queries with one request is supported with the HTTP Adaptor: 52 | 53 | results = neo4j_session.queries do 54 | append 'MATCH (n:Foo) RETURN n LIMIT 10' 55 | append 'MATCH (n:Bar) RETURN n LIMIT 5' 56 | end 57 | 58 | results[0] # results of first query 59 | results[1] # results of second query 60 | 61 | When doing batched queries, there is also a shortcut for getting a new `Neo4j::Core::Query`: 62 | 63 | results = neo4j_session.queries do 64 | append query.match(:n).return(:n).limit(10) 65 | end 66 | 67 | results[0] # result 68 | 69 | ### Adaptor Options 70 | 71 | As mentioned above, each of the adaptors take different sets of options. They are enumerated below: 72 | 73 | #### Shared options 74 | 75 | All adaptors take `wrap_level` as an option. This can be used to control how nodes, relationships, and path data is returned: 76 | 77 | * `wrap_level: :none` will return Ruby hashes 78 | * `wrap_level: :core_entity` will return objects from the `neo4j-core` gem (`Neo4j::Core::Node`, `Neo4j::Core::Relationship`, and `Neo4j::Core::Path` 79 | * `wrap_level: :prop` allows you to define Ruby Procs to do your own wrapping. This is how the `neo4j` gem provides `ActiveNode` and `ActiveRel` objects (see the [`node_wrapper.rb`](https://github.com/neo4jrb/neo4j/blob/master/lib/neo4j/active_node/node_wrapper.rb) and [`rel_wrapper.rb`](https://github.com/neo4jrb/neo4j/blob/master/lib/neo4j/active_rel/rel_wrapper.rb) files for examples on how this works 80 | 81 | All adaptors will also take either a `logger` option with a Ruby logger to define where it will log to. 82 | 83 | All adaptors will also take the `skip_instrumentation` option to skip logging of queries. 84 | 85 | All adaptors will also take the `verbose_query_logs` option which can be `true` or `false` (`false` being the default). This will change the logging to output the source line of code which caused a query to be executed (note that the `skip_instrumentation` should not be set for logging to be produced). 86 | 87 | #### Bolt 88 | 89 | The Bolt adaptor takes `connect_timeout`, `read_timeout`, and `write_timeout` options which define appropriate timeouts. The `connect_timeout` is 10 seconds and the `read_timeout` and `write_timeout` are -1 (no timeout). This is to cause the underlying `net_tcp_client` gem to operate in blocking mode (as opposed to non-blocking mode). When using non-blocking mode problems were found and since the official Neo4j drivers in other languages use blocking mode, this is what this gem uses by default. This issue could potentially be a bug in the handling of the `EAGAIN` signal, but it was not investigated further. Set the read/write timeouts at your own risk. 90 | 91 | The Bolt adaptor also takes an `ssl` option which also corresponds to `net_tcp_client`'s `ssl` option (which, in turn, corresponds to Ruby's `OpenSSL::SSL::SSLContext`). By default SSL is used. For most cloud providers that use public certificate authorities this open generally won't be needed. If you've setup Neo4j yourself you will need to provide the certificate like so: 92 | 93 | ```ruby 94 | cert_store = OpenSSL::X509::Store.new 95 | cert_store.add_file('/the/path/to/your/neo4j.cert') 96 | ssl: {cert_store: cert_store}} 97 | bolt_adaptor = Neo4j::Core::CypherSession::Adaptors::Bolt.new('bolt://neo4j:pass@localhost:7687', ssl: {cert_store: cert_store}) 98 | ``` 99 | 100 | You can also turn SSL off by simply specifying `ssl: false` 101 | 102 | #### HTTP 103 | 104 | Since the HTTP adaptor uses the `faraday` gem under the covers, it takes a `faraday_configurator` option. This allows you to pass in a `Proc` which works just like a Faraday setup block: 105 | 106 | ```ruby 107 | faraday_configurator: proc do |faraday| 108 | # The default configurator uses typhoeus so if you override the configurator you must specify this 109 | faraday.adapter :typhoeus 110 | # Optionally you can instead specify another adaptor 111 | # faraday.use Faraday::Adapter::NetHttpPersistent 112 | 113 | # If you need to set options which would normally be the second argument of `Faraday.new`, you can do the following: 114 | faraday.options[:open_timeout] = 5 115 | faraday.options[:timeout] = 65 116 | # faraday.options[:ssl] = { verify: true } 117 | end 118 | ``` 119 | 120 | #### Embedded 121 | 122 | The Embedded adaptor takes `properties_file` and `properties_map` options which are passed to `loadPropertiesFromFile` and `setConfig` on the `GraphDatabaseBuilder` class from the Neo4j Java API. 123 | 124 | ## Documentation 125 | 126 | Our documentation on ReadTheDocs covers both the `neo4j` and `neo4j-core` gems: 127 | 128 | * http://neo4jrb.readthedocs.org/en/stable/ 129 | 130 | 131 | ## Support 132 | 133 | ### Issues 134 | 135 | [![Next Release](https://badge.waffle.io/neo4jrb/neo4j-core.png?label=Next%20Release&title=Next%20Release) ![In Progress](https://badge.waffle.io/neo4jrb/neo4j-core.png?label=In%20Progress&title=In%20Progress) ![In Master](https://badge.waffle.io/neo4jrb/neo4j-core.png?label=In%20Master&title=In%20Master)](https://waffle.io/neo4jrb/neo4j-core) 136 | 137 | [![Post an issue](https://img.shields.io/badge/Bug%3F-Post%20an%20issue!-blue.svg)](https://waffle.io/neo4jrb/neo4j-core) 138 | 139 | 140 | ### Get Support 141 | 142 | #### Documentation 143 | 144 | All new documentation will be done via our [readthedocs](http://neo4jrb.readthedocs.org) site, though some old documentation has yet to be moved from our [wiki](https://github.com/neo4jrb/neo4j/wiki) (also there is the [neo4j-core wiki](https://github.com/neo4jrb/neo4j-core/wiki)) 145 | 146 | #### Contact Us 147 | 148 | [![StackOverflow](https://img.shields.io/badge/StackOverflow-Ask%20a%20question!-blue.svg)](http://stackoverflow.com/questions/ask?tags=neo4j.rb+neo4j+ruby) [![Gitter](https://img.shields.io/badge/Gitter-Join%20our%20chat!-blue.svg)](https://gitter.im/neo4jrb/neo4j?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Twitter](https://img.shields.io/badge/Twitter-Tweet%20with%20us!-blue.svg)](https://twitter.com/neo4jrb) 149 | 150 | 151 | ## Developers 152 | 153 | ### Original Author 154 | 155 | * [Andreas Ronge](https://github.com/andreasronge) 156 | 157 | ### Previous Maintainers 158 | 159 | * [Brian Underwood](https://github.com/cheerfulstoic) 160 | * [Chris Grigg](https://github.com/subvertallchris) 161 | 162 | ### Current Maintainers 163 | 164 | * [Heinrich Klobuczek](https://github.com/klobuczek) 165 | * [Amit Suryavanshi](https://github.com/amitsuryavanshi) 166 | 167 | Consulting support? Contact [Heinrich](https://gitter.im/klobuczek) and/or [Amit](https://gitter.im/amitTheSongadya_twitter) 168 | 169 | ## Contributing 170 | 171 | Pull request with high test coverage and good [code climate](https://codeclimate.com/github/neo4jrb/neo4j-core) values will be accepted faster. 172 | Notice, only JRuby can run all the tests (embedded and server db). To run tests with coverage: `rake coverage`. 173 | 174 | ## License 175 | * Neo4j.rb - MIT, see the LICENSE file http://github.com/neo4jrb/neo4j-core/tree/master/LICENSE. 176 | * Lucene - Apache, see http://lucene.apache.org/java/docs/features.html 177 | * Neo4j - Dual free software/commercial license, see http://neo4j.org/ 178 | 179 | Notice there are different license for the neo4j-community, neo4j-advanced and neo4j-enterprise jar gems. 180 | Only the neo4j-community gem is by default required. 181 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'bundler/gem_tasks' 3 | require 'neo4j/rake_tasks' 4 | require 'yard' 5 | 6 | load './spec/lib/yard_rspec.rb' 7 | 8 | def jar_path 9 | spec = Gem::Specification.find_by_name('neo4j-community') 10 | gem_root = spec.gem_dir 11 | gem_root + '/lib/neo4j-community/jars' 12 | end 13 | 14 | YARD::Rake::YardocTask.new do |t| 15 | t.files = ['lib/**/*.rb', 'spec/**/*_spec.rb'] 16 | end 17 | 18 | desc 'Run neo4j-core specs' 19 | task 'spec' do 20 | success = system('rspec spec') 21 | abort('RSpec neo4j-core failed') unless success 22 | end 23 | 24 | desc 'Generate coverage report' 25 | task 'coverage' do 26 | ENV['COVERAGE'] = 'true' 27 | rm_rf 'coverage/' 28 | task = Rake::Task['spec'] 29 | task.reenable 30 | task.invoke 31 | end 32 | 33 | task default: [:spec] 34 | 35 | CHARS = ('0'..'z').to_a 36 | def string 37 | Array.new(rand(1_000)) { CHARS.sample }.join 38 | end 39 | 40 | MAX_NUM = 10_00 * 999_999 41 | HALF_MAX_NUM = MAX_NUM.fdiv(2) 42 | def int 43 | rand(MAX_NUM) 44 | end 45 | 46 | def float 47 | (rand * MAX_NUM) - HALF_MAX_NUM 48 | end 49 | 50 | DIFFERENT_QUERIES = [ 51 | ['MATCH (n) RETURN n LIMIT {limit}', -> { {limit: rand(20)} }], 52 | ['MATCH (n) WITH n LIMIT {limit} DELETE n', -> { {limit: rand(5)} }], 53 | ['MATCH (n) SET n.some_prop = {value}', -> { {value: send(%i[string float int].sample)} }], 54 | ['CREATE (n:Report) SET n = {props} RETURN n', -> { {props: {a_string: string, a_int: int, a_float: float}} }] 55 | ] 56 | 57 | task :stress_test, [:times, :local] do |_task, args| 58 | require 'logger' 59 | require 'neo4j/core' 60 | require 'neo4j/core/cypher_session/adaptors/bolt' 61 | system('rm stress_test.log') 62 | 63 | if args[:local] == 'true' 64 | cert_store = OpenSSL::X509::Store.new.tap { |store| store.add_file('./tmp/certificates/neo4j.cert') } 65 | ssl_options = {cert_store: cert_store} 66 | url = 'bolt://neo4j:neo5j@localhost:7687' 67 | else 68 | url = 'bolt://neo4j:y7_mAQaA0RG3pqOmAAVs0hvas_XJWgAG38WOdPWGJ9o@b40527a0.databases.neo4j.io' 69 | ssl_options = {} 70 | end 71 | 72 | # logger_options = {} 73 | logger_options = {logger_location: 'stress_test.log', logger_level: Logger::DEBUG} 74 | 75 | bolt_adaptor = Neo4j::Core::CypherSession::Adaptors::Bolt.new(url, {timeout: 10, ssl: ssl_options}.merge(logger_options)) 76 | 77 | neo4j_session = Neo4j::Core::CypherSession.new(bolt_adaptor) 78 | 79 | i = 0 80 | start = Time.now 81 | args.fetch(:times, 100).to_i.times do 82 | # putc '.' 83 | 84 | begin 85 | query, params = DIFFERENT_QUERIES.sample 86 | params = params.call if params.respond_to?(:call) 87 | params ||= {} 88 | neo4j_session.query(query, params).to_a.inspect 89 | rescue => e 90 | raise e 91 | end 92 | 93 | i += 1 94 | puts "#{i.fdiv(Time.now - start)} i/s" if (i % 20).zero? 95 | end 96 | 97 | puts 'Done!' 98 | end 99 | 100 | # require 'coveralls/rake/task' 101 | # Coveralls::RakeTask.new 102 | # task :test_with_coveralls => [:spec, 'coveralls:push'] 103 | # 104 | # task :default => ['test_with_coveralls'] 105 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rake' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require 'pathname' 12 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', 13 | Pathname.new(__FILE__).realpath) 14 | 15 | require 'rubygems' 16 | require 'bundler/setup' 17 | 18 | load Gem.bin_path('rake', 'rake') 19 | -------------------------------------------------------------------------------- /code_of_conduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [EMAIL ADDRESS COMING SOON]. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /lib/neo4j-core.rb: -------------------------------------------------------------------------------- 1 | # DO NOT ADD ANYTHING HERE 2 | # lib/neo4j/core.rb should be the base for requiring essential files 3 | require 'neo4j/core' 4 | -------------------------------------------------------------------------------- /lib/neo4j/ansi.rb: -------------------------------------------------------------------------------- 1 | module Neo4j 2 | module ANSI 3 | CLEAR = "\e[0m" 4 | BOLD = "\e[1m" 5 | 6 | RED = "\e[31m" 7 | GREEN = "\e[32m" 8 | YELLOW = "\e[33m" 9 | BLUE = "\e[34m" 10 | MAGENTA = "\e[35m" 11 | CYAN = "\e[36m" 12 | WHITE = "\e[37m" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/neo4j/core.rb: -------------------------------------------------------------------------------- 1 | require 'neo4j/core/config' 2 | 3 | require 'neo4j/transaction' 4 | require 'neo4j/core/query' 5 | -------------------------------------------------------------------------------- /lib/neo4j/core/config.rb: -------------------------------------------------------------------------------- 1 | module Neo4j 2 | module Core 3 | module Config 4 | def self.wrapping_level(level = nil) 5 | if level.nil? 6 | @wrapping_level || :core_entity 7 | else 8 | @wrapping_level = level 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/neo4j/core/cypher_session.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/module/delegation' 2 | 3 | module Neo4j 4 | module Core 5 | class CypherSession 6 | attr_reader :adaptor 7 | delegate :close, to: :adaptor 8 | 9 | def initialize(adaptor) 10 | fail ArgumentError, "Invalid adaptor: #{adaptor.inspect}" if !adaptor.is_a?(Adaptors::Base) 11 | 12 | @adaptor = adaptor 13 | 14 | @adaptor.connect 15 | end 16 | 17 | def transaction_class 18 | Neo4j::Core::CypherSession::Transactions::Base 19 | end 20 | 21 | %w[ 22 | query 23 | queries 24 | 25 | transaction 26 | 27 | version 28 | 29 | indexes 30 | constraints 31 | ].each do |method, &_block| 32 | define_method(method) do |*args, &block| 33 | @adaptor.send(method, self, *args, &block) 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/neo4j/core/cypher_session/adaptors.rb: -------------------------------------------------------------------------------- 1 | require 'neo4j/core/cypher_session' 2 | require 'neo4j/core/instrumentable' 3 | require 'neo4j/core/label' 4 | require 'neo4j/core/version' 5 | require 'neo4j/core/logging' 6 | require 'neo4j/ansi' 7 | 8 | module Neo4j 9 | module Core 10 | class CypherSession 11 | class CypherError < StandardError 12 | attr_reader :code, :original_message, :stack_trace 13 | 14 | def initialize(code = nil, original_message = nil, stack_trace = nil) 15 | @code = code 16 | @original_message = original_message 17 | @stack_trace = stack_trace 18 | 19 | msg = <<-ERROR 20 | Cypher error: 21 | #{ANSI::CYAN}#{code}#{ANSI::CLEAR}: #{original_message} 22 | #{stack_trace} 23 | ERROR 24 | super(msg) 25 | end 26 | 27 | def self.new_from(code, message, stack_trace = nil) 28 | error_class_from(code).new(code, message, stack_trace) 29 | end 30 | 31 | def self.error_class_from(code) 32 | case code 33 | when /(ConstraintValidationFailed|ConstraintViolation)/ 34 | SchemaErrors::ConstraintValidationFailedError 35 | when /IndexAlreadyExists/ 36 | SchemaErrors::IndexAlreadyExistsError 37 | when /ConstraintAlreadyExists/ # ????? 38 | SchemaErrors::ConstraintAlreadyExistsError 39 | else 40 | CypherError 41 | end 42 | end 43 | end 44 | module SchemaErrors 45 | class ConstraintValidationFailedError < CypherError; end 46 | class ConstraintAlreadyExistsError < CypherError; end 47 | class IndexAlreadyExistsError < CypherError; end 48 | end 49 | class ConnectionFailedError < StandardError; end 50 | 51 | module Adaptors 52 | MAP = {} 53 | 54 | class Base 55 | include Neo4j::Core::Instrumentable 56 | 57 | gem_name, version = if defined?(::Neo4j::ActiveNode) 58 | ['neo4j', ::Neo4j::VERSION] 59 | else 60 | ['neo4j-core', ::Neo4j::Core::VERSION] 61 | end 62 | 63 | 64 | USER_AGENT_STRING = "#{gem_name}-gem/#{version} (https://github.com/neo4jrb/#{gem_name})" 65 | 66 | def connect(*_args) 67 | fail '#connect not implemented!' 68 | end 69 | 70 | attr_accessor :wrap_level 71 | attr_reader :options 72 | 73 | Query = Struct.new(:cypher, :parameters, :pretty_cypher, :context) 74 | 75 | class QueryBuilder 76 | attr_reader :queries 77 | 78 | def initialize 79 | @queries = [] 80 | end 81 | 82 | def append(*args) 83 | query = case args.map(&:class) 84 | when [String], [String, Hash] 85 | Query.new(args[0], args[1] || {}) 86 | when [::Neo4j::Core::Query] 87 | args[0] 88 | else 89 | fail ArgumentError, "Could not determine query from arguments: #{args.inspect}" 90 | end 91 | 92 | @queries << query 93 | end 94 | 95 | def query 96 | # `nil` sessions are just a workaround until 97 | # we phase out `Query` objects containing sessions 98 | Neo4j::Core::Query.new(session: nil) 99 | end 100 | end 101 | 102 | def query(session, *args) 103 | options = case args.size 104 | when 3 105 | args.pop 106 | when 2 107 | args.pop if args[0].is_a?(::Neo4j::Core::Query) 108 | end || {} 109 | 110 | queries(session, options) { append(*args) }[0] 111 | end 112 | 113 | def queries(session, options = {}, &block) 114 | query_builder = QueryBuilder.new 115 | 116 | query_builder.instance_eval(&block) 117 | 118 | new_or_current_transaction(session, options[:transaction]) do |tx| 119 | query_set(tx, query_builder.queries, {commit: !options[:transaction]}.merge(options)) 120 | end 121 | end 122 | 123 | %i[query_set 124 | version 125 | indexes 126 | constraints 127 | connected?].each do |method| 128 | define_method(method) do |*_args| 129 | fail "##{method} method not implemented on adaptor!" 130 | end 131 | end 132 | 133 | # If called without a block, returns a Transaction object 134 | # which can be used to call query/queries/mark_failed/commit 135 | # If called with a block, the Transaction object is yielded 136 | # to the block and `commit` is ensured. Any uncaught exceptions 137 | # will mark the transaction as failed first 138 | def transaction(session) 139 | return self.class.transaction_class.new(session) if !block_given? 140 | 141 | begin 142 | tx = transaction(session) 143 | 144 | yield tx 145 | rescue => e 146 | tx.mark_failed if tx 147 | 148 | raise e 149 | ensure 150 | tx.close if tx 151 | end 152 | end 153 | 154 | def logger 155 | return @logger if @logger 156 | 157 | @logger = if @options[:logger] 158 | @options[:logger] 159 | else 160 | Logger.new(logger_location).tap do |logger| 161 | logger.level = logger_level 162 | end 163 | end 164 | end 165 | 166 | def setup_queries!(queries, transaction, options = {}) 167 | validate_connection!(transaction) 168 | 169 | return if options[:skip_instrumentation] 170 | queries.each do |query| 171 | self.class.instrument_query(query, self) {} 172 | end 173 | end 174 | 175 | EMPTY = '' 176 | NEWLINE_W_SPACES = "\n " 177 | 178 | instrument(:query, 'neo4j.core.cypher_query', %w[query adaptor]) do |_, _start, _finish, _id, payload| 179 | query = payload[:query] 180 | params_string = (query.parameters && !query.parameters.empty? ? "| #{query.parameters.inspect}" : EMPTY) 181 | cypher = query.pretty_cypher ? (NEWLINE_W_SPACES if query.pretty_cypher.include?("\n")).to_s + query.pretty_cypher.gsub(/\n/, NEWLINE_W_SPACES) : query.cypher 182 | 183 | source_line, line_number = Logging.first_external_path_and_line(caller_locations) 184 | 185 | " #{ANSI::CYAN}#{query.context || 'CYPHER'}#{ANSI::CLEAR} #{cypher} #{params_string}" + 186 | ("\n ↳ #{source_line}:#{line_number}" if payload[:adaptor].options[:verbose_query_logs] && source_line).to_s 187 | end 188 | 189 | def default_subscribe 190 | subscribe_to_request 191 | end 192 | 193 | def close; end 194 | 195 | def supports_metadata? 196 | true 197 | end 198 | 199 | class << self 200 | def transaction_class 201 | fail '.transaction_class method not implemented on adaptor!' 202 | end 203 | end 204 | 205 | private 206 | 207 | def new_or_current_transaction(session, tx, &block) 208 | if tx 209 | yield(tx) 210 | else 211 | transaction(session, &block) 212 | end 213 | end 214 | 215 | def validate_connection!(transaction) 216 | fail 'Query attempted without a connection' if !connected? 217 | fail "Invalid transaction object: #{transaction}" if !transaction.is_a?(self.class.transaction_class) 218 | end 219 | 220 | def logger_location 221 | @options[:logger_location] || STDOUT 222 | end 223 | 224 | def logger_level 225 | @options[:logger_level] || Logger::WARN 226 | end 227 | end 228 | end 229 | end 230 | end 231 | end 232 | -------------------------------------------------------------------------------- /lib/neo4j/core/cypher_session/adaptors/bolt.rb: -------------------------------------------------------------------------------- 1 | require 'neo4j/core/cypher_session/adaptors' 2 | require 'neo4j/core/cypher_session/adaptors/has_uri' 3 | require 'neo4j/core/cypher_session/adaptors/schema' 4 | require 'neo4j/core/cypher_session/adaptors/bolt/pack_stream' 5 | require 'neo4j/core/cypher_session/adaptors/bolt/chunk_writer_io' 6 | require 'neo4j/core/cypher_session/responses/bolt' 7 | require 'net/tcp_client' 8 | 9 | # TODO: Work with `Query` objects? 10 | module Neo4j 11 | module Core 12 | class CypherSession 13 | module Adaptors 14 | class Bolt < Base 15 | include Adaptors::HasUri 16 | include Adaptors::Schema 17 | default_url('bolt://neo4:neo4j@localhost:7687') 18 | validate_uri do |uri| 19 | uri.scheme == 'bolt' 20 | end 21 | 22 | SUPPORTED_VERSIONS = [1, 0, 0, 0].freeze 23 | VERSION = '0.0.1'.freeze 24 | 25 | def initialize(url, options = {}) 26 | self.url = url 27 | @options = options 28 | @net_tcp_client_options = {read_timeout: options.fetch(:read_timeout, -1), 29 | write_timeout: options.fetch(:write_timeout, -1), 30 | connect_timeout: options.fetch(:connect_timeout, 10), 31 | ssl: options.fetch(:ssl, {})} 32 | 33 | open_socket 34 | end 35 | 36 | def connect 37 | handshake 38 | 39 | init 40 | 41 | message = flush_messages[0] 42 | return if message.type == :success 43 | 44 | data = message.args[0] 45 | fail "Init did not complete successfully\n\n#{data['code']}\n#{data['message']}" 46 | end 47 | 48 | def query_set(transaction, queries, options = {}) 49 | setup_queries!(queries, transaction, skip_instrumentation: options[:skip_instrumentation]) 50 | 51 | self.class.instrument_request(self) do 52 | send_query_jobs(queries) 53 | 54 | build_response(queries, options[:wrap_level] || @options[:wrap_level]) 55 | end 56 | end 57 | 58 | def connected? 59 | !!@tcp_client && !@tcp_client.closed? 60 | end 61 | 62 | def self.transaction_class 63 | require 'neo4j/core/cypher_session/transactions/bolt' 64 | Neo4j::Core::CypherSession::Transactions::Bolt 65 | end 66 | 67 | instrument(:request, 'neo4j.core.bolt.request', %w[adaptor body]) do |_, start, finish, _id, payload| 68 | ms = (finish - start) * 1000 69 | adaptor = payload[:adaptor] 70 | 71 | type = adaptor.ssl? ? '+TLS' : ' UNSECURE' 72 | " #{ANSI::BLUE}BOLT#{type}:#{ANSI::CLEAR} #{ANSI::YELLOW}#{ms.round}ms#{ANSI::CLEAR} #{adaptor.url_without_password}" 73 | end 74 | 75 | def ssl? 76 | @tcp_client.socket.is_a?(OpenSSL::SSL::SSLSocket) 77 | end 78 | 79 | private 80 | 81 | def build_response(queries, wrap_level) 82 | catch(:cypher_bolt_failure) do 83 | Responses::Bolt.new(queries, method(:flush_messages), wrap_level: wrap_level).results 84 | end.tap do |error_data| 85 | handle_failure!(error_data) if !error_data.is_a?(Array) 86 | end 87 | end 88 | 89 | def handle_failure!(error_data) 90 | flush_messages 91 | 92 | send_job do |job| 93 | job.add_message(:ack_failure) 94 | end 95 | fail 'Expected SUCCESS for ACK_FAILURE' if flush_messages[0].type != :success 96 | 97 | fail CypherError.new_from(error_data['code'], error_data['message']) 98 | end 99 | 100 | def send_query_jobs(queries) 101 | send_job do |job| 102 | queries.each do |query| 103 | job.add_message(:run, query.cypher, query.parameters || {}) 104 | job.add_message(:pull_all) 105 | end 106 | end 107 | end 108 | 109 | def new_job 110 | Job.new(self) 111 | end 112 | 113 | def secure_connection? 114 | @is_secure_socket ||= @options.key?(:ssl) 115 | end 116 | 117 | def open_socket 118 | @tcp_client = Net::TCPClient.new(@net_tcp_client_options.merge(buffered: false, server: "#{host}:#{port}")) 119 | rescue Errno::ECONNREFUSED => e 120 | raise Neo4j::Core::CypherSession::ConnectionFailedError, e.message 121 | end 122 | 123 | GOGOBOLT = "\x60\x60\xB0\x17" 124 | def handshake 125 | log_message :C, :handshake, nil 126 | 127 | sendmsg(GOGOBOLT + SUPPORTED_VERSIONS.pack('l>*')) 128 | 129 | agreed_version = recvmsg(4).unpack('l>*')[0] 130 | 131 | if agreed_version.zero? 132 | @tcp_client.close 133 | 134 | fail "Couldn't agree on a version (Sent versions #{SUPPORTED_VERSIONS.inspect})" 135 | end 136 | 137 | logger.debug { "Agreed to version: #{agreed_version}" } 138 | end 139 | 140 | def init 141 | send_job do |job| 142 | job.add_message(:init, USER_AGENT_STRING, principal: user, credentials: password, scheme: 'basic') 143 | end 144 | end 145 | 146 | # Allows you to send messages to the server 147 | # Returns an array of Message objects 148 | def send_job 149 | new_job.tap do |job| 150 | yield job 151 | log_message :C, :job, job 152 | sendmsg(job.chunked_packed_stream) 153 | end 154 | end 155 | 156 | # Don't need to calculate these every time. Cache in memory 157 | BYTE_STRINGS = (0..255).map { |byte| byte.to_s(16).rjust(2, '0') } 158 | 159 | STREAM_INSPECTOR = lambda do |stream| 160 | stream.bytes.map { |byte| BYTE_STRINGS[byte] }.join(':') 161 | end 162 | 163 | def sendmsg(message) 164 | log_message :C, message 165 | @tcp_client.write(message) 166 | end 167 | 168 | def recvmsg(size) 169 | @tcp_client.read(size) do |result| 170 | log_message :S, result 171 | end 172 | end 173 | 174 | def flush_messages 175 | if structures = flush_response 176 | structures.map do |structure| 177 | Message.new(structure.signature, *structure.list).tap do |message| 178 | log_message :S, message.type, message.args.join(' ') if logger.debug? 179 | end 180 | end 181 | end 182 | end 183 | 184 | def log_message(side, *args) 185 | logger.debug do 186 | if args.size == 1 187 | "#{side}: #{STREAM_INSPECTOR.call(args[0])}" 188 | else 189 | type, message = args 190 | "#{side}: #{ANSI::CYAN}#{type.to_s.upcase}#{ANSI::CLEAR} #{message}" 191 | end 192 | end 193 | end 194 | 195 | # Replace with Enumerator? 196 | def flush_response 197 | chunk = '' 198 | 199 | while !(header = recvmsg(2)).empty? && (chunk_size = header.unpack('s>*')[0]) > 0 200 | log_message :S, :chunk_size, chunk_size 201 | 202 | chunk << recvmsg(chunk_size) 203 | end 204 | 205 | unpacker = PackStream::Unpacker.new(StringIO.new(chunk)) 206 | [].tap { |r| while arg = unpacker.unpack_value!; r << arg; end } 207 | end 208 | 209 | # Represents messages sent to or received from the server 210 | class Message 211 | TYPE_CODES = { 212 | # client message types 213 | init: 0x01, # 0000 0001 // INIT 214 | ack_failure: 0x0E, # 0000 1110 // ACK_FAILURE 215 | reset: 0x0F, # 0000 1111 // RESET 216 | run: 0x10, # 0001 0000 // RUN 217 | discard_all: 0x2F, # 0010 1111 // DISCARD * 218 | pull_all: 0x3F, # 0011 1111 // PULL * 219 | 220 | # server message types 221 | success: 0x70, # 0111 0000 // SUCCESS 222 | record: 0x71, # 0111 0001 // RECORD 223 | ignored: 0x7E, # 0111 1110 // IGNORED 224 | failure: 0x7F # 0111 1111 // FAILURE 225 | }.freeze 226 | 227 | CODE_TYPES = TYPE_CODES.invert 228 | 229 | def initialize(type_or_code, *args) 230 | @type_code = Message.type_code_for(type_or_code) 231 | fail "Invalid message type: #{@type_code.inspect}" if !@type_code 232 | @type = CODE_TYPES[@type_code] 233 | 234 | @args = args 235 | end 236 | 237 | def struct 238 | PackStream::Structure.new(@type_code, @args) 239 | end 240 | 241 | def to_s 242 | "#{ANSI::GREEN}#{@type.to_s.upcase}#{ANSI::CLEAR} #{@args.inspect if !@args.empty?}" 243 | end 244 | 245 | def packed_stream 246 | PackStream::Packer.new(struct).packed_stream 247 | end 248 | 249 | def value 250 | return if @type != :record 251 | @args.map do |arg| 252 | # Assuming result is Record 253 | field_names = arg[1] 254 | 255 | field_values = arg[2].map do |field_value| 256 | Message.interpret_field_value(field_value) 257 | end 258 | 259 | Hash[field_names.zip(field_values)] 260 | end 261 | end 262 | 263 | attr_reader :type, :args 264 | 265 | def self.type_code_for(type_or_code) 266 | type_or_code.is_a?(Integer) ? type_or_code : TYPE_CODES[type_or_code] 267 | end 268 | 269 | def self.interpret_field_value(value) 270 | if value.is_a?(Array) && (1..3).cover?(value[0]) 271 | case value[0] 272 | when 1 273 | {type: :node, identity: value[1], 274 | labels: value[2], properties: value[3]} 275 | end 276 | else 277 | value 278 | end 279 | end 280 | end 281 | 282 | # Represents a set of messages to send to the server 283 | class Job 284 | def initialize(session) 285 | @messages = [] 286 | @session = session 287 | end 288 | 289 | def add_message(type, *args) 290 | @messages << Message.new(type, *args) 291 | end 292 | 293 | def chunked_packed_stream 294 | io = ChunkWriterIO.new 295 | 296 | @messages.each do |message| 297 | io.write(message.packed_stream) 298 | io.flush(true) 299 | end 300 | 301 | io.rewind 302 | io.read 303 | end 304 | 305 | def to_s 306 | @messages.join(' | ') 307 | end 308 | end 309 | end 310 | end 311 | end 312 | end 313 | end 314 | -------------------------------------------------------------------------------- /lib/neo4j/core/cypher_session/adaptors/bolt/chunk_writer_io.rb: -------------------------------------------------------------------------------- 1 | require 'stringio' 2 | 3 | class ChunkWriterIO < StringIO 4 | # Writer for chunked data. 5 | 6 | MAX_CHUNK_SIZE = 0xFFFF 7 | 8 | def initialize 9 | @output_buffer = [] 10 | @output_size = 0 11 | super 12 | end 13 | 14 | # Write some bytes, splitting into chunks if necessary. 15 | def write_with_chunking(string) 16 | until string.empty? 17 | future_size = @output_size + string.size 18 | if future_size >= MAX_CHUNK_SIZE 19 | last = MAX_CHUNK_SIZE - @output_size 20 | write_buffer!(string[0, last], MAX_CHUNK_SIZE) 21 | string = string[last..-1] 22 | 23 | write_without_chunking(buffer_result) 24 | clear_buffer! 25 | else 26 | write_buffer!(string, future_size) 27 | 28 | string = '' 29 | end 30 | end 31 | end 32 | 33 | alias write_without_chunking write 34 | alias write write_with_chunking 35 | 36 | def flush(zero_chunk = false) 37 | write_without_chunking(buffer_result(zero_chunk)) 38 | clear_buffer! 39 | 40 | super() 41 | end 42 | 43 | # Close the stream. 44 | def close(zero_chunk = false) 45 | flush(zero_chunk) 46 | super 47 | end 48 | 49 | # private 50 | def write_buffer!(string, size) 51 | @output_buffer << string 52 | @output_size = size 53 | end 54 | 55 | def buffer_result(zero_chunk = false) 56 | result = '' 57 | 58 | if !@output_buffer.empty? 59 | result << [@output_size].pack('s>*') 60 | result.concat(@output_buffer.join) 61 | end 62 | 63 | result << "\x00\x00" if zero_chunk 64 | 65 | result 66 | end 67 | 68 | def clear_buffer! 69 | @output_buffer.clear 70 | @output_size = 0 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/neo4j/core/cypher_session/adaptors/bolt/pack_stream.rb: -------------------------------------------------------------------------------- 1 | require 'stringio' 2 | 3 | module Neo4j 4 | module Core 5 | # Implements the the PackStream packing and unpacking specifications 6 | # as specified by Neo Technology for the Neo4j graph database 7 | module PackStream 8 | MARKER_TYPES = { 9 | C0: nil, 10 | C1: [:float, 64], 11 | C2: false, 12 | C3: true, 13 | C8: [:int, 8], 14 | C9: [:int, 16], 15 | CA: [:int, 32], 16 | CB: [:int, 64], 17 | CC: [:bytes, 8], 18 | CD: [:bytes, 16], 19 | CE: [:bytes, 32], 20 | D0: [:text, 8], 21 | D1: [:text, 16], 22 | D2: [:text, 32], 23 | D4: [:list, 8], 24 | D5: [:list, 16], 25 | D6: [:list, 32], 26 | D8: [:map, 8], 27 | D9: [:map, 16], 28 | DA: [:map, 32], 29 | DC: [:struct, 8], 30 | DD: [:struct, 16], 31 | DE: [:struct, 32] 32 | } 33 | # For efficiency. Translates directly from bytes to types 34 | # Disabling because this needs to be able to change the hash inside the blocks 35 | # There's probably a better way 36 | MARKER_TYPES.keys.each do |key| 37 | ord = key.to_s.to_i(16) 38 | MARKER_TYPES[ord] = MARKER_TYPES.delete(key) 39 | end 40 | 41 | # Translates directly from types to bytes 42 | MARKER_BYTES = MARKER_TYPES.invert 43 | MARKER_BYTES.keys.each do |key| 44 | MARKER_BYTES.delete(key) if key.is_a?(Array) 45 | end 46 | 47 | 48 | MARKER_HEADERS = MARKER_TYPES.each_with_object({}) do |(byte, (type, size)), headers| 49 | headers[type] ||= {} 50 | headers[type][size] = [byte].pack('C') 51 | end 52 | 53 | HEADER_PACK_STRINGS = %w[C S L].freeze 54 | 55 | Structure = Struct.new(:signature, :list) 56 | 57 | # Object which holds a Ruby object and can 58 | # pack it into a PackStream stream 59 | class Packer 60 | def initialize(object) 61 | @object = object 62 | end 63 | 64 | def packed_stream 65 | if byte = MARKER_BYTES[@object] 66 | pack_array_as_string([byte]) 67 | else 68 | case @object 69 | when Date, Time, DateTime then string_stream 70 | when Integer, Float, String, Symbol, Array, Set, Structure, Hash 71 | send(@object.class.name.split('::').last.downcase + '_stream') 72 | end 73 | end 74 | end 75 | 76 | # Range Minimum | Range Maximum | Representation | Byte | 77 | # ============================|============================|================|======| 78 | # -9 223 372 036 854 775 808 | -2 147 483 649 | INT_64 | CB | 79 | # -2 147 483 648 | -32 769 | INT_32 | CA | 80 | # -32 768 | -129 | INT_16 | C9 | 81 | # -128 | -17 | INT_8 | C8 | 82 | # -16 | +127 | TINY_INT | N/A | 83 | # +128 | +32 767 | INT_16 | C9 | 84 | # +32 768 | +2 147 483 647 | INT_32 | CA | 85 | # +2 147 483 648 | +9 223 372 036 854 775 807 | INT_64 | CB | 86 | 87 | INT_HEADERS = MARKER_HEADERS[:int] 88 | def integer_stream 89 | case @object 90 | when -0x10...0x80 # TINY_INT 91 | pack_integer_object_as_string 92 | when -0x80...-0x10 # INT_8 93 | INT_HEADERS[8] + pack_integer_object_as_string 94 | when -0x8000...0x8000 # INT_16 95 | INT_HEADERS[16] + pack_integer_object_as_string(2) 96 | when -0x80000000...0x80000000 # INT_32 97 | INT_HEADERS[32] + pack_integer_object_as_string(4) 98 | when -0x8000000000000000...0x8000000000000000 # INT_64 99 | INT_HEADERS[64] + pack_integer_object_as_string(8) 100 | end 101 | end 102 | 103 | alias fixnum_stream integer_stream 104 | alias bignum_stream integer_stream 105 | 106 | def float_stream 107 | MARKER_HEADERS[:float][64] + [@object].pack('G').force_encoding(Encoding::BINARY) 108 | end 109 | 110 | # Marker | Size | Maximum size 111 | # ========|=============================================|===================== 112 | # 80..8F | contained within low-order nibble of marker | 15 bytes 113 | # D0 | 8-bit big-endian unsigned integer | 255 bytes 114 | # D1 | 16-bit big-endian unsigned integer | 65 535 bytes 115 | # D2 | 32-bit big-endian unsigned integer | 4 294 967 295 bytes 116 | 117 | def string_stream 118 | s = @object.to_s 119 | s = s.dup if s.frozen? 120 | marker_string(0x80, 0xD0, @object.to_s.bytesize) + s.force_encoding(Encoding::BINARY) 121 | end 122 | 123 | alias symbol_stream string_stream 124 | 125 | def array_stream 126 | marker_string(0x90, 0xD4, @object.size) + @object.map do |e| 127 | Packer.new(e).packed_stream 128 | end.join 129 | end 130 | 131 | alias set_stream array_stream 132 | 133 | def structure_stream 134 | fail 'Structure too big' if @object.list.size > 65_535 135 | marker_string(0xB0, 0xDC, @object.list.size) + [@object.signature].pack('C') + @object.list.map do |e| 136 | Packer.new(e).packed_stream 137 | end.join 138 | end 139 | 140 | def hash_stream 141 | marker_string(0xA0, 0xD8, @object.size) + 142 | @object.map do |key, value| 143 | Packer.new(key).packed_stream + 144 | Packer.new(value).packed_stream 145 | end.join 146 | end 147 | 148 | def self.pack_arguments(*objects) 149 | objects.map { |o| new(o).packed_stream }.join 150 | end 151 | 152 | private 153 | 154 | def marker_string(tiny_base, regular_base, size) 155 | head_byte = case size 156 | when 0...0x10 then tiny_base + size 157 | when 0x10...0x100 then regular_base 158 | when 0x100...0x10000 then regular_base + 1 159 | when 0x10000...0x100000000 then regular_base + 2 160 | end 161 | 162 | result = [head_byte].pack('C') 163 | result += [size].pack(HEADER_PACK_STRINGS[head_byte - regular_base]).reverse if size >= 0x10 164 | result 165 | end 166 | 167 | def pack_integer_object_as_string(size = 1) 168 | bytes = [] 169 | (0...size).to_a.reverse.inject(@object) do |current, i| 170 | bytes << (current / (256**i)) 171 | current % (256**i) 172 | end 173 | 174 | pack_array_as_string(bytes) 175 | end 176 | 177 | def pack_array_as_string(a) 178 | a.pack('c*') 179 | end 180 | end 181 | 182 | # Object which holds a stream of PackStream data 183 | # and can unpack it 184 | class Unpacker 185 | def initialize(stream) 186 | @stream = stream 187 | end 188 | 189 | HEADER_BASE_BYTES = {text: 0xD0, list: 0xD4, struct: 0xDC, map: 0xD8}.freeze 190 | 191 | def unpack_value! 192 | return nil if depleted? 193 | 194 | marker = shift_byte! 195 | 196 | if type_and_size = PackStream.marker_type_and_size(marker) 197 | type, size = type_and_size 198 | 199 | shift_value_for_type!(type, size, marker) 200 | elsif MARKER_TYPES.key?(marker) 201 | MARKER_TYPES[marker] 202 | else 203 | marker >= 0xF0 ? -0x100 + marker : marker 204 | end 205 | end 206 | 207 | private 208 | 209 | METHOD_MAP = { 210 | int: :value_for_int!, 211 | float: :value_for_float!, 212 | tiny_list: :value_for_list!, 213 | list: :value_for_list!, 214 | tiny_map: :value_for_map!, 215 | map: :value_for_map!, 216 | tiny_struct: :value_for_struct!, 217 | struct: :value_for_struct! 218 | } 219 | 220 | def shift_value_for_type!(type, size, marker) 221 | if %i[text list map struct].include?(type) 222 | offset = marker - HEADER_BASE_BYTES[type] 223 | size = shift_stream!(2 << (offset - 1)).reverse.unpack(HEADER_PACK_STRINGS[offset])[0] 224 | end 225 | 226 | if %i[tiny_text text bytes].include?(type) 227 | shift_stream!(size).force_encoding('UTF-8') 228 | else 229 | send(METHOD_MAP[type], size) 230 | end 231 | end 232 | 233 | def value_for_int!(size) 234 | r = shift_bytes!(size >> 3).reverse.each_with_index.inject(0) do |sum, (byte, i)| 235 | sum + (byte * (256**i)) 236 | end 237 | 238 | (r >> (size - 1)) == 1 ? (r - (2**size)) : r 239 | end 240 | 241 | def value_for_float!(_size) 242 | shift_stream!(8).unpack('G')[0] 243 | end 244 | 245 | def value_for_map!(size) 246 | size.times.each_with_object({}) do |_, r| 247 | key = unpack_value! 248 | r[key] = unpack_value! 249 | end 250 | end 251 | 252 | def value_for_list!(size) 253 | Array.new(size) { unpack_value! } 254 | end 255 | 256 | def value_for_struct!(size) 257 | Structure.new(shift_byte!, value_for_list!(size)) 258 | end 259 | 260 | def shift_byte! 261 | shift_bytes!(1).first unless depleted? 262 | end 263 | 264 | def shift_bytes!(length) 265 | result = shift_stream!(length) 266 | result && result.bytes.to_a 267 | end 268 | 269 | def shift_stream!(length) 270 | @stream.read(length) if !depleted? || length.zero? 271 | end 272 | 273 | def depleted? 274 | @stream.eof? 275 | end 276 | end 277 | 278 | def self.marker_type_and_size(marker) 279 | if (marker_spec = MARKER_TYPES[marker]).is_a?(Array) 280 | marker_spec 281 | else 282 | case marker 283 | when 0x80..0x8F then [:tiny_text, marker - 0x80] 284 | when 0x90..0x9F then [:tiny_list, marker - 0x90] 285 | when 0xA0..0xAF then [:tiny_map, marker - 0xA0] 286 | when 0xB0..0xBF then [:tiny_struct, marker - 0xB0] 287 | end 288 | end 289 | end 290 | end 291 | end 292 | end 293 | -------------------------------------------------------------------------------- /lib/neo4j/core/cypher_session/adaptors/driver.rb: -------------------------------------------------------------------------------- 1 | require 'neo4j/core/cypher_session/adaptors' 2 | require 'neo4j/core/cypher_session/adaptors/has_uri' 3 | require 'neo4j/core/cypher_session/adaptors/schema' 4 | require 'neo4j/core/cypher_session/responses/driver' 5 | require 'singleton' 6 | 7 | module Neo4j 8 | module Core 9 | class CypherSession 10 | module Adaptors 11 | # The registry is necessary due to the specs constantly creating new CypherSessions. 12 | # Closing a driver is costly. Not closing it prevents the process from termination. 13 | # The registry allows reusage of drivers which are thread safe and conveniently closing them in one call. 14 | class DriverRegistry < Hash 15 | include Singleton 16 | 17 | at_exit do 18 | instance.close_all 19 | end 20 | 21 | def initialize 22 | super 23 | @mutex = Mutex.new 24 | end 25 | 26 | def driver_for(url) 27 | uri = URI(url) 28 | user = uri.user 29 | password = uri.password 30 | auth_token = if user 31 | Neo4j::Driver::AuthTokens.basic(user, password) 32 | else 33 | Neo4j::Driver::AuthTokens.none 34 | end 35 | @mutex.synchronize { self[url] ||= Neo4j::Driver::GraphDatabase.driver(url, auth_token) } 36 | end 37 | 38 | def close(driver) 39 | delete(key(driver)) 40 | driver.close 41 | end 42 | 43 | def close_all 44 | values.each(&:close) 45 | clear 46 | end 47 | end 48 | 49 | class Driver < Base 50 | include Adaptors::HasUri 51 | include Adaptors::Schema 52 | default_url('bolt://neo4:neo4j@localhost:7687') 53 | validate_uri do |uri| 54 | uri.scheme == 'bolt' 55 | end 56 | 57 | attr_reader :driver 58 | alias connected? driver 59 | 60 | def initialize(url, options = {}) 61 | self.url = url 62 | @driver = DriverRegistry.instance.driver_for(url) 63 | @options = options 64 | end 65 | 66 | def connect; end 67 | 68 | def close 69 | DriverRegistry.instance.close(driver) 70 | end 71 | 72 | def query_set(transaction, queries, options = {}) 73 | setup_queries!(queries, transaction, skip_instrumentation: options[:skip_instrumentation]) 74 | 75 | responses = queries.map do |query| 76 | transaction.root_tx.run(query.cypher, query.parameters) 77 | end 78 | wrap_level = options[:wrap_level] || @options[:wrap_level] 79 | Responses::Driver.new(responses, wrap_level: wrap_level).results 80 | rescue Neo4j::Driver::Exceptions::Neo4jException => e 81 | raise Neo4j::Core::CypherSession::CypherError.new_from(e.code, e.message) # , e.stack_track.to_a 82 | end 83 | 84 | # def transaction(_session, &block) 85 | # session = driver.session(org.neo4j.driver.v1.AccessMode::WRITE) 86 | # session.writeTransaction(&block) 87 | # ensure 88 | # session.close 89 | # end 90 | 91 | def self.transaction_class 92 | require 'neo4j/core/cypher_session/transactions/driver' 93 | Neo4j::Core::CypherSession::Transactions::Driver 94 | end 95 | 96 | instrument(:request, 'neo4j.core.bolt.request', %w[adaptor body]) do |_, start, finish, _id, payload| 97 | ms = (finish - start) * 1000 98 | adaptor = payload[:adaptor] 99 | 100 | type = nil # adaptor.ssl? ? '+TLS' : ' UNSECURE' 101 | " #{ANSI::BLUE}BOLT#{type}:#{ANSI::CLEAR} #{ANSI::YELLOW}#{ms.round}ms#{ANSI::CLEAR} #{adaptor.url_without_password}" 102 | end 103 | end 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/neo4j/core/cypher_session/adaptors/embedded.rb: -------------------------------------------------------------------------------- 1 | require 'neo4j/core/cypher_session/adaptors' 2 | require 'neo4j/core/cypher_session/responses/embedded' 3 | require 'active_support/hash_with_indifferent_access' 4 | 5 | module Neo4j 6 | module Core 7 | class CypherSession 8 | module Adaptors 9 | class Embedded < Base 10 | attr_reader :graph_db, :path 11 | 12 | def initialize(path, options = {}) 13 | fail 'JRuby is required for embedded mode' if RUBY_PLATFORM != 'java' 14 | # TODO: Will this cause an error if a new path is specified? 15 | fail ArgumentError, "Invalid path: #{path}" if File.file?(path) 16 | FileUtils.mkdir_p(path) 17 | 18 | @path = path 19 | @options = options 20 | end 21 | 22 | def connect 23 | factory = Java::OrgNeo4jGraphdbFactory::GraphDatabaseFactory.new 24 | db_service = factory.newEmbeddedDatabaseBuilder(@path) 25 | db_service.loadPropertiesFromFile(@options[:properties_file]) if @options[:properties_file] 26 | db_service.setConfig(@options[:properties_map]) if @options[:properties_map] 27 | 28 | @graph_db = db_service.newGraphDatabase 29 | end 30 | 31 | def query_set(transaction, queries, options = {}) 32 | # I think that this is the best way to do a batch in embedded... 33 | # Should probably do within a transaction in case of errors... 34 | setup_queries!(queries, transaction, options) 35 | 36 | self.class.instrument_transaction do 37 | Responses::Embedded.new(execution_results(queries), wrap_level: options[:wrap_level] || @options[:wrap_level]).results 38 | end 39 | rescue Java::OrgNeo4jCypher::CypherExecutionException, Java::OrgNeo4jCypher::SyntaxException => e 40 | raise CypherError.new_from(e.status.to_s, e.message) # , e.stack_track.to_a 41 | end 42 | 43 | def version(_session) 44 | if defined?(::Neo4j::Community) 45 | ::Neo4j::Community::NEO_VERSION 46 | elsif defined?(::Neo4j::Enterprise) 47 | ::Neo4j::Enterprise::NEO_VERSION 48 | else 49 | fail 'Could not determine embedded version!' 50 | end 51 | end 52 | 53 | def indexes(session, _label = nil) 54 | Transaction.run(session) do 55 | graph_db = session.adaptor.graph_db 56 | 57 | graph_db.schema.get_indexes.map do |definition| 58 | {properties: definition.property_keys.map(&:to_sym), 59 | label: definition.label.to_s.to_sym} 60 | end 61 | end 62 | end 63 | 64 | CONSTRAINT_TYPES = { 65 | 'UNIQUENESS' => :uniqueness 66 | } 67 | def constraints(session) 68 | Transaction.run(session) do 69 | all_labels(session).flat_map do |label| 70 | graph_db.schema.get_constraints(label).map do |definition| 71 | {label: label.to_s.to_sym, 72 | properties: definition.property_keys.map(&:to_sym), 73 | type: CONSTRAINT_TYPES[definition.get_constraint_type.to_s]} 74 | end 75 | end 76 | end 77 | end 78 | 79 | def self.transaction_class 80 | require 'neo4j/core/cypher_session/transactions/embedded' 81 | Neo4j::Core::CypherSession::Transactions::Embedded 82 | end 83 | 84 | def connected? 85 | !!@graph_db 86 | end 87 | 88 | instrument(:transaction, 'neo4j.core.embedded.transaction', []) do |_, start, finish, _id, _payload| 89 | ms = (finish - start) * 1000 90 | 91 | " #{ANSI::BLUE}EMBEDDED CYPHER TRANSACTION:#{ANSI::CLEAR} #{ANSI::YELLOW}#{ms.round}ms#{ANSI::CLEAR}" 92 | end 93 | 94 | def default_subscribe 95 | subscribe_to_transaction 96 | end 97 | 98 | private 99 | 100 | def execution_results(queries) 101 | queries.map do |query| 102 | engine.execute(query.cypher, indifferent_params(query)) 103 | end 104 | end 105 | 106 | def all_labels(session) 107 | Java::OrgNeo4jTooling::GlobalGraphOperations.at(session.adaptor.graph_db).get_all_labels.to_a 108 | end 109 | 110 | def indifferent_params(query) 111 | params = query.parameters 112 | params.each { |k, v| params[k] = HashWithIndifferentAccess.new(params[k]) if v.is_a?(Hash) && !v.respond_to?(:nested_under_indifferent_access) } 113 | HashWithIndifferentAccess.new(params) 114 | end 115 | 116 | def engine 117 | @engine ||= Java::OrgNeo4jCypherJavacompat::ExecutionEngine.new(@graph_db) 118 | end 119 | 120 | def constraint_definitions_for(graph_db, label); end 121 | end 122 | end 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /lib/neo4j/core/cypher_session/adaptors/has_uri.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/concern' 2 | 3 | module Neo4j 4 | module Core 5 | class CypherSession 6 | module Adaptors 7 | # Containing the logic for dealing with adaptors which use URIs 8 | module HasUri 9 | extend ActiveSupport::Concern 10 | 11 | module ClassMethods 12 | attr_reader :default_uri 13 | 14 | def default_url(default_url) 15 | @default_uri = uri_from_url!(default_url) 16 | end 17 | 18 | def validate_uri(&block) 19 | @uri_validator = block 20 | end 21 | 22 | def uri_from_url!(url) 23 | validate_url!(url) 24 | 25 | @uri = url.nil? ? @default_uri : URI(url) 26 | 27 | fail ArgumentError, "Invalid URL: #{url.inspect}" if uri_valid?(@uri) 28 | 29 | @uri 30 | end 31 | 32 | private 33 | 34 | def validate_url!(url) 35 | fail ArgumentError, "Invalid URL: #{url.inspect}" if !(url.is_a?(String) || url.nil?) 36 | fail ArgumentError, 'No URL or default URL specified' if url.nil? && @default_uri.nil? 37 | end 38 | 39 | def uri_valid?(uri) 40 | @uri_validator && !@uri_validator.call(uri) 41 | end 42 | end 43 | 44 | def url 45 | @uri.to_s 46 | end 47 | 48 | def url=(url) 49 | @uri = self.class.uri_from_url!(url) 50 | end 51 | 52 | def url_without_password 53 | @url_without_password ||= "#{scheme}://#{user + ':...@' if user}#{host}:#{port}" 54 | end 55 | 56 | included do 57 | %w[scheme user password host port].each do |method| 58 | define_method(method) do 59 | (@uri && @uri.send(method)) || (self.class.default_uri && self.class.default_uri.send(method)) 60 | end 61 | end 62 | end 63 | end 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/neo4j/core/cypher_session/adaptors/http.rb: -------------------------------------------------------------------------------- 1 | require 'neo4j/core/cypher_session/adaptors' 2 | require 'neo4j/core/cypher_session/adaptors/has_uri' 3 | require 'neo4j/core/cypher_session/responses/http' 4 | require 'uri' 5 | 6 | # TODO: Work with `Query` objects 7 | module Neo4j 8 | module Core 9 | class CypherSession 10 | module Adaptors 11 | class HTTP < Base 12 | attr_reader :requestor, :url 13 | 14 | def initialize(url, options = {}) 15 | @url = url 16 | @options = options 17 | end 18 | 19 | DEFAULT_FARADAY_CONFIGURATOR = proc do |faraday| 20 | require 'typhoeus' 21 | require 'typhoeus/adapters/faraday' 22 | faraday.adapter :typhoeus 23 | end 24 | 25 | def connect 26 | @requestor = Requestor.new(@url, USER_AGENT_STRING, self.class.method(:instrument_request), @options.fetch(:faraday_configurator, DEFAULT_FARADAY_CONFIGURATOR)) 27 | end 28 | 29 | ROW_REST = %w[row REST] 30 | 31 | def query_set(transaction, queries, options = {}) 32 | setup_queries!(queries, transaction) 33 | 34 | return unless path = transaction.query_path(options.delete(:commit)) 35 | 36 | faraday_response = @requestor.post(path, queries) 37 | 38 | transaction.apply_id_from_url!(faraday_response.env[:response_headers][:location]) 39 | 40 | wrap_level = options[:wrap_level] || @options[:wrap_level] 41 | Responses::HTTP.new(faraday_response, wrap_level: wrap_level).results 42 | end 43 | 44 | def version(_session) 45 | @version ||= @requestor.get('db/data/').body[:neo4j_version] 46 | end 47 | 48 | # Schema inspection methods 49 | def indexes(_session) 50 | response = @requestor.get('db/data/schema/index') 51 | 52 | check_for_schema_response_error!(response) 53 | list = response.body || [] 54 | 55 | list.map do |item| 56 | {label: item[:label].to_sym, 57 | properties: item[:property_keys].map(&:to_sym)} 58 | end 59 | end 60 | 61 | CONSTRAINT_TYPES = { 62 | 'UNIQUENESS' => :uniqueness 63 | } 64 | def constraints(_session, _label = nil, _options = {}) 65 | response = @requestor.get('db/data/schema/constraint') 66 | 67 | check_for_schema_response_error!(response) 68 | list = response.body || [] 69 | list.map do |item| 70 | {type: CONSTRAINT_TYPES[item[:type]], 71 | label: item[:label].to_sym, 72 | properties: item[:property_keys].map(&:to_sym)} 73 | end 74 | end 75 | 76 | def check_for_schema_response_error!(response) 77 | if response.body.is_a?(Hash) && response.body.key?(:errors) 78 | message = response.body[:errors].map { |error| "#{error[:code]}: #{error[:message]}" }.join("\n ") 79 | fail CypherSession::ConnectionFailedError, "Connection failure: \n #{message}" 80 | elsif !response.success? 81 | fail CypherSession::ConnectionFailedError, "Connection failure: \n status: #{response.status} \n #{response.body}" 82 | end 83 | end 84 | 85 | def self.transaction_class 86 | require 'neo4j/core/cypher_session/transactions/http' 87 | Neo4j::Core::CypherSession::Transactions::HTTP 88 | end 89 | 90 | # Schema inspection methods 91 | def indexes_for_label(label) 92 | url = db_data_url + "schema/index/#{label}" 93 | @connection.get(url) 94 | end 95 | 96 | instrument(:request, 'neo4j.core.http.request', %w[method url body]) do |_, start, finish, _id, payload| 97 | ms = (finish - start) * 1000 98 | " #{ANSI::BLUE}HTTP REQUEST:#{ANSI::CLEAR} #{ANSI::YELLOW}#{ms.round}ms#{ANSI::CLEAR} #{payload[:method].upcase} #{payload[:url]} (#{payload[:body].size} bytes)" 99 | end 100 | 101 | def connected? 102 | !!@requestor 103 | end 104 | 105 | def supports_metadata? 106 | Gem::Version.new(version(nil)) >= Gem::Version.new('2.1.5') 107 | end 108 | 109 | # Basic wrapper around HTTP requests to standard Neo4j HTTP endpoints 110 | # - Takes care of JSONifying objects passed as body (Hash/Array/Query) 111 | # - Sets headers, including user agent string 112 | class Requestor 113 | include Adaptors::HasUri 114 | default_url('http://neo4:neo4j@localhost:7474') 115 | validate_uri { |uri| uri.is_a?(URI::HTTP) } 116 | def initialize(url, user_agent_string, instrument_proc, faraday_configurator) 117 | self.url = url 118 | @user = user 119 | @password = password 120 | @user_agent_string = user_agent_string 121 | @faraday = wrap_connection_failed! { faraday_connection(faraday_configurator) } 122 | @instrument_proc = instrument_proc 123 | end 124 | 125 | REQUEST_HEADERS = {'Accept'.to_sym => 'application/json; charset=UTF-8', 126 | 'Content-Type'.to_sym => 'application/json'} 127 | 128 | # @method HTTP method (:get/:post/:delete/:put) 129 | # @path Path part of URL 130 | # @body Body for the request. If a Query or Array of Queries, 131 | # it is automatically converted 132 | def request(method, path, body = '', _options = {}) 133 | request_body = request_body(body) 134 | url = url_from_path(path) 135 | @instrument_proc.call(method, url, request_body) do 136 | wrap_connection_failed! do 137 | @faraday.run_request(method, url, request_body, REQUEST_HEADERS) 138 | end 139 | end 140 | end 141 | 142 | # Convenience method to #request(:post, ...) 143 | def post(path, body = '', options = {}) 144 | request(:post, path, body, options) 145 | end 146 | 147 | # Convenience method to #request(:get, ...) 148 | def get(path, body = '', options = {}) 149 | request(:get, path, body, options) 150 | end 151 | 152 | private 153 | 154 | def faraday_connection(configurator) 155 | require 'faraday' 156 | require 'faraday_middleware/multi_json' 157 | 158 | Faraday.new(url) do |faraday| 159 | faraday.request :multi_json 160 | 161 | faraday.response :multi_json, symbolize_keys: true, content_type: 'application/json' 162 | 163 | faraday.headers['Content-Type'] = 'application/json' 164 | faraday.headers['User-Agent'] = @user_agent_string 165 | 166 | configurator.call(faraday) 167 | end 168 | end 169 | 170 | def password_config(options) 171 | options.fetch(:basic_auth, {}).fetch(:password, @password) 172 | end 173 | 174 | def username_config(options) 175 | options.fetch(:basic_auth, {}).fetch(:username, @user) 176 | end 177 | 178 | def request_body(body) 179 | return body if body.is_a?(String) 180 | 181 | body_is_query_array = body.is_a?(Array) && body.all? { |o| o.respond_to?(:cypher) } 182 | case body 183 | when Hash, Array 184 | return {statements: body.map(&self.class.method(:statement_from_query))} if body_is_query_array 185 | 186 | body 187 | else 188 | {statements: [self.class.statement_from_query(body)]} if body.respond_to?(:cypher) 189 | end 190 | end 191 | 192 | def wrap_connection_failed! 193 | yield 194 | rescue Faraday::ConnectionFailed => e 195 | raise CypherSession::ConnectionFailedError, "#{e.class}: #{e.message}" 196 | end 197 | 198 | class << self 199 | private 200 | 201 | def statement_from_query(query) 202 | {statement: query.cypher, 203 | parameters: query.parameters || {}, 204 | resultDataContents: ROW_REST} 205 | end 206 | end 207 | 208 | def url_base 209 | "#{scheme}://#{host}:#{port}" 210 | end 211 | 212 | def url_from_path(path) 213 | url_base + (path[0] != '/' ? '/' + path : path) 214 | end 215 | end 216 | end 217 | end 218 | end 219 | end 220 | end 221 | -------------------------------------------------------------------------------- /lib/neo4j/core/cypher_session/adaptors/schema.rb: -------------------------------------------------------------------------------- 1 | module Neo4j 2 | module Core 3 | class CypherSession 4 | module Adaptors 5 | module Schema 6 | def version(session) 7 | result = query(session, 'CALL dbms.components()', {}, skip_instrumentation: true) 8 | 9 | # BTW: community / enterprise could be retrieved via `result.first.edition` 10 | result.first.versions[0] 11 | end 12 | 13 | def indexes(session) 14 | result = query(session, 'CALL db.indexes()', {}, skip_instrumentation: true) 15 | 16 | result.map do |row| 17 | label, property = row.description.match(/INDEX ON :([^\(]+)\(([^\)]+)\)/)[1, 2] 18 | {type: row.type.to_sym, label: label.to_sym, properties: [property.to_sym], state: row.state.to_sym} 19 | end 20 | end 21 | 22 | def constraints(session) 23 | result = query(session, 'CALL db.indexes()', {}, skip_instrumentation: true) 24 | 25 | result.select { |row| row.type == 'node_unique_property' }.map do |row| 26 | label, property = row.description.match(/INDEX ON :([^\(]+)\(([^\)]+)\)/)[1, 2] 27 | {type: :uniqueness, label: label.to_sym, properties: [property.to_sym]} 28 | end 29 | end 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/neo4j/core/cypher_session/responses.rb: -------------------------------------------------------------------------------- 1 | require 'neo4j/core/cypher_session/result' 2 | 3 | module Neo4j 4 | module Core 5 | class CypherSession 6 | module Responses 7 | MAP = {} 8 | 9 | class Base 10 | include Enumerable 11 | 12 | def each 13 | results.each do |result| 14 | yield result 15 | end 16 | end 17 | 18 | def wrap_by_level(none_value) 19 | case @wrap_level 20 | when :none 21 | if none_value.is_a?(Array) 22 | none_value.map(&:symbolize_keys) 23 | else 24 | none_value.symbolize_keys 25 | end 26 | when :core_entity 27 | yield 28 | when :proc 29 | yield.wrap 30 | else 31 | fail ArgumentError, "Invalid wrap_level: #{@wrap_level.inspect}" 32 | end 33 | end 34 | 35 | def results 36 | fail '#results not implemented!' 37 | end 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/neo4j/core/cypher_session/responses/bolt.rb: -------------------------------------------------------------------------------- 1 | require 'neo4j/core/cypher_session/responses' 2 | require 'active_support/core_ext/hash/keys' 3 | 4 | module Neo4j 5 | module Core 6 | class CypherSession 7 | module Responses 8 | class Bolt < Base 9 | attr_reader :results, :result_info 10 | 11 | def initialize(queries, flush_messages_proc, options = {}) 12 | @wrap_level = options[:wrap_level] || Neo4j::Core::Config.wrapping_level 13 | 14 | @results = queries.map do 15 | fields, result_messages, _footer_messages = extract_message_groups(flush_messages_proc) 16 | # @result_info = footer_messages[0].args[0] 17 | 18 | data = result_messages.map do |result_message| 19 | validate_message_type!(result_message, :record) 20 | 21 | result_message.args[0] 22 | end 23 | 24 | result_from_data(fields, data) 25 | end 26 | end 27 | 28 | def result_from_data(columns, entities_data) 29 | rows = entities_data.map do |entity_data| 30 | wrap_entity(entity_data) 31 | end 32 | 33 | Result.new(columns, rows) 34 | end 35 | 36 | def wrap_entity(entity_data) 37 | case entity_data 38 | when Array 39 | entity_data.map(&method(:wrap_entity)) 40 | when PackStream::Structure 41 | wrap_structure(entity_data) 42 | when Hash 43 | entity_data.each_with_object({}) do |(k, v), result| 44 | result[k.to_sym] = wrap_entity(v) 45 | end 46 | else 47 | entity_data 48 | end 49 | end 50 | 51 | private 52 | 53 | def extract_message_groups(flush_messages_proc) 54 | fields_messages = flush_messages_proc.call 55 | 56 | validate_message_type!(fields_messages[0], :success) 57 | 58 | result_messages = [] 59 | messages = nil 60 | 61 | loop do 62 | messages = flush_messages_proc.call 63 | next if messages.nil? 64 | break if messages[0].type == :success 65 | result_messages.concat(messages) 66 | end 67 | 68 | [fields_messages[0].args[0]['fields'], 69 | result_messages, 70 | messages] 71 | end 72 | 73 | def wrap_structure(structure) 74 | case structure.signature 75 | when 0x4E then wrap_node(*structure.list) 76 | when 0x52 then wrap_relationship(*structure.list) 77 | when 0x72 then wrap_unbound_relationship(*structure.list) 78 | when 0x50 then wrap_path(*structure.list) 79 | else 80 | fail CypherError, "Unsupported structure signature: #{structure.signature}" 81 | end 82 | end 83 | 84 | def wrap_node(id, labels, properties) 85 | wrap_by_level(properties) { ::Neo4j::Core::Node.new(id, labels, properties) } 86 | end 87 | 88 | def wrap_relationship(id, from_node_id, to_node_id, type, properties) 89 | wrap_by_level(properties) { ::Neo4j::Core::Relationship.new(id, type, properties, from_node_id, to_node_id) } 90 | end 91 | 92 | def wrap_unbound_relationship(id, type, properties) 93 | wrap_by_level(properties) { ::Neo4j::Core::Relationship.new(id, type, properties) } 94 | end 95 | 96 | def wrap_path(nodes, relationships, directions) 97 | none_value = nodes.zip(relationships).flatten.compact.map { |obj| obj.list.last } 98 | wrap_by_level(none_value) do 99 | ::Neo4j::Core::Path.new(nodes.map(&method(:wrap_entity)), 100 | relationships.map(&method(:wrap_entity)), 101 | directions.map(&method(:wrap_direction))) 102 | end 103 | end 104 | 105 | def wrap_direction(_direction_int) 106 | '' 107 | end 108 | 109 | def validate_message_type!(message, type) 110 | case message.type 111 | when type 112 | nil 113 | when :failure 114 | data = message.args[0] 115 | throw :cypher_bolt_failure, data 116 | else 117 | fail "Unexpected message type: #{message.type} (#{message.inspect})" 118 | end 119 | end 120 | end 121 | end 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /lib/neo4j/core/cypher_session/responses/driver.rb: -------------------------------------------------------------------------------- 1 | require 'neo4j/core/cypher_session/responses' 2 | 3 | module Neo4j 4 | module Core 5 | class CypherSession 6 | module Responses 7 | class Driver < Base 8 | attr_reader :results 9 | 10 | def initialize(responses, options = {}) 11 | @wrap_level = options[:wrap_level] || Neo4j::Core::Config.wrapping_level 12 | 13 | @results = responses.map(&method(:result_from_data)) 14 | end 15 | 16 | def result_from_data(entities_data) 17 | rows = entities_data.map do |entity_data| 18 | wrap_value(entity_data.values) 19 | end 20 | 21 | Neo4j::Core::CypherSession::Result.new(entities_data.keys, rows) 22 | end 23 | 24 | def wrap_by_level(none_proc) 25 | super(@wrap_level == :none ? none_proc.call : nil) 26 | end 27 | 28 | private 29 | 30 | # In the future the ::Neo4j::Core::Node should either monkey patch or wrap Neo4j::Driver:Types::Node to avoid 31 | # multiple object creation. This is probably best done once the other adapters (http, embedded) are removed. 32 | def wrap_node(node) 33 | wrap_by_level(-> { node.properties }) { ::Neo4j::Core::Node.new(node.id, node.labels, node.properties) } 34 | end 35 | 36 | def wrap_relationship(rel) 37 | wrap_by_level(-> { rel.properties }) do 38 | ::Neo4j::Core::Relationship.new(rel.id, rel.type, rel.properties, rel.start_node_id, rel.end_node_id) 39 | end 40 | end 41 | 42 | def wrap_path(path) 43 | nodes = path.nodes 44 | relationships = path.relationships 45 | wrap_by_level(-> { nodes.zip(relationships).flatten.compact.map(&:properties) }) do 46 | ::Neo4j::Core::Path.new(nodes.map(&method(:wrap_node)), 47 | relationships.map(&method(:wrap_relationship)), 48 | nil) # remove directions from Path, looks like unused 49 | end 50 | end 51 | 52 | def wrap_value(value) 53 | if value.is_a? Array 54 | value.map(&method(:wrap_value)) 55 | elsif value.is_a? Hash 56 | value.map { |key, val| [key, wrap_value(val)] }.to_h 57 | elsif value.is_a? Neo4j::Driver::Types::Node 58 | wrap_node(value) 59 | elsif value.is_a? Neo4j::Driver::Types::Relationship 60 | wrap_relationship(value) 61 | elsif value.is_a? Neo4j::Driver::Types::Path 62 | wrap_path(value) 63 | else 64 | value 65 | end 66 | end 67 | end 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/neo4j/core/cypher_session/responses/embedded.rb: -------------------------------------------------------------------------------- 1 | require 'neo4j/core/cypher_session/responses' 2 | 3 | module Neo4j 4 | module Core 5 | class CypherSession 6 | module Responses 7 | class Embedded < Base 8 | attr_reader :results, :request_data 9 | 10 | def initialize(execution_results, options = {}) 11 | # validate_response!(execution_results) 12 | 13 | @wrap_level = options[:wrap_level] || Neo4j::Core::Config.wrapping_level 14 | 15 | @results = execution_results.map do |execution_result| 16 | result_from_execution_result(execution_result) 17 | end 18 | end 19 | 20 | private 21 | 22 | def result_from_execution_result(execution_result) 23 | columns = execution_result.columns.to_a 24 | rows = execution_result.map do |execution_result_row| 25 | columns.map { |column| wrap_entity(execution_result_row[column]) } 26 | end 27 | Result.new(columns, rows) 28 | end 29 | 30 | def wrap_entity(entity) 31 | if entity.is_a?(Array) || 32 | entity.is_a?(Java::ScalaCollectionConvert::Wrappers::SeqWrapper) 33 | entity.to_a.map(&method(:wrap_entity)) 34 | else 35 | _wrap_entity(entity) 36 | end 37 | end 38 | 39 | def _wrap_entity(entity) 40 | case @wrap_level 41 | when :none then wrap_value(entity) 42 | when :core_entity, :proc 43 | if type = type_for_entity(entity) 44 | result = send("wrap_#{type}", entity) 45 | 46 | @wrap_level == :proc ? result.wrap : result 47 | else 48 | wrap_value(entity) 49 | end 50 | else 51 | fail ArgumentError, "Inalid wrap_level: #{@wrap_level.inspect}" 52 | end 53 | end 54 | 55 | def type_for_entity(entity) 56 | if entity.is_a?(Java::OrgNeo4jKernelImplCore::NodeProxy) 57 | :node 58 | elsif entity.is_a?(Java::OrgNeo4jKernelImplCore::RelationshipProxy) 59 | :relationship 60 | elsif entity.respond_to?(:path_entities) 61 | :path 62 | end 63 | end 64 | 65 | def wrap_node(entity) 66 | ::Neo4j::Core::Node.new(entity.get_id, 67 | entity.get_labels.map(&:to_s), 68 | get_entity_properties(entity)) 69 | end 70 | 71 | def wrap_relationship(entity) 72 | ::Neo4j::Core::Relationship.new(entity.get_id, 73 | entity.get_type.name, 74 | get_entity_properties(entity), 75 | entity.get_start_node.id, 76 | entity.get_end_node.id) 77 | end 78 | 79 | def wrap_path(entity) 80 | ::Neo4j::Core::Path.new(entity.nodes.map(&method(:wrap_node)), 81 | entity.relationships.map(&method(:wrap_relationship)), 82 | nil) 83 | end 84 | 85 | def wrap_value(entity) 86 | case entity 87 | when Java::ScalaCollectionConvert::Wrappers::MapWrapper 88 | entity.each_with_object({}) { |(k, v), r| r[k.to_sym] = _wrap_entity(v) } 89 | when Java::OrgNeo4jKernelImplCore::NodeProxy, Java::OrgNeo4jKernelImplCore::RelationshipProxy 90 | entity.property_keys.each_with_object({}) { |key, hash| hash[key.to_sym] = entity.get_property(key) } 91 | else 92 | if entity.respond_to?(:path_entities) || entity.is_a?(Java::ScalaCollectionConvert::Wrappers::SeqWrapper) 93 | entity.to_a.map(&method(:_wrap_entity)) 94 | else 95 | # Convert from Java? 96 | entity.is_a?(Hash) ? entity.symbolize_keys : entity 97 | end 98 | end 99 | end 100 | 101 | def get_entity_properties(entity) 102 | entity.get_property_keys.each_with_object({}) do |key, result| 103 | result[key.to_sym] = entity.get_property(key) 104 | end 105 | end 106 | 107 | def validate_response!(_execution_results) 108 | require 'pry' 109 | end 110 | end 111 | end 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/neo4j/core/cypher_session/responses/http.rb: -------------------------------------------------------------------------------- 1 | require 'neo4j/core/cypher_session/responses' 2 | 3 | module Neo4j 4 | module Core 5 | class CypherSession 6 | module Responses 7 | class HTTP < Base 8 | attr_reader :results, :request_data 9 | 10 | def initialize(faraday_response, options = {}) 11 | @faraday_response = faraday_response 12 | @request_data = request_data 13 | 14 | validate_faraday_response!(faraday_response) 15 | 16 | @wrap_level = options[:wrap_level] || Neo4j::Core::Config.wrapping_level 17 | 18 | @results = faraday_response.body[:results].map do |result_data| 19 | result_from_data(result_data[:columns], result_data[:data]) 20 | end 21 | end 22 | 23 | def result_from_data(columns, entities_data) 24 | rows = entities_data.map do |entity_data| 25 | wrap_entity entity_data[:row], entity_data[:rest] 26 | end 27 | 28 | Result.new(columns, rows) 29 | end 30 | 31 | def wrap_entity(row_datum, rest_datum) 32 | if rest_datum.is_a?(Array) 33 | row_datum.zip(rest_datum).map { |row, rest| wrap_entity(row, rest) } 34 | elsif ident = identify_entity(rest_datum) 35 | send("wrap_#{ident}", rest_datum, row_datum) 36 | elsif rest_datum.is_a?(Hash) 37 | rest_datum.each_with_object({}) do |(k, v), result| 38 | result[k.to_sym] = wrap_entity(row_datum[k], v) 39 | end 40 | else 41 | row_datum 42 | end 43 | end 44 | 45 | private 46 | 47 | def identify_entity(rest_datum) 48 | return if !rest_datum.is_a?(Hash) 49 | self_string = rest_datum[:self] 50 | if self_string 51 | type = self_string.split('/')[-2] 52 | type.to_sym if %w[node relationship].include?(type) 53 | elsif %i[nodes relationships start end length].all? { |k| rest_datum.key?(k) } 54 | :path 55 | end 56 | end 57 | 58 | def wrap_node(rest_datum, row_datum) 59 | wrap_by_level(row_datum) do 60 | metadata_data = rest_datum[:metadata] 61 | ::Neo4j::Core::Node.new(id_from_rest_datum(rest_datum), 62 | metadata_data && metadata_data[:labels], 63 | rest_datum[:data]) 64 | end 65 | end 66 | 67 | def wrap_relationship(rest_datum, row_datum) 68 | wrap_by_level(row_datum) do 69 | metadata_data = rest_datum[:metadata] 70 | ::Neo4j::Core::Relationship.new(id_from_rest_datum(rest_datum), 71 | metadata_data && metadata_data[:type], 72 | rest_datum[:data], 73 | id_from_url(rest_datum[:start]), 74 | id_from_url(rest_datum[:end])) 75 | end 76 | end 77 | 78 | def wrap_path(rest_datum, row_datum) 79 | wrap_by_level(row_datum) do 80 | nodes = rest_datum[:nodes].each_with_index.map do |url, i| 81 | Node.from_url(url, row_datum[2 * i]) 82 | end 83 | relationships = rest_datum[:relationships].each_with_index.map do |url, i| 84 | Relationship.from_url(url, row_datum[(2 * i) + 1]) 85 | end 86 | 87 | ::Neo4j::Core::Path.new(nodes, relationships, rest_datum[:directions]) 88 | end 89 | end 90 | 91 | def id_from_rest_datum(rest_datum) 92 | if rest_datum[:metadata] 93 | rest_datum[:metadata][:id] 94 | else 95 | id_from_url(rest_datum[:self]) 96 | end 97 | end 98 | 99 | def id_from_url(url) 100 | url.split('/').last.to_i 101 | end 102 | 103 | def validate_faraday_response!(faraday_response) 104 | if faraday_response.body.is_a?(Hash) && error = faraday_response.body[:errors][0] 105 | fail CypherError.new_from(error[:code], error[:message], error[:stack_trace]) 106 | end 107 | 108 | return if (200..299).cover?(status = faraday_response.status) 109 | 110 | fail CypherError, "Expected 200-series response for #{faraday_response.env.url} (got #{status})" 111 | end 112 | end 113 | end 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/neo4j/core/cypher_session/result.rb: -------------------------------------------------------------------------------- 1 | require 'neo4j/core/node' 2 | require 'neo4j/core/relationship' 3 | require 'neo4j/core/path' 4 | 5 | module Neo4j 6 | module Core 7 | class CypherSession 8 | class Result 9 | attr_reader :columns, :rows 10 | 11 | def initialize(columns, rows) 12 | @columns = columns.map(&:to_sym) 13 | @rows = rows 14 | @struct_class = Struct.new(:index, *@columns) 15 | end 16 | 17 | include Enumerable 18 | 19 | def each 20 | structs.each do |struct| 21 | yield struct 22 | end 23 | end 24 | 25 | def structs 26 | @structs ||= rows.each_with_index.map do |row, index| 27 | @struct_class.new(index, *row) 28 | end 29 | end 30 | 31 | def hashes 32 | @hashes ||= rows.map do |row| 33 | Hash[@columns.zip(row)] 34 | end 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/neo4j/core/cypher_session/transactions.rb: -------------------------------------------------------------------------------- 1 | require 'neo4j/transaction' 2 | 3 | module Neo4j 4 | module Core 5 | class CypherSession 6 | module Transactions 7 | class Base < Neo4j::Transaction::Base 8 | def query(*args) 9 | options = if args[0].is_a?(::Neo4j::Core::Query) 10 | args[1] ||= {} 11 | else 12 | args[1] ||= {} 13 | args[2] ||= {} 14 | end 15 | options[:transaction] ||= self 16 | 17 | adaptor.query(@session, *args) 18 | end 19 | 20 | def queries(options = {}, &block) 21 | adaptor.queries(@session, {transaction: self}.merge(options), &block) 22 | end 23 | 24 | def after_commit_registry 25 | @after_commit_registry ||= [] 26 | end 27 | 28 | def after_commit(&block) 29 | after_commit_registry << block 30 | end 31 | 32 | def post_close! 33 | super 34 | after_commit_registry.each(&:call) unless failed? 35 | end 36 | 37 | private 38 | 39 | # Because we're inheriting from the old Transaction class 40 | # but the new adaptors work much like the old sessions 41 | def adaptor 42 | @session.adaptor 43 | end 44 | end 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/neo4j/core/cypher_session/transactions/bolt.rb: -------------------------------------------------------------------------------- 1 | require 'neo4j/core/cypher_session/transactions' 2 | 3 | module Neo4j 4 | module Core 5 | class CypherSession 6 | module Transactions 7 | class Bolt < Base 8 | def initialize(*args) 9 | super 10 | 11 | tx_query('BEGIN') if root? 12 | end 13 | 14 | def commit 15 | tx_query('COMMIT') if root? 16 | end 17 | 18 | def delete 19 | tx_query('ROLLBACK') 20 | end 21 | 22 | def started? 23 | true 24 | end 25 | 26 | private 27 | 28 | def tx_query(cypher) 29 | query = Adaptors::Base::Query.new(cypher, {}, cypher) 30 | adaptor.send(:query_set, self, [query], skip_instrumentation: true) 31 | end 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/neo4j/core/cypher_session/transactions/driver.rb: -------------------------------------------------------------------------------- 1 | require 'neo4j/core/cypher_session/transactions' 2 | 3 | module Neo4j 4 | module Core 5 | class CypherSession 6 | module Transactions 7 | class Driver < Base 8 | attr_reader :driver_tx, :driver_session 9 | 10 | def initialize(*args) 11 | super 12 | return unless root? 13 | @driver_session = session.adaptor.driver.session(Neo4j::Driver::AccessMode::WRITE) 14 | @driver_tx = @driver_session.begin_transaction 15 | rescue StandardError => e 16 | clean_transaction_registry 17 | @driver_tx.close if @driver_tx 18 | @driver_session.close if @driver_session 19 | raise e 20 | end 21 | 22 | def commit 23 | return unless root? 24 | begin 25 | @driver_tx.success 26 | @driver_tx.close 27 | ensure 28 | @driver_session.close 29 | end 30 | end 31 | 32 | def delete 33 | root.driver_tx.failure 34 | root.driver_tx.close 35 | root.driver_session.close 36 | end 37 | 38 | def started? 39 | true 40 | end 41 | 42 | def root_tx 43 | root.driver_tx 44 | end 45 | 46 | private 47 | 48 | def clean_transaction_registry 49 | Neo4j::Transaction::TransactionsRegistry.transactions_by_session_id[session.object_id] = [] 50 | end 51 | end 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/neo4j/core/cypher_session/transactions/embedded.rb: -------------------------------------------------------------------------------- 1 | require 'neo4j/core/cypher_session/transactions' 2 | 3 | module Neo4j 4 | module Core 5 | class CypherSession 6 | module Transactions 7 | class Embedded < Base 8 | def initialize(*args) 9 | super 10 | @java_tx = adaptor.graph_db.begin_tx 11 | end 12 | 13 | def commit 14 | return if !@java_tx 15 | 16 | @java_tx.success 17 | @java_tx.close 18 | rescue Java::OrgNeo4jGraphdb::TransactionFailureException => e 19 | raise CypherError, e.message 20 | end 21 | 22 | def delete 23 | return if !@java_tx 24 | 25 | @java_tx.failure 26 | @java_tx.close 27 | end 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/neo4j/core/cypher_session/transactions/http.rb: -------------------------------------------------------------------------------- 1 | require 'neo4j/core/cypher_session/transactions' 2 | 3 | module Neo4j 4 | module Core 5 | class CypherSession 6 | module Transactions 7 | class HTTP < Base 8 | # Should perhaps have transaction adaptors only define #close 9 | # commit/delete are, I think, an implementation detail 10 | 11 | def commit 12 | adaptor.requestor.request(:post, query_path(true)) if started? 13 | end 14 | 15 | def delete 16 | adaptor.requestor.request(:delete, query_path) if started? 17 | end 18 | 19 | def query_path(commit = false) 20 | if id 21 | "/db/data/transaction/#{id}" 22 | else 23 | '/db/data/transaction' 24 | end.tap do |path| 25 | path << '/commit' if commit 26 | end 27 | end 28 | 29 | # Takes the transaction URL from Neo4j and parses out the ID 30 | def apply_id_from_url!(url) 31 | root.instance_variable_set('@id', url.match(%r{/(\d+)/?$})[1].to_i) if url 32 | # @id = url.match(%r{/(\d+)/?$})[1].to_i if url 33 | end 34 | 35 | def started? 36 | !!id 37 | end 38 | 39 | def id 40 | root.instance_variable_get('@id') 41 | end 42 | 43 | private 44 | 45 | def connection 46 | adaptor.connection 47 | end 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/neo4j/core/helpers.rb: -------------------------------------------------------------------------------- 1 | # TODO: Needed? 2 | module Neo4j 3 | module Core 4 | module TxMethods 5 | def tx_methods(*methods) 6 | methods.each do |method| 7 | tx_method = "#{method}_in_tx" 8 | send(:alias_method, tx_method, method) 9 | send(:define_method, method) do |*args, &block| 10 | session = args.last.is_a?(Neo4j::Session) ? args.pop : Neo4j::Session.current! 11 | 12 | Neo4j::Transaction.run(session.auto_commit?) { send(tx_method, *args, &block) } 13 | end 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/neo4j/core/instrumentable.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/notifications' 2 | 3 | module Neo4j 4 | module Core 5 | module Instrumentable 6 | def self.included(base) 7 | base.send :include, InstanceMethods 8 | base.extend ClassMethods 9 | end 10 | 11 | module InstanceMethods 12 | end 13 | 14 | module ClassMethods 15 | def instrument(name, label, arguments) 16 | # defining class methods 17 | klass = class << self; self; end 18 | klass.instance_eval do 19 | define_method("subscribe_to_#{name}") do |&b| 20 | ActiveSupport::Notifications.subscribe(label) do |a, start, finish, id, payload| 21 | b.call yield(a, start, finish, id, payload) 22 | end 23 | end 24 | 25 | define_method("instrument_#{name}") do |*args, &b| 26 | hash = arguments.each_with_index.each_with_object({}) do |(argument, i), result| 27 | result[argument.to_sym] = args[i] 28 | end 29 | ActiveSupport::Notifications.instrument(label, hash) { b.call } 30 | end 31 | end 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/neo4j/core/label.rb: -------------------------------------------------------------------------------- 1 | module Neo4j 2 | module Core 3 | class Label 4 | attr_reader :name 5 | 6 | def initialize(name, session) 7 | @name = name 8 | @session = session 9 | end 10 | 11 | def create_index(property, options = {}) 12 | validate_index_options!(options) 13 | properties = property.is_a?(Array) ? property.join(',') : property 14 | schema_query("CREATE INDEX ON :`#{@name}`(#{properties})") 15 | end 16 | 17 | def drop_index(property, options = {}) 18 | validate_index_options!(options) 19 | schema_query("DROP INDEX ON :`#{@name}`(#{property})") 20 | end 21 | 22 | # Creates a neo4j constraint on a property 23 | # See http://docs.neo4j.org/chunked/stable/query-constraints.html 24 | # @example 25 | # label = Neo4j::Label.create(:person, session) 26 | # label.create_constraint(:name, {type: :unique}, session) 27 | # 28 | def create_constraint(property, constraints) 29 | cypher = case constraints[:type] 30 | when :unique, :uniqueness 31 | "CREATE CONSTRAINT ON (n:`#{name}`) ASSERT n.`#{property}` IS UNIQUE" 32 | else 33 | fail "Not supported constraint #{constraints.inspect} for property #{property} (expected :type => :unique)" 34 | end 35 | schema_query(cypher) 36 | end 37 | 38 | def create_uniqueness_constraint(property, options = {}) 39 | create_constraint(property, options.merge(type: :unique)) 40 | end 41 | 42 | # Drops a neo4j constraint on a property 43 | # See http://docs.neo4j.org/chunked/stable/query-constraints.html 44 | # @example 45 | # label = Neo4j::Label.create(:person, session) 46 | # label.create_constraint(:name, {type: :unique}, session) 47 | # label.drop_constraint(:name, {type: :unique}, session) 48 | # 49 | def drop_constraint(property, constraint) 50 | cypher = case constraint[:type] 51 | when :unique, :uniqueness 52 | "DROP CONSTRAINT ON (n:`#{name}`) ASSERT n.`#{property}` IS UNIQUE" 53 | else 54 | fail "Not supported constraint #{constraint.inspect}" 55 | end 56 | schema_query(cypher) 57 | end 58 | 59 | def drop_uniqueness_constraint(property, options = {}) 60 | drop_constraint(property, options.merge(type: :unique)) 61 | end 62 | 63 | def indexes 64 | @session.indexes.select do |definition| 65 | definition[:label] == @name.to_sym 66 | end 67 | end 68 | 69 | def self.indexes_for(session) 70 | session.indexes 71 | end 72 | 73 | def drop_indexes 74 | indexes.each do |definition| 75 | begin 76 | @session.query("DROP INDEX ON :`#{definition[:label]}`(#{definition[:properties][0]})") 77 | rescue Neo4j::Server::CypherResponse::ResponseError 78 | # This will error on each constraint. Ignore and continue. 79 | next 80 | end 81 | end 82 | end 83 | 84 | def self.drop_indexes_for(session) 85 | indexes_for(session).each do |definition| 86 | begin 87 | session.query("DROP INDEX ON :`#{definition[:label]}`(#{definition[:properties][0]})") 88 | rescue Neo4j::Server::CypherResponse::ResponseError 89 | # This will error on each constraint. Ignore and continue. 90 | next 91 | end 92 | end 93 | end 94 | 95 | def index?(property) 96 | indexes.any? { |definition| definition[:properties] == [property.to_sym] } 97 | end 98 | 99 | def constraints(_options = {}) 100 | @session.constraints.select do |definition| 101 | definition[:label] == @name.to_sym 102 | end 103 | end 104 | 105 | def uniqueness_constraints(_options = {}) 106 | constraints.select do |definition| 107 | definition[:type] == :uniqueness 108 | end 109 | end 110 | 111 | def drop_uniqueness_constraints 112 | uniqueness_constraints.each do |definition| 113 | @session.query("DROP CONSTRAINT ON (n:`#{definition[:label]}`) ASSERT n.`#{definition[:properties][0]}` IS UNIQUE") 114 | end 115 | end 116 | 117 | def self.drop_uniqueness_constraints_for(session) 118 | session.constraints.each do |definition| 119 | session.query("DROP CONSTRAINT ON (n:`#{definition[:label]}`) ASSERT n.`#{definition[:properties][0]}` IS UNIQUE") 120 | end 121 | end 122 | 123 | def constraint?(property) 124 | constraints.any? { |definition| definition[:properties] == [property.to_sym] } 125 | end 126 | 127 | def uniqueness_constraint?(property) 128 | uniqueness_constraints.include?([property]) 129 | end 130 | 131 | def self.wait_for_schema_changes(session) 132 | schema_threads(session).map(&:join) 133 | set_schema_threads(session, []) 134 | end 135 | 136 | private 137 | 138 | # Store schema threads on the session so that we can easily wait for all 139 | # threads on a session regardless of label 140 | def schema_threads 141 | self.class.schema_threads(@session) 142 | end 143 | 144 | def schema_threads=(array) 145 | self.class.set_schema_threads(@session, array) 146 | end 147 | 148 | class << self 149 | def schema_threads(session) 150 | session.instance_variable_get('@_schema_threads') || [] 151 | end 152 | 153 | def set_schema_threads(session, array) 154 | session.instance_variable_set('@_schema_threads', array) 155 | end 156 | end 157 | 158 | def schema_query(cypher) 159 | @session.transaction { |tx| tx.query(cypher, {}) } 160 | end 161 | 162 | def validate_index_options!(options) 163 | return unless options[:type] && options[:type] != :exact 164 | fail "Type #{options[:type]} is not supported" 165 | end 166 | end 167 | end 168 | end 169 | -------------------------------------------------------------------------------- /lib/neo4j/core/logging.rb: -------------------------------------------------------------------------------- 1 | # Copied largely from activerecord/lib/active_record/log_subscriber.rb 2 | module Neo4j 3 | module Core 4 | module Logging 5 | class << self 6 | def first_external_path_and_line(callstack) 7 | line = callstack.find do |frame| 8 | frame.absolute_path && !ignored_callstack(frame.absolute_path) 9 | end 10 | 11 | offending_line = line || callstack.first 12 | 13 | [offending_line.path, 14 | offending_line.lineno] 15 | end 16 | 17 | NEO4J_CORE_GEM_ROOT = File.expand_path('../../..', __dir__) + '/' 18 | 19 | def ignored_callstack(path) 20 | paths_to_ignore.any?(&path.method(:start_with?)) 21 | end 22 | 23 | def paths_to_ignore 24 | @paths_to_ignore ||= [NEO4J_CORE_GEM_ROOT, 25 | RbConfig::CONFIG['rubylibdir'], 26 | neo4j_gem_path, 27 | active_support_gem_path].compact 28 | end 29 | 30 | def neo4j_gem_path 31 | return if !defined?(::Rails.root) 32 | 33 | @neo4j_gem_path ||= File.expand_path('../../..', Neo4j::ActiveBase.method(:current_session).source_location[0]) 34 | end 35 | 36 | def active_support_gem_path 37 | return if !defined?(::ActiveSupport::Notifications) 38 | 39 | @active_support_gem_path ||= File.expand_path('../../..', ActiveSupport::Notifications.method(:subscribe).source_location[0]) 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/neo4j/core/node.rb: -------------------------------------------------------------------------------- 1 | require 'neo4j/core/wrappable' 2 | require 'active_support/core_ext/hash/keys' 3 | 4 | module Neo4j 5 | module Core 6 | class Node 7 | attr_reader :id, :labels, :properties 8 | alias props properties 9 | 10 | include Wrappable 11 | 12 | # Perhaps we should deprecate this? 13 | alias neo_id id 14 | 15 | def initialize(id, labels, properties = {}) 16 | @id = id 17 | @labels = labels.map(&:to_sym) unless labels.nil? 18 | @properties = properties.symbolize_keys 19 | end 20 | 21 | def ==(other) 22 | other.is_a?(Node) && neo_id == other.neo_id 23 | end 24 | 25 | class << self 26 | def from_url(url, properties = {}) 27 | id = url.split('/')[-1].to_i 28 | labels = nil # unknown 29 | properties = properties 30 | 31 | new(id, labels, properties) 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/neo4j/core/path.rb: -------------------------------------------------------------------------------- 1 | module Neo4j 2 | module Core 3 | class Path 4 | attr_reader :nodes, :relationships, :directions 5 | 6 | include Wrappable 7 | 8 | def initialize(nodes, relationships, directions) 9 | @nodes = nodes 10 | @relationships = relationships 11 | @directions = directions 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/neo4j/core/query.rb: -------------------------------------------------------------------------------- 1 | require 'neo4j/core/query_clauses' 2 | require 'neo4j/core/query_find_in_batches' 3 | require 'active_support/notifications' 4 | 5 | module Neo4j 6 | module Core 7 | # Allows for generation of cypher queries via ruby method calls (inspired by ActiveRecord / arel syntax) 8 | # 9 | # Can be used to express cypher queries in ruby nicely, or to more easily generate queries programatically. 10 | # 11 | # Also, queries can be passed around an application to progressively build a query across different concerns 12 | # 13 | # See also the following link for full cypher language documentation: 14 | # http://docs.neo4j.org/chunked/milestone/cypher-query-lang.html 15 | class Query 16 | include Neo4j::Core::QueryClauses 17 | include Neo4j::Core::QueryFindInBatches 18 | DEFINED_CLAUSES = {} 19 | 20 | 21 | attr_accessor :clauses 22 | 23 | class Parameters 24 | def initialize(hash = nil) 25 | @parameters = (hash || {}) 26 | end 27 | 28 | def to_hash 29 | @parameters 30 | end 31 | 32 | def copy 33 | self.class.new(@parameters.dup) 34 | end 35 | 36 | def add_param(key, value) 37 | free_param_key(key).tap do |k| 38 | @parameters[k.freeze] = value 39 | end 40 | end 41 | 42 | def remove_param(key) 43 | @parameters.delete(key.to_sym) 44 | end 45 | 46 | def add_params(params) 47 | params.map do |key, value| 48 | add_param(key, value) 49 | end 50 | end 51 | 52 | private 53 | 54 | def free_param_key(key) 55 | k = key.to_sym 56 | 57 | return k if !@parameters.key?(k) 58 | 59 | i = 2 60 | i += 1 while @parameters.key?("#{key}#{i}".to_sym) 61 | 62 | "#{key}#{i}".to_sym 63 | end 64 | end 65 | 66 | class << self 67 | attr_accessor :pretty_cypher 68 | end 69 | 70 | def initialize(options = {}) 71 | @session = options[:session] 72 | 73 | @options = options 74 | @clauses = [] 75 | @_params = {} 76 | @params = Parameters.new 77 | end 78 | 79 | def inspect 80 | "#" 81 | end 82 | 83 | # @method start *args 84 | # START clause 85 | # @return [Query] 86 | 87 | # @method match *args 88 | # MATCH clause 89 | # @return [Query] 90 | 91 | # @method optional_match *args 92 | # OPTIONAL MATCH clause 93 | # @return [Query] 94 | 95 | # @method using *args 96 | # USING clause 97 | # @return [Query] 98 | 99 | # @method where *args 100 | # WHERE clause 101 | # @return [Query] 102 | 103 | # @method with *args 104 | # WITH clause 105 | # @return [Query] 106 | 107 | # @method with_distinct *args 108 | # WITH clause with DISTINCT specified 109 | # @return [Query] 110 | 111 | # @method order *args 112 | # ORDER BY clause 113 | # @return [Query] 114 | 115 | # @method limit *args 116 | # LIMIT clause 117 | # @return [Query] 118 | 119 | # @method skip *args 120 | # SKIP clause 121 | # @return [Query] 122 | 123 | # @method set *args 124 | # SET clause 125 | # @return [Query] 126 | 127 | # @method remove *args 128 | # REMOVE clause 129 | # @return [Query] 130 | 131 | # @method unwind *args 132 | # UNWIND clause 133 | # @return [Query] 134 | 135 | # @method return *args 136 | # RETURN clause 137 | # @return [Query] 138 | 139 | # @method create *args 140 | # CREATE clause 141 | # @return [Query] 142 | 143 | # @method create_unique *args 144 | # CREATE UNIQUE clause 145 | # @return [Query] 146 | 147 | # @method merge *args 148 | # MERGE clause 149 | # @return [Query] 150 | 151 | # @method on_create_set *args 152 | # ON CREATE SET clause 153 | # @return [Query] 154 | 155 | # @method on_match_set *args 156 | # ON MATCH SET clause 157 | # @return [Query] 158 | 159 | # @method delete *args 160 | # DELETE clause 161 | # @return [Query] 162 | 163 | # @method detach_delete *args 164 | # DETACH DELETE clause 165 | # @return [Query] 166 | 167 | 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] # rubocop:disable Metrics/LineLength 168 | BREAK_METHODS = %(with with_distinct call) 169 | 170 | CLAUSIFY_CLAUSE = proc { |method| const_get(method.to_s.split('_').map(&:capitalize).join + 'Clause') } 171 | CLAUSES = METHODS.map(&CLAUSIFY_CLAUSE) 172 | 173 | METHODS.each_with_index do |clause, i| 174 | clause_class = CLAUSES[i] 175 | 176 | DEFINED_CLAUSES[clause.to_sym] = clause_class 177 | define_method(clause) do |*args| 178 | result = build_deeper_query(clause_class, args) 179 | 180 | BREAK_METHODS.include?(clause) ? result.break : result 181 | end 182 | end 183 | 184 | alias offset skip 185 | alias order_by order 186 | 187 | # Clears out previous order clauses and allows only for those specified by args 188 | def reorder(*args) 189 | query = copy 190 | 191 | query.remove_clause_class(OrderClause) 192 | query.order(*args) 193 | end 194 | 195 | # Works the same as the #where method, but the clause is surrounded by a 196 | # Cypher NOT() function 197 | def where_not(*args) 198 | build_deeper_query(WhereClause, args, not: true) 199 | end 200 | 201 | # Works the same as the #set method, but when given a nested array it will set properties rather than setting entire objects 202 | # @example 203 | # # Creates a query representing the cypher: MATCH (n:Person) SET n.age = 19 204 | # Query.new.match(n: :Person).set_props(n: {age: 19}) 205 | def set_props(*args) # rubocop:disable Naming/AccessorMethodName 206 | build_deeper_query(SetClause, args, set_props: true) 207 | end 208 | 209 | # 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 210 | # @example 211 | # # Creates a query representing the cypher: MATCH (q:Person), r:Car MATCH (p: Person)-->q 212 | # Query.new.match(q: Person).match('r:Car').break.match('(p: Person)-->q') 213 | def break 214 | build_deeper_query(nil) 215 | end 216 | 217 | # Allows for the specification of values for params specified in query 218 | # @example 219 | # # Creates a query representing the cypher: MATCH (q: Person {id: {id}}) 220 | # # Calls to params don't affect the cypher query generated, but the params will be 221 | # # Passed down when the query is made 222 | # Query.new.match('(q: Person {id: {id}})').params(id: 12) 223 | # 224 | def params(args) 225 | copy.tap { |new_query| new_query.instance_variable_get('@params'.freeze).add_params(args) } 226 | end 227 | 228 | def unwrapped 229 | @_unwrapped_obj = true 230 | self 231 | end 232 | 233 | def unwrapped? 234 | !!@_unwrapped_obj 235 | end 236 | 237 | def session_is_new_api? 238 | defined?(::Neo4j::Core::CypherSession) && @session.is_a?(::Neo4j::Core::CypherSession) 239 | end 240 | 241 | def response 242 | return @response if @response 243 | 244 | @response = if session_is_new_api? 245 | @session.query(self, transaction: Transaction.current_for(@session), wrap_level: (:core_entity if unwrapped?)) 246 | else 247 | @session._query(to_cypher, merge_params, 248 | context: @options[:context], pretty_cypher: (pretty_cypher if self.class.pretty_cypher)).tap(&method(:raise_if_cypher_error!)) 249 | end 250 | end 251 | 252 | def raise_if_cypher_error!(response) 253 | response.raise_cypher_error if response.respond_to?(:error?) && response.error? 254 | end 255 | 256 | def match_nodes(hash, optional_match = false) 257 | hash.inject(self) do |query, (variable, node_object)| 258 | neo_id = (node_object.respond_to?(:neo_id) ? node_object.neo_id : node_object) 259 | 260 | match_method = optional_match ? :optional_match : :match 261 | query.send(match_method, variable).where(variable => {neo_id: neo_id}) 262 | end 263 | end 264 | 265 | def optional_match_nodes(hash) 266 | match_nodes(hash, true) 267 | end 268 | 269 | include Enumerable 270 | 271 | def count(var = nil) 272 | v = var.nil? ? '*' : var 273 | pluck("count(#{v})").first 274 | end 275 | 276 | def each 277 | response = self.response 278 | if defined?(Neo4j::Server::CypherResponse) && response.is_a?(Neo4j::Server::CypherResponse) 279 | response.unwrapped! if unwrapped? 280 | response.to_node_enumeration 281 | elsif defined?(Neo4j::Core::CypherSession::Result) && response.is_a?(Neo4j::Core::CypherSession::Result) 282 | response.to_a 283 | else 284 | Neo4j::Embedded::ResultWrapper.new(response, to_cypher, unwrapped?) 285 | end.each { |object| yield object } 286 | end 287 | 288 | # @method to_a 289 | # 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 290 | # @return [Array] 291 | # @raise [Neo4j::Server::CypherResponse::ResponseError] Raises errors from neo4j server 292 | 293 | 294 | # Executes a query without returning the result 295 | # @return [Boolean] true if successful 296 | # @raise [Neo4j::Server::CypherResponse::ResponseError] Raises errors from neo4j server 297 | def exec 298 | response 299 | 300 | true 301 | end 302 | 303 | # Return the specified columns as an array. 304 | # If one column is specified, a one-dimensional array is returned with the values of that column 305 | # If two columns are specified, a n-dimensional array is returned with the values of those columns 306 | # 307 | # @example 308 | # Query.new.match(n: :Person).return(p: :name}.pluck(p: :name) # => Array of names 309 | # @example 310 | # Query.new.match(n: :Person).return(p: :name}.pluck('p, DISTINCT p.name') # => Array of [node, name] pairs 311 | # 312 | def pluck(*columns) 313 | fail ArgumentError, 'No columns specified for Query#pluck' if columns.size.zero? 314 | 315 | query = return_query(columns) 316 | columns = query.response.columns 317 | 318 | if columns.size == 1 319 | column = columns[0] 320 | query.map { |row| row[column] } 321 | else 322 | query.map { |row| columns.map { |column| row[column] } } 323 | end 324 | end 325 | 326 | def return_query(columns) 327 | query = copy 328 | query.remove_clause_class(ReturnClause) 329 | 330 | query.return(*columns) 331 | end 332 | 333 | # Returns a CYPHER query string from the object query representation 334 | # @example 335 | # Query.new.match(p: :Person).where(p: {age: 30}) # => "MATCH (p:Person) WHERE p.age = 30 336 | # 337 | # @return [String] Resulting cypher query string 338 | EMPTY = ' ' 339 | NEWLINE = "\n" 340 | def to_cypher(options = {}) 341 | join_string = options[:pretty] ? NEWLINE : EMPTY 342 | 343 | cypher_string = partitioned_clauses.map do |clauses| 344 | clauses_by_class = clauses.group_by(&:class) 345 | 346 | cypher_parts = CLAUSES.map do |clause_class| 347 | clause_class.to_cypher(clauses, options[:pretty]) if clauses = clauses_by_class[clause_class] 348 | end.compact 349 | 350 | cypher_parts.join(join_string).tap(&:strip!) 351 | end.join(join_string) 352 | 353 | cypher_string = "CYPHER #{@options[:parser]} #{cypher_string}" if @options[:parser] 354 | cypher_string.tap(&:strip!) 355 | end 356 | alias cypher to_cypher 357 | 358 | def pretty_cypher 359 | to_cypher(pretty: true) 360 | end 361 | 362 | def context 363 | @options[:context] 364 | end 365 | 366 | def parameters 367 | to_cypher 368 | merge_params 369 | end 370 | 371 | def partitioned_clauses 372 | @partitioned_clauses ||= PartitionedClauses.new(@clauses) 373 | end 374 | 375 | def print_cypher 376 | puts to_cypher(pretty: true).gsub(/\e[^m]+m/, '') 377 | end 378 | 379 | # Returns a CYPHER query specifying the union of the callee object's query and the argument's query 380 | # 381 | # @example 382 | # # Generates cypher: MATCH (n:Person) UNION MATCH (o:Person) WHERE o.age = 10 383 | # q = Neo4j::Core::Query.new.match(o: :Person).where(o: {age: 10}) 384 | # result = Neo4j::Core::Query.new.match(n: :Person).union_cypher(q) 385 | # 386 | # @param other [Query] Second half of UNION 387 | # @param options [Hash] Specify {all: true} to use UNION ALL 388 | # @return [String] Resulting UNION cypher query string 389 | def union_cypher(other, options = {}) 390 | "#{to_cypher} UNION#{options[:all] ? ' ALL' : ''} #{other.to_cypher}" 391 | end 392 | 393 | def &(other) 394 | self.class.new(session: @session).tap do |new_query| 395 | new_query.options = options.merge(other.options) 396 | new_query.clauses = clauses + other.clauses 397 | end.params(other._params) 398 | end 399 | 400 | def copy 401 | dup.tap do |query| 402 | to_cypher 403 | query.instance_variable_set('@params'.freeze, @params.copy) 404 | query.instance_variable_set('@partitioned_clauses'.freeze, nil) 405 | query.instance_variable_set('@response'.freeze, nil) 406 | end 407 | end 408 | 409 | def clause?(method) 410 | clause_class = DEFINED_CLAUSES[method] || CLAUSIFY_CLAUSE.call(method) 411 | clauses.any? { |clause| clause.is_a?(clause_class) } 412 | end 413 | 414 | protected 415 | 416 | attr_accessor :session, :options, :_params 417 | 418 | def add_clauses(clauses) 419 | @clauses += clauses 420 | end 421 | 422 | def remove_clause_class(clause_class) 423 | @clauses = @clauses.reject { |clause| clause.is_a?(clause_class) } 424 | end 425 | 426 | private 427 | 428 | def build_deeper_query(clause_class, args = {}, options = {}) 429 | copy.tap do |new_query| 430 | new_query.add_clauses [nil] if [nil, WithClause].include?(clause_class) 431 | new_query.add_clauses clause_class.from_args(args, new_query.instance_variable_get('@params'.freeze), options) if clause_class 432 | end 433 | end 434 | 435 | class PartitionedClauses 436 | def initialize(clauses) 437 | @clauses = clauses 438 | @partitioning = [[]] 439 | end 440 | 441 | include Enumerable 442 | 443 | def each 444 | generate_partitioning! 445 | 446 | @partitioning.each { |partition| yield partition } 447 | end 448 | 449 | def generate_partitioning! 450 | @partitioning = [[]] 451 | 452 | @clauses.each do |clause| 453 | if clause.nil? && !fresh_partition? 454 | @partitioning << [] 455 | elsif clause_is_order_or_limit_directly_following_with_or_order?(clause) 456 | second_to_last << clause 457 | elsif clause_is_with_following_order_or_limit?(clause) 458 | second_to_last << clause 459 | second_to_last.sort_by! { |c| c.is_a?(::Neo4j::Core::QueryClauses::OrderClause) ? 1 : 0 } 460 | else 461 | @partitioning.last << clause 462 | end 463 | end 464 | end 465 | 466 | private 467 | 468 | def fresh_partition? 469 | @partitioning.last == [] 470 | end 471 | 472 | def second_to_last 473 | @partitioning[-2] 474 | end 475 | 476 | def clause_is_order_or_limit_directly_following_with_or_order?(clause) 477 | self.class.clause_is_order_or_limit?(clause) && 478 | @partitioning[-2] && 479 | @partitioning[-1].empty? && 480 | (@partitioning[-2].last.is_a?(::Neo4j::Core::QueryClauses::WithClause) || 481 | @partitioning[-2].last.is_a?(::Neo4j::Core::QueryClauses::OrderClause)) 482 | end 483 | 484 | def clause_is_with_following_order_or_limit?(clause) 485 | clause.is_a?(::Neo4j::Core::QueryClauses::WithClause) && 486 | @partitioning[-2] && @partitioning[-2].any? { |c| self.class.clause_is_order_or_limit?(c) } 487 | end 488 | 489 | class << self 490 | def clause_is_order_or_limit?(clause) 491 | clause.is_a?(::Neo4j::Core::QueryClauses::OrderClause) || 492 | clause.is_a?(::Neo4j::Core::QueryClauses::LimitClause) 493 | end 494 | end 495 | end 496 | 497 | # SHOULD BE DEPRECATED 498 | def merge_params 499 | @merge_params_base ||= @clauses.compact.inject({}) { |params, clause| params.merge!(clause.params) } 500 | @params.to_hash.merge(@merge_params_base) 501 | end 502 | end 503 | end 504 | end 505 | -------------------------------------------------------------------------------- /lib/neo4j/core/query_clauses.rb: -------------------------------------------------------------------------------- 1 | module Neo4j 2 | module Core 3 | module QueryClauses 4 | class ArgError < StandardError 5 | attr_reader :arg_part 6 | def initialize(arg_part = nil) 7 | super 8 | @arg_part = arg_part 9 | end 10 | end 11 | 12 | class Clause 13 | UNDERSCORE = '_' 14 | COMMA_SPACE = ', ' 15 | AND = ' AND ' 16 | PRETTY_NEW_LINE = "\n " 17 | 18 | attr_accessor :params, :arg 19 | attr_reader :options, :param_vars_added 20 | 21 | def initialize(arg, params, options = {}) 22 | @arg = arg 23 | @options = options 24 | @params = params 25 | @param_vars_added = [] 26 | end 27 | 28 | def value 29 | return @value if @value 30 | 31 | [String, Symbol, Integer, Hash, NilClass].each do |arg_class| 32 | from_method = "from_#{arg_class.name.downcase}" 33 | return @value = send(from_method, @arg) if @arg.is_a?(arg_class) && respond_to?(from_method) 34 | end 35 | 36 | fail ArgError 37 | rescue ArgError => arg_error 38 | message = "Invalid argument for #{self.class.keyword}. Full arguments: #{@arg.inspect}" 39 | message += " | Invalid part: #{arg_error.arg_part.inspect}" if arg_error.arg_part 40 | 41 | raise ArgumentError, message 42 | end 43 | 44 | def from_hash(value) 45 | fail ArgError if !respond_to?(:from_key_and_value) 46 | 47 | value.map do |k, v| 48 | from_key_and_value k, v 49 | end 50 | end 51 | 52 | def from_string(value) 53 | value 54 | end 55 | 56 | def node_from_key_and_value(key, value, options = {}) 57 | prefer = options[:prefer] || :var 58 | var = var_from_key_and_value(key, value, prefer) 59 | label = label_from_key_and_value(key, value, prefer) 60 | 61 | attributes = attributes_from_key_and_value(key, value) 62 | 63 | prefix_value = value 64 | if value.is_a?(Hash) 65 | prefix_value = (value.keys.join(UNDERSCORE) if value.values.any? { |v| v.is_a?(Hash) }) 66 | end 67 | 68 | prefix_array = [key, prefix_value].tap(&:compact!).join(UNDERSCORE) 69 | formatted_attributes = attributes_string(attributes, "#{prefix_array}#{UNDERSCORE}") 70 | "(#{var}#{format_label(label)}#{formatted_attributes})" 71 | end 72 | 73 | def var_from_key_and_value(key, value, prefer = :var) 74 | case value 75 | when String, Symbol, Class, Module, NilClass, Array then key 76 | when Hash 77 | key if _use_key_for_var?(value, prefer) 78 | else 79 | fail ArgError, value 80 | end 81 | end 82 | 83 | def label_from_key_and_value(key, value, prefer = :var) 84 | case value 85 | when String, Symbol, Array, NilClass then value 86 | when Class, Module then value.name 87 | when Hash 88 | if value.values.map(&:class) == [Hash] 89 | value.first.first 90 | elsif !_use_key_for_var?(value, prefer) 91 | key 92 | end 93 | else 94 | fail ArgError, value 95 | end 96 | end 97 | 98 | def _use_key_for_var?(value, prefer) 99 | _nested_value_hash?(value) || prefer == :var 100 | end 101 | 102 | def _nested_value_hash?(value) 103 | value.values.any? { |v| v.is_a?(Hash) } 104 | end 105 | 106 | def attributes_from_key_and_value(_key, value) 107 | return nil unless value.is_a?(Hash) 108 | 109 | value.values.map(&:class) == [Hash] ? value.first[1] : value 110 | end 111 | 112 | class << self 113 | def keyword 114 | self::KEYWORD 115 | end 116 | 117 | def keyword_downcase 118 | keyword.downcase 119 | end 120 | 121 | def from_args(args, params, options = {}) 122 | args.flatten! 123 | args.map { |arg| from_arg(arg, params, options) }.tap(&:compact!) 124 | end 125 | 126 | def from_arg(arg, params, options = {}) 127 | new(arg, params, options) if !arg.respond_to?(:empty?) || !arg.empty? 128 | end 129 | 130 | def to_cypher(clauses, pretty = false) 131 | string = clause_string(clauses, pretty) 132 | 133 | final_keyword = if pretty 134 | "#{clause_color}#{keyword}#{ANSI::CLEAR}" 135 | else 136 | keyword 137 | end 138 | 139 | "#{final_keyword} #{string}" if !string.empty? 140 | end 141 | 142 | def clause_string(clauses, pretty) 143 | join_string = pretty ? clause_join + PRETTY_NEW_LINE : clause_join 144 | 145 | strings = clause_strings(clauses) 146 | stripped_string = strings.join(join_string).strip 147 | pretty && strings.size > 1 ? PRETTY_NEW_LINE + stripped_string : stripped_string 148 | end 149 | 150 | def clause_join 151 | '' 152 | end 153 | 154 | def clause_color 155 | ANSI::CYAN 156 | end 157 | 158 | def from_key_and_single_value(key, value) 159 | value.to_sym == :neo_id ? "ID(#{key})" : "#{key}.#{value}" 160 | end 161 | end 162 | 163 | def self.paramaterize_key!(key) 164 | key.tr_s!('^a-zA-Z0-9', UNDERSCORE) 165 | key.gsub!(/^_+|_+$/, '') 166 | end 167 | 168 | def add_param(key, value) 169 | @param_vars_added << key 170 | @params.add_param(key, value) 171 | end 172 | 173 | def add_params(keys_and_values) 174 | @param_vars_added += keys_and_values.keys 175 | @params.add_params(keys_and_values) 176 | end 177 | 178 | private 179 | 180 | def key_value_string(key, value, previous_keys = [], is_set = false) 181 | param = (previous_keys << key).join(UNDERSCORE) 182 | self.class.paramaterize_key!(param) 183 | 184 | if value.is_a?(Range) 185 | range_key_value_string(key, value, previous_keys, param) 186 | else 187 | value = value.first if array_value?(value, is_set) && value.size == 1 188 | 189 | param = add_param(param, value) 190 | 191 | "#{key} #{array_value?(value, is_set) ? 'IN' : '='} {#{param}}" 192 | end 193 | end 194 | 195 | def range_key_value_string(key, value, previous_keys, param) 196 | begin_param, end_param = add_params("#{param}_range_min" => value.begin, "#{param}_range_max" => value.end) 197 | "#{key} >= {#{begin_param}} AND #{previous_keys[-2]}.#{key} <#{'=' unless value.exclude_end?} {#{end_param}}" 198 | end 199 | 200 | def array_value?(value, is_set) 201 | value.is_a?(Array) && !is_set 202 | end 203 | 204 | def format_label(label_arg) 205 | return label_arg.map { |arg| format_label(arg) }.join if label_arg.is_a?(Array) 206 | 207 | label_arg = label_arg.to_s.strip 208 | if !label_arg.empty? && label_arg[0] != ':' 209 | label_arg = "`#{label_arg}`" unless label_arg[' '] 210 | label_arg = ":#{label_arg}" 211 | end 212 | label_arg 213 | end 214 | 215 | def attributes_string(attributes, prefix = '') 216 | return '' if not attributes 217 | 218 | attributes_string = attributes.map do |key, value| 219 | if value.to_s =~ /^{.+}$/ 220 | "#{key}: #{value}" 221 | else 222 | param_key = "#{prefix}#{key}".gsub(/:+/, '_') 223 | param_key = add_param(param_key, value) 224 | "#{key}: {#{param_key}}" 225 | end 226 | end.join(Clause::COMMA_SPACE) 227 | 228 | " {#{attributes_string}}" 229 | end 230 | end 231 | 232 | class StartClause < Clause 233 | KEYWORD = 'START' 234 | 235 | def from_symbol(value) 236 | from_string(value.to_s) 237 | end 238 | 239 | def from_key_and_value(key, value) 240 | case value 241 | when String, Symbol 242 | "#{key} = #{value}" 243 | else 244 | fail ArgError, value 245 | end 246 | end 247 | 248 | class << self 249 | def clause_strings(clauses) 250 | clauses.map!(&:value) 251 | end 252 | 253 | def clause_join 254 | Clause::COMMA_SPACE 255 | end 256 | end 257 | end 258 | 259 | class WhereClause < Clause 260 | KEYWORD = 'WHERE' 261 | 262 | PAREN_SURROUND_REGEX = /^\s*\(.+\)\s*$/ 263 | 264 | def from_key_and_value(key, value, previous_keys = []) 265 | case value 266 | when Hash then hash_key_value_string(key, value, previous_keys) 267 | when NilClass then "#{key} IS NULL" 268 | when Regexp then regexp_key_value_string(key, value, previous_keys) 269 | else 270 | key_value_string(key, value, previous_keys) 271 | end 272 | end 273 | 274 | class << self 275 | def clause_strings(clauses) 276 | clauses.flat_map do |clause| 277 | Array(clause.value).map do |v| 278 | (clause.options[:not] ? 'NOT' : '') + (v.to_s.match(PAREN_SURROUND_REGEX) ? v.to_s : "(#{v})") 279 | end 280 | end 281 | end 282 | 283 | def clause_join 284 | Clause::AND 285 | end 286 | end 287 | 288 | private 289 | 290 | def hash_key_value_string(key, value, previous_keys) 291 | value.map do |k, v| 292 | if k.to_sym == :neo_id 293 | v = Array(v).map { |item| (item.respond_to?(:neo_id) ? item.neo_id : item).to_i } 294 | key_value_string("ID(#{key})", v) 295 | else 296 | "#{key}.#{from_key_and_value(k, v, previous_keys + [key])}" 297 | end 298 | end.join(AND) 299 | end 300 | 301 | def regexp_key_value_string(key, value, previous_keys) 302 | pattern = (value.casefold? ? '(?i)' : '') + value.source 303 | 304 | param = [previous_keys + [key]].join(UNDERSCORE) 305 | self.class.paramaterize_key!(param) 306 | 307 | param = add_param(param, pattern) 308 | 309 | "#{key} =~ {#{param}}" 310 | end 311 | 312 | class << self 313 | ARG_HAS_QUESTION_MARK_REGEX = /(^|\(|\s)\?(\s|\)|$)/ 314 | 315 | def from_args(args, params, options = {}) 316 | query_string, params_arg = args 317 | 318 | if query_string.is_a?(String) && (query_string.match(ARG_HAS_QUESTION_MARK_REGEX) || params_arg.is_a?(Hash)) 319 | if params_arg.is_a?(Hash) 320 | params.add_params(params_arg) 321 | else 322 | param_var = params.add_params(question_mark_param: params_arg)[0] 323 | query_string = query_string.gsub(ARG_HAS_QUESTION_MARK_REGEX, "\\1{#{param_var}}\\2") 324 | end 325 | 326 | [from_arg(query_string, params, options)] 327 | else 328 | super 329 | end 330 | end 331 | end 332 | end 333 | 334 | class CallClause < Clause 335 | KEYWORD = 'CALL' 336 | 337 | def from_string(value) 338 | value 339 | end 340 | 341 | class << self 342 | def clause_strings(clauses) 343 | clauses.map!(&:value) 344 | end 345 | 346 | def clause_join 347 | " #{KEYWORD} " 348 | end 349 | end 350 | end 351 | 352 | class MatchClause < Clause 353 | KEYWORD = 'MATCH' 354 | 355 | def from_symbol(value) 356 | '(' + from_string(value.to_s) + ')' 357 | end 358 | 359 | def from_key_and_value(key, value) 360 | node_from_key_and_value(key, value) 361 | end 362 | 363 | class << self 364 | def clause_strings(clauses) 365 | clauses.map!(&:value) 366 | end 367 | 368 | def clause_join 369 | Clause::COMMA_SPACE 370 | end 371 | end 372 | end 373 | 374 | class OptionalMatchClause < MatchClause 375 | KEYWORD = 'OPTIONAL MATCH' 376 | end 377 | 378 | class WithClause < Clause 379 | KEYWORD = 'WITH' 380 | 381 | def from_symbol(value) 382 | from_string(value.to_s) 383 | end 384 | 385 | def from_key_and_value(key, value) 386 | "#{value} AS #{key}" 387 | end 388 | 389 | class << self 390 | def clause_strings(clauses) 391 | clauses.map!(&:value) 392 | end 393 | 394 | def clause_join 395 | Clause::COMMA_SPACE 396 | end 397 | end 398 | end 399 | 400 | class WithDistinctClause < WithClause 401 | KEYWORD = 'WITH DISTINCT' 402 | end 403 | 404 | class UsingClause < Clause 405 | KEYWORD = 'USING' 406 | 407 | class << self 408 | def clause_strings(clauses) 409 | clauses.map!(&:value) 410 | end 411 | 412 | def clause_join 413 | " #{keyword} " 414 | end 415 | end 416 | end 417 | 418 | class CreateClause < Clause 419 | KEYWORD = 'CREATE' 420 | 421 | def from_string(value) 422 | value 423 | end 424 | 425 | def from_symbol(value) 426 | "(:#{value})" 427 | end 428 | 429 | def from_hash(hash) 430 | if hash.values.any? { |value| value.is_a?(Hash) } 431 | hash.map do |key, value| 432 | from_key_and_value(key, value) 433 | end 434 | else 435 | "(#{attributes_string(hash)})" 436 | end 437 | end 438 | 439 | def from_key_and_value(key, value) 440 | node_from_key_and_value(key, value, prefer: :label) 441 | end 442 | 443 | class << self 444 | def clause_strings(clauses) 445 | clauses.map!(&:value) 446 | end 447 | 448 | def clause_join 449 | ', ' 450 | end 451 | 452 | def clause_color 453 | ANSI::GREEN 454 | end 455 | end 456 | end 457 | 458 | class CreateUniqueClause < CreateClause 459 | KEYWORD = 'CREATE UNIQUE' 460 | end 461 | 462 | class MergeClause < CreateClause 463 | KEYWORD = 'MERGE' 464 | 465 | class << self 466 | def clause_color 467 | ANSI::MAGENTA 468 | end 469 | 470 | def clause_join 471 | ' MERGE ' 472 | end 473 | end 474 | end 475 | 476 | class DeleteClause < Clause 477 | KEYWORD = 'DELETE' 478 | 479 | def from_symbol(value) 480 | from_string(value.to_s) 481 | end 482 | 483 | class << self 484 | def clause_strings(clauses) 485 | clauses.map!(&:value) 486 | end 487 | 488 | def clause_join 489 | Clause::COMMA_SPACE 490 | end 491 | 492 | def clause_color 493 | ANSI::RED 494 | end 495 | end 496 | end 497 | 498 | class DetachDeleteClause < DeleteClause 499 | KEYWORD = 'DETACH DELETE' 500 | end 501 | 502 | class OrderClause < Clause 503 | KEYWORD = 'ORDER BY' 504 | 505 | def from_symbol(value) 506 | from_string(value.to_s) 507 | end 508 | 509 | def from_key_and_value(key, value) 510 | case value 511 | when String, Symbol 512 | self.class.from_key_and_single_value(key, value) 513 | when Array 514 | value.map do |v| 515 | v.is_a?(Hash) ? from_key_and_value(key, v) : self.class.from_key_and_single_value(key, v) 516 | end 517 | when Hash 518 | value.map { |k, v| "#{self.class.from_key_and_single_value(key, k)} #{v.upcase}" } 519 | end 520 | end 521 | 522 | class << self 523 | def clause_strings(clauses) 524 | clauses.map!(&:value) 525 | end 526 | 527 | def clause_join 528 | Clause::COMMA_SPACE 529 | end 530 | end 531 | end 532 | 533 | class LimitClause < Clause 534 | KEYWORD = 'LIMIT' 535 | 536 | def from_string(value) 537 | param_var = "#{self.class.keyword_downcase}_#{value}" 538 | param_var = add_param(param_var, value.to_i) 539 | "{#{param_var}}" 540 | end 541 | 542 | def from_integer(value) 543 | from_string(value) 544 | end 545 | 546 | def from_nilclass(_value) 547 | '' 548 | end 549 | 550 | class << self 551 | def clause_strings(clauses) 552 | result_clause = clauses.last 553 | 554 | clauses[0..-2].map(&:param_vars_added).flatten.grep(/^limit_\d+$/).each do |var| 555 | result_clause.params.remove_param(var) 556 | end 557 | 558 | [result_clause.value] 559 | end 560 | end 561 | end 562 | 563 | class SkipClause < Clause 564 | KEYWORD = 'SKIP' 565 | 566 | def from_string(value) 567 | clause_id = "#{self.class.keyword_downcase}_#{value}" 568 | clause_id = add_param(clause_id, value.to_i) 569 | "{#{clause_id}}" 570 | end 571 | 572 | def from_integer(value) 573 | clause_id = "#{self.class.keyword_downcase}_#{value}" 574 | clause_id = add_param(clause_id, value) 575 | "{#{clause_id}}" 576 | end 577 | 578 | class << self 579 | def clause_strings(clauses) 580 | result_clause = clauses.last 581 | 582 | clauses[0..-2].map(&:param_vars_added).flatten.grep(/^skip_\d+$/).each do |var| 583 | result_clause.params.remove_param(var) 584 | end 585 | 586 | [result_clause.value] 587 | end 588 | end 589 | end 590 | 591 | class SetClause < Clause 592 | KEYWORD = 'SET' 593 | 594 | def from_key_and_value(key, value) 595 | case value 596 | when String, Symbol then "#{key}:`#{value}`" 597 | when Hash 598 | if @options[:set_props] 599 | param = add_param("#{key}_set_props", value) 600 | "#{key} = {#{param}}" 601 | else 602 | value.map { |k, v| key_value_string("#{key}.`#{k}`", v, ['setter'], true) } 603 | end 604 | when Array then value.map { |v| from_key_and_value(key, v) } 605 | when NilClass then [] 606 | else 607 | fail ArgError, value 608 | end 609 | end 610 | 611 | class << self 612 | def clause_strings(clauses) 613 | clauses.map!(&:value) 614 | end 615 | 616 | def clause_join 617 | Clause::COMMA_SPACE 618 | end 619 | 620 | def clause_color 621 | ANSI::YELLOW 622 | end 623 | end 624 | end 625 | 626 | class OnCreateSetClause < SetClause 627 | KEYWORD = 'ON CREATE SET' 628 | 629 | def initialize(*args) 630 | super 631 | @options[:set_props] = false 632 | end 633 | end 634 | 635 | class OnMatchSetClause < OnCreateSetClause 636 | KEYWORD = 'ON MATCH SET' 637 | end 638 | 639 | class RemoveClause < Clause 640 | KEYWORD = 'REMOVE' 641 | 642 | def from_key_and_value(key, value) 643 | case value 644 | when /^:/ 645 | "#{key}:`#{value[1..-1]}`" 646 | when String 647 | "#{key}.#{value}" 648 | when Symbol 649 | "#{key}:`#{value}`" 650 | when Array 651 | value.map do |v| 652 | from_key_and_value(key, v) 653 | end 654 | else 655 | fail ArgError, value 656 | end 657 | end 658 | 659 | class << self 660 | def clause_strings(clauses) 661 | clauses.map!(&:value) 662 | end 663 | 664 | def clause_join 665 | Clause::COMMA_SPACE 666 | end 667 | end 668 | end 669 | 670 | class UnwindClause < Clause 671 | KEYWORD = 'UNWIND' 672 | 673 | def from_key_and_value(key, value) 674 | case value 675 | when String, Symbol 676 | "#{value} AS #{key}" 677 | when Array 678 | "#{value.inspect} AS #{key}" 679 | else 680 | fail ArgError, value 681 | end 682 | end 683 | 684 | class << self 685 | def clause_strings(clauses) 686 | clauses.map!(&:value) 687 | end 688 | 689 | def clause_join 690 | ' UNWIND ' 691 | end 692 | end 693 | end 694 | 695 | class ReturnClause < Clause 696 | KEYWORD = 'RETURN' 697 | 698 | def from_symbol(value) 699 | from_string(value.to_s) 700 | end 701 | 702 | def from_key_and_value(key, value) 703 | case value 704 | when Array 705 | value.map do |v| 706 | from_key_and_value(key, v) 707 | end.join(Clause::COMMA_SPACE) 708 | when String, Symbol 709 | self.class.from_key_and_single_value(key, value) 710 | else 711 | fail ArgError, value 712 | end 713 | end 714 | 715 | class << self 716 | def clause_strings(clauses) 717 | clauses.map!(&:value) 718 | end 719 | 720 | def clause_join 721 | Clause::COMMA_SPACE 722 | end 723 | end 724 | end 725 | end 726 | end 727 | end 728 | -------------------------------------------------------------------------------- /lib/neo4j/core/query_find_in_batches.rb: -------------------------------------------------------------------------------- 1 | module Neo4j 2 | module Core 3 | module QueryFindInBatches 4 | def find_in_batches(node_var, prop_var, options = {}) 5 | validate_find_in_batches_options!(options) 6 | 7 | batch_size = options.delete(:batch_size) || 1000 8 | 9 | query = reorder(node_var => prop_var).limit(batch_size) 10 | 11 | records = query.to_a 12 | 13 | while records.any? 14 | records_size = records.size 15 | primary_key_offset = primary_key_offset(records.last, node_var, prop_var) 16 | 17 | yield records 18 | 19 | break if records_size < batch_size 20 | 21 | primary_key_var = Neo4j::Core::QueryClauses::Clause.from_key_and_single_value(node_var, prop_var) 22 | records = query.where("#{primary_key_var} > {primary_key_offset}") 23 | .params(primary_key_offset: primary_key_offset).to_a 24 | end 25 | end 26 | 27 | def find_each(*args, &block) 28 | find_in_batches(*args) { |batch| batch.each(&block) } 29 | end 30 | 31 | private 32 | 33 | def validate_find_in_batches_options!(options) 34 | invalid_keys = options.keys.map(&:to_sym) - [:batch_size] 35 | fail ArgumentError, "Invalid keys: #{invalid_keys.join(', ')}" if not invalid_keys.empty? 36 | end 37 | 38 | def primary_key_offset(last_record, node_var, prop_var) 39 | last_record.send(node_var).send(prop_var) 40 | rescue NoMethodError 41 | begin 42 | last_record.send(node_var).properties[prop_var.to_sym] 43 | rescue NoMethodError 44 | last_record.send("#{node_var}.#{prop_var}") # In case we're explicitly returning it 45 | end 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/neo4j/core/rake_tasks_deprecation.rake: -------------------------------------------------------------------------------- 1 | namespace :neo4j do 2 | %i[install start start_no_wait console shell config stop info indexes constraints 3 | restart reset_yes_i_am_sure change_password enable_auth disable_auth].each do |task_name| 4 | task task_name do |_, _args| 5 | puts <<-INFO 6 | The `neo4j-rake_tasks` gem is no longer a dependency of the `neo4j-core` gem. 7 | If you would like to use the rake tasks, you will need to explicitly include the `neo4j-rake_tasks` gem in your project. 8 | Please note that the `neo4j-rake_tasks` gem is only for development and test environments (NOT for production). 9 | Be sure to require the `neo4j-rake_tasks` gem AFTER the `neo4j-core` and `neo4j` gems. 10 | For more details see the Neo4j.rb documentation 11 | INFO 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/neo4j/core/relationship.rb: -------------------------------------------------------------------------------- 1 | require 'neo4j/core/wrappable' 2 | require 'active_support/core_ext/hash/keys' 3 | 4 | module Neo4j 5 | module Core 6 | class Relationship 7 | attr_reader :id, :type, :properties, :start_node_id, :end_node_id 8 | alias props properties 9 | alias neo_id id 10 | alias start_node_neo_id start_node_id 11 | alias end_node_neo_id end_node_id 12 | alias rel_type type 13 | 14 | include Wrappable 15 | 16 | def initialize(id, type, properties, start_node_id = nil, end_node_id = nil) 17 | @id = id 18 | @type = type.to_sym unless type.nil? 19 | @properties = properties.symbolize_keys 20 | @start_node_id = start_node_id 21 | @end_node_id = end_node_id 22 | end 23 | 24 | class << self 25 | def from_url(url, properties = {}) 26 | id = url.split('/')[-1].to_i 27 | type = nil # unknown 28 | properties = properties 29 | 30 | new(id, type, properties, nil, nil) 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/neo4j/core/version.rb: -------------------------------------------------------------------------------- 1 | module Neo4j 2 | module Core 3 | VERSION = '9.0.0' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/neo4j/core/wrappable.rb: -------------------------------------------------------------------------------- 1 | module Neo4j 2 | module Core 3 | module Wrappable 4 | def self.included(base) 5 | base.send :include, InstanceMethods 6 | base.extend ClassMethods 7 | end 8 | 9 | module InstanceMethods 10 | def wrap 11 | self.class.wrap(self) 12 | end 13 | end 14 | 15 | module ClassMethods 16 | def wrapper_callback(proc) 17 | fail 'Callback already specified!' if @wrapper_callback 18 | @wrapper_callback = proc 19 | end 20 | 21 | def clear_wrapper_callback 22 | @wrapper_callback = nil 23 | end 24 | 25 | def wrap(node) 26 | if @wrapper_callback 27 | @wrapper_callback.call(node) 28 | else 29 | node 30 | end 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/neo4j/transaction.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/module/delegation' 2 | require 'active_support/per_thread_registry' 3 | 4 | module Neo4j 5 | module Transaction 6 | extend self 7 | 8 | # Provides a simple API to manage transactions for each session in a thread-safe manner 9 | class TransactionsRegistry 10 | extend ActiveSupport::PerThreadRegistry 11 | 12 | attr_accessor :transactions_by_session_id 13 | end 14 | 15 | class Base 16 | attr_reader :session, :root 17 | 18 | def initialize(session, _options = {}) 19 | @session = session 20 | 21 | Transaction.stack_for(session) << self 22 | 23 | @root = Transaction.stack_for(session).first 24 | # Neo4j::Core::Label::SCHEMA_QUERY_SEMAPHORE.lock if root? 25 | 26 | # @parent = session_transaction_stack.last 27 | # session_transaction_stack << self 28 | end 29 | 30 | def inspect 31 | status_string = %i[id failed? active? commit_url].map do |method| 32 | "#{method}: #{send(method)}" if respond_to?(method) 33 | end.compact.join(', ') 34 | 35 | "<#{self.class} [#{status_string}]" 36 | end 37 | 38 | # Commits or marks this transaction for rollback, depending on whether #mark_failed has been previously invoked. 39 | def close 40 | tx_stack = Transaction.stack_for(@session) 41 | fail 'Tried closing when transaction stack is empty (maybe you closed too many?)' if tx_stack.empty? 42 | fail "Closed transaction which wasn't the most recent on the stack (maybe you forgot to close one?)" if tx_stack.pop != self 43 | 44 | @closed = true 45 | 46 | post_close! if tx_stack.empty? 47 | end 48 | 49 | def delete 50 | fail 'not implemented' 51 | end 52 | 53 | def commit 54 | fail 'not implemented' 55 | end 56 | 57 | def autoclosed! 58 | @autoclosed = true if transient_failures_autoclose? 59 | end 60 | 61 | def closed? 62 | !!@closed 63 | end 64 | 65 | # Marks this transaction as failed, 66 | # which means that it will unconditionally be rolled back 67 | # when #close is called. 68 | # Aliased for legacy purposes. 69 | def mark_failed 70 | root.mark_failed if root && root != self 71 | @failure = true 72 | end 73 | alias failure mark_failed 74 | 75 | # If it has been marked as failed. 76 | # Aliased for legacy purposes. 77 | def failed? 78 | !!@failure 79 | end 80 | alias failure? failed? 81 | 82 | def mark_expired 83 | @parent.mark_expired if @parent 84 | @expired = true 85 | end 86 | 87 | def expired? 88 | !!@expired 89 | end 90 | 91 | def root? 92 | @root == self 93 | end 94 | 95 | private 96 | 97 | def transient_failures_autoclose? 98 | Gem::Version.new(@session.version) >= Gem::Version.new('2.2.6') 99 | end 100 | 101 | def autoclosed? 102 | !!@autoclosed 103 | end 104 | 105 | def active? 106 | !closed? 107 | end 108 | 109 | def post_close! 110 | return if autoclosed? 111 | if failed? 112 | delete 113 | else 114 | commit 115 | end 116 | end 117 | end 118 | 119 | # @return [Neo4j::Transaction::Instance] 120 | def new(session = Session.current!) 121 | session.transaction 122 | end 123 | 124 | # Runs the given block in a new transaction. 125 | # @param [Boolean] run_in_tx if true a new transaction will not be created, instead if will simply yield to the given block 126 | # @@yield [Neo4j::Transaction::Instance] 127 | def run(*args) 128 | session, run_in_tx = session_and_run_in_tx_from_args(args) 129 | 130 | fail ArgumentError, 'Expected a block to run in Transaction.run' unless block_given? 131 | 132 | return yield(nil) unless run_in_tx 133 | 134 | tx = Neo4j::Transaction.new(session) 135 | yield tx 136 | rescue Exception => e # rubocop:disable Lint/RescueException 137 | # print_exception_cause(e) 138 | 139 | tx.mark_failed unless tx.nil? 140 | raise e 141 | ensure 142 | tx.close unless tx.nil? 143 | end 144 | 145 | # To support old syntax of providing run_in_tx first 146 | # But session first is ideal 147 | def session_and_run_in_tx_from_args(args) 148 | fail ArgumentError, 'Too few arguments' if args.empty? 149 | fail ArgumentError, 'Too many arguments' if args.size > 2 150 | 151 | if args.size == 1 152 | fail ArgumentError, 'Session must be specified' if !args[0].is_a?(Neo4j::Core::CypherSession) 153 | 154 | [args[0], true] 155 | else 156 | [true, false].include?(args[0]) ? args.reverse : args.dup 157 | end 158 | end 159 | 160 | def current_for(session) 161 | stack_for(session).first 162 | end 163 | 164 | def stack_for(session) 165 | TransactionsRegistry.transactions_by_session_id ||= {} 166 | TransactionsRegistry.transactions_by_session_id[session.object_id] ||= [] 167 | end 168 | 169 | private 170 | 171 | def print_exception_cause(exception) 172 | return if !exception.respond_to?(:cause) || !exception.cause.respond_to?(:print_stack_trace) 173 | 174 | Core.logger.info "Java Exception in a transaction, cause: #{exception.cause}" 175 | exception.cause.print_stack_trace 176 | end 177 | end 178 | end 179 | -------------------------------------------------------------------------------- /neo4j-core.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('lib', __dir__) 2 | $LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib) 3 | 4 | require 'neo4j/core/version' 5 | 6 | Gem::Specification.new do |s| 7 | s.name = 'neo4j-core' 8 | s.version = Neo4j::Core::VERSION 9 | s.required_ruby_version = '>= 2.1.0' 10 | 11 | s.authors = 'Andreas Ronge, Chris Grigg, Brian Underwood' 12 | s.email = 'andreas.ronge@gmail.com, chris@subvertallmedia.com, brian@brian-underwood.codes' 13 | s.homepage = 'https://github.com/neo4jrb/neo4j-core' 14 | s.summary = 'A basic library to work with the graph database Neo4j.' 15 | s.license = 'MIT' 16 | 17 | s.description = <<-DESCRIPTION 18 | Neo4j-core provides classes and methods to work with the graph database Neo4j. 19 | DESCRIPTION 20 | 21 | s.require_path = 'lib' 22 | s.files = Dir.glob('{bin,lib,config}/**/*') + %w[README.md Gemfile neo4j-core.gemspec] 23 | s.has_rdoc = true 24 | s.extra_rdoc_files = %w[README.md] 25 | s.rdoc_options = ['--quiet', '--title', 'Neo4j::Core', '--line-numbers', '--main', 'README.rdoc', '--inline-source'] 26 | s.metadata = { 27 | 'homepage_uri' => 'http://neo4jrb.io/', 28 | 'changelog_uri' => 'https://github.com/neo4jrb/neo4j-core/blob/master/CHANGELOG.md', 29 | 'source_code_uri' => 'https://github.com/neo4jrb/neo4j-core/', 30 | 'bug_tracker_uri' => 'https://github.com/neo4jrb/neo4j-core/issues' 31 | } 32 | 33 | s.add_dependency('activesupport', '>= 4.0') 34 | s.add_dependency('faraday', '>= 0.9.0') 35 | s.add_dependency('faraday_middleware', '>= 0.10.0') 36 | s.add_dependency('faraday_middleware-multi_json') 37 | s.add_dependency('httpclient') 38 | s.add_dependency('json') 39 | s.add_dependency('multi_json') 40 | s.add_dependency('net_tcp_client', '>= 2.0.1') 41 | s.add_dependency('typhoeus', '>= 1.1.2') 42 | 43 | s.add_development_dependency('dryspec') 44 | s.add_development_dependency('neo4j-rake_tasks', '>= 0.3.0') 45 | s.add_development_dependency('pry') 46 | s.add_development_dependency('simplecov') 47 | s.add_development_dependency('yard') 48 | 49 | if RUBY_PLATFORM =~ /java/ 50 | s.add_development_dependency('neo4j-community', '>= 2.1.1') 51 | s.add_development_dependency('neo4j-ruby-driver', '!= 1.7.2.beta.1') 52 | s.add_development_dependency 'ruby-debug' 53 | end 54 | 55 | s.add_development_dependency('guard') 56 | s.add_development_dependency('guard-rubocop') 57 | s.add_development_dependency('rubocop', '~> 0.56.0') 58 | end 59 | -------------------------------------------------------------------------------- /spec/fixtures/adirectory/.githold: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4jrb/neo4j-core/4b649fca33a1b1dba8705e6f999764ad9fb4d76e/spec/fixtures/adirectory/.githold -------------------------------------------------------------------------------- /spec/fixtures/neo4j.properties: -------------------------------------------------------------------------------- 1 | # Enable this to be able to upgrade a store from 1.4 -> 1.5 or 1.4 -> 1.6 2 | allow_store_upgrade=true 3 | 4 | # Enable this to specify a parser other than the default one. 1.5, 1.6, 1.7 are available 5 | #cypher_parser_version=1.6 6 | 7 | # Keep logical logs, helps debugging but uses more disk space, enabled for 8 | # legacy reasons To limit space needed to store historical logs use values such 9 | # as: "7 days" or "100M size" instead of "true" 10 | keep_logical_logs=10 days 11 | 12 | # Autoindexing 13 | 14 | # Enable auto-indexing for nodes, default is false 15 | node_auto_indexing=false 16 | 17 | # The node property keys to be auto-indexed, if enabled 18 | #node_keys_indexable= 19 | 20 | # Enable auto-indexing for relationships, default is false 21 | #relationship_auto_indexing=true 22 | 23 | # The relationship property keys to be auto-indexed, if enabled 24 | #relationship_keys_indexable=name,age 25 | 26 | # Configure caching type 27 | # cache_type=gcr 28 | 29 | # HA Setup 30 | # ha.discovery.enabled=false 31 | # ha.pull_interval=0 32 | # ha.tx_push_factor=2 33 | # ha.server=10.100.41.57:6002 34 | # ha.cluster_server=10.100.41.57:5001 35 | # ha.server_id=10553 36 | # ha.initial_hosts=10.100.41.57:5001,10.100.40.221:5001,10.100.41.155:5001 37 | # ha.heartbeat_interval=2s 38 | # ha.heartbeat_timeout=20s -------------------------------------------------------------------------------- /spec/fixtures/notadirectory: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4jrb/neo4j-core/4b649fca33a1b1dba8705e6f999764ad9fb4d76e/spec/fixtures/notadirectory -------------------------------------------------------------------------------- /spec/helpers.rb: -------------------------------------------------------------------------------- 1 | module Helpers 2 | def server_username 3 | ENV['NEO4J_USERNAME'] || 'neo4j' 4 | end 5 | 6 | def server_password 7 | ENV['NEO4J_PASSWORD'] || 'neo4jrb rules, ok?' 8 | end 9 | 10 | def basic_auth_hash 11 | if server_uri.user && server_uri.password 12 | { 13 | username: server_uri.user, 14 | password: server_uri.password 15 | } 16 | else 17 | { 18 | username: server_username, 19 | password: server_password 20 | } 21 | end 22 | end 23 | 24 | def server_url 25 | ENV['NEO4J_URL'] || 'http://localhost:7474' 26 | end 27 | 28 | def server_uri 29 | @server_uri ||= URI(server_url) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/lib/yard_rspec.rb: -------------------------------------------------------------------------------- 1 | 2 | class RSpecDescribeHandler < YARD::Handlers::Ruby::Base 3 | handles method_call(:describe) 4 | 5 | def process 6 | param = statement.parameters.first.jump(:string_content).source 7 | 8 | parse_block(statement.last.last, owner: obj_for_param(param)) 9 | rescue YARD::Handlers::NamespaceMissingError 10 | end 11 | 12 | private 13 | 14 | def obj_for_param(param) 15 | (owner || {}).tap do |obj| 16 | if param == 'Neo4j::Core::Query' 17 | obj = {spec: ''} 18 | elsif obj[:spec] 19 | case param[0] 20 | when '#' then obj[:spec] = "Neo4j::Core::Query#{param}" 21 | when '.' then obj[:ruby] = param 22 | end 23 | end 24 | end 25 | end 26 | end 27 | 28 | class RSpecItGeneratesHandler < YARD::Handlers::Ruby::Base 29 | handles method_call(:it_generates) 30 | 31 | def process 32 | return if owner.nil? 33 | return unless owner[:spec] 34 | 35 | path, ruby = owner.values_at(:spec, :ruby) 36 | 37 | object = P(path) 38 | return if object.is_a?(Proxy) 39 | 40 | cypher = statement.parameters.first.jump(:string_content).source 41 | 42 | (object[:examples] ||= []) << {path: path, ruby: ruby, cypher: cypher} 43 | end 44 | end 45 | 46 | YARD::Templates::Engine.register_template_path File.dirname(__FILE__) + '/yard_rspec/templates' 47 | -------------------------------------------------------------------------------- /spec/lib/yard_rspec/templates/default/method_details/html/specs.erb: -------------------------------------------------------------------------------- 1 | <% if object[:examples] %> 2 |
3 |

Examples:

4 |
    5 | 6 | 7 | 8 | 9 | 10 | <% for example in object[:examples] %> 11 | 12 | 13 | 14 | 15 | <% end %> 16 |
    Ruby CodeGenerated Cypher
    query<%= example[:ruby].gsub('\\"', '"') %>  
    <%= example[:cypher].gsub('\\"', '"') %>
    17 |
18 |
19 | <% end %> 20 | -------------------------------------------------------------------------------- /spec/lib/yard_rspec/templates/default/method_details/setup.rb: -------------------------------------------------------------------------------- 1 | def init 2 | super 3 | sections.last.place(:specs).before(:source) 4 | end 5 | -------------------------------------------------------------------------------- /spec/lib/yard_rspec/templates/default/method_details/text/specs.erb: -------------------------------------------------------------------------------- 1 | <% if object[:specifications] %> 2 | 3 | Specifications: 4 | --------------- 5 | 6 | <% for spec in object[:specifications] %> 7 | <%= indent wrap("- " + spec[:name]) %> 8 | <% end %> 9 | <% end %> 10 | -------------------------------------------------------------------------------- /spec/neo4j/core/cypher_session/adaptors/bolt_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'neo4j/core/cypher_session/adaptors/bolt' 3 | require './spec/neo4j/core/shared_examples/adaptor' 4 | 5 | describe Neo4j::Core::CypherSession::Adaptors::Bolt, bolt: true do 6 | let(:extra_options) { {} } 7 | let(:url) { test_bolt_url } 8 | let(:adaptor) { test_bolt_adaptor(url, extra_options) } 9 | 10 | subject { adaptor } 11 | 12 | describe '#initialize' do 13 | before do 14 | allow_any_instance_of(Neo4j::Core::CypherSession::Adaptors::Bolt).to receive(:open_socket) 15 | end 16 | 17 | let_context(url: 'url') { subject_should_raise ArgumentError, /Invalid URL/ } 18 | let_context(url: :symbol) { subject_should_raise ArgumentError, /Invalid URL/ } 19 | let_context(url: 123) { subject_should_raise ArgumentError, /Invalid URL/ } 20 | 21 | let_context(url: 'http://localhost:7687') { subject_should_raise ArgumentError, /Invalid URL/ } 22 | let_context(url: 'http://foo:bar@localhost:7687') { subject_should_raise ArgumentError, /Invalid URL/ } 23 | let_context(url: 'https://localhost:7687') { subject_should_raise ArgumentError, /Invalid URL/ } 24 | let_context(url: 'https://foo:bar@localhost:7687') { subject_should_raise ArgumentError, /Invalid URL/ } 25 | 26 | let_context(url: 'bolt://foo@localhost:') { subject_should_not_raise } 27 | let_context(url: 'bolt://:foo@localhost:7687') { subject_should_not_raise } 28 | 29 | let_context(url: 'bolt://localhost:7687') { subject_should_not_raise } 30 | let_context(url: 'bolt://foo:bar@localhost:7687') { subject_should_not_raise } 31 | end 32 | 33 | describe '#default_subscribe' do 34 | it 'makes the right subscription' do 35 | expect(adaptor).to receive(:subscribe_to_request) 36 | adaptor.default_subscribe 37 | end 38 | end 39 | 40 | describe 'message in multiple chunks' do 41 | before do 42 | # This is standard response for INIT message, split into two chunks. 43 | # Normally it has form of ["\x00\x03", "\xB1p\xA0", "\x00\x00"] 44 | responses = [ 45 | "\x00\x02", 46 | "\xB1p", 47 | "\x00\x01", 48 | "\xA0", 49 | "\x00\x00" 50 | ] 51 | 52 | allow(adaptor).to receive(:recvmsg) { responses.shift } 53 | end 54 | 55 | it 'handles chunked responses' do 56 | adaptor.send(:init) 57 | expect(adaptor.send(:flush_messages)[0].args.first).to eq({}) 58 | end 59 | end 60 | 61 | context 'connected adaptor' do 62 | before { adaptor.connect } 63 | 64 | it_behaves_like 'Neo4j::Core::CypherSession::Adaptor' 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/neo4j/core/cypher_session/adaptors/driver_spec.rb: -------------------------------------------------------------------------------- 1 | if RUBY_PLATFORM =~ /java/ 2 | require 'spec_helper' 3 | require 'neo4j/core/cypher_session/adaptors/driver' 4 | require './spec/neo4j/core/shared_examples/adaptor' 5 | require 'neo4j/driver' 6 | 7 | def port 8 | URI(ENV['NEO4J_BOLT_URL']).port 9 | end 10 | 11 | describe Neo4j::Core::CypherSession::Adaptors::Driver, bolt: true do 12 | let(:adaptor_class) { Neo4j::Core::CypherSession::Adaptors::Driver } 13 | let(:url) { ENV['NEO4J_BOLT_URL'] } 14 | 15 | # let(:adaptor) { adaptor_class.new(url, logger_level: Logger::DEBUG) } 16 | let(:adaptor) { adaptor_class.new(url) } 17 | 18 | after(:context) do 19 | Neo4j::Core::CypherSession::Adaptors::DriverRegistry.instance.close_all 20 | end 21 | 22 | subject { adaptor } 23 | 24 | describe '#initialize' do 25 | let_context(url: 'url') { subject_should_raise ArgumentError, /Invalid URL/ } 26 | let_context(url: :symbol) { subject_should_raise ArgumentError, /Invalid URL/ } 27 | let_context(url: 123) { subject_should_raise ArgumentError, /Invalid URL/ } 28 | 29 | let_context(url: "http://localhost:#{port}") { subject_should_raise ArgumentError, /Invalid URL/ } 30 | let_context(url: "http://foo:bar@localhost:#{port}") { subject_should_raise ArgumentError, /Invalid URL/ } 31 | let_context(url: "https://localhost:#{port}") { subject_should_raise ArgumentError, /Invalid URL/ } 32 | let_context(url: "https://foo:bar@localhost:#{port}") { subject_should_raise ArgumentError, /Invalid URL/ } 33 | 34 | let_context(url: 'bolt://foo@localhost:') { port == '7687' ? subject_should_not_raise : subject_should_raise } 35 | let_context(url: "bolt://:foo@localhost:#{port}") { subject_should_not_raise } 36 | 37 | let_context(url: "bolt://localhost:#{port}") { subject_should_not_raise } 38 | let_context(url: "bolt://foo:bar@localhost:#{port}") { subject_should_not_raise } 39 | end 40 | 41 | context 'connected adaptor' do 42 | before { adaptor.connect } 43 | 44 | it_behaves_like 'Neo4j::Core::CypherSession::Adaptor' 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/neo4j/core/cypher_session/adaptors/embedded_spec.rb: -------------------------------------------------------------------------------- 1 | if RUBY_PLATFORM == 'java' 2 | require 'spec_helper' 3 | require 'neo4j/core/cypher_session/adaptors/embedded' 4 | require './spec/neo4j/core/shared_examples/adaptor' 5 | require 'tmpdir' 6 | require 'neo4j-community' 7 | 8 | describe Neo4j::Core::CypherSession::Adaptors::Embedded do 9 | let(:adaptor_class) { Neo4j::Core::CypherSession::Adaptors::Embedded } 10 | 11 | describe '#initialize' do 12 | it 'validates path' do 13 | expect { adaptor_class.new('./spec/fixtures/notadirectory') }.to raise_error ArgumentError, /Invalid path:/ 14 | expect { adaptor_class.new('./spec/fixtures/adirectory') }.not_to raise_error 15 | end 16 | end 17 | 18 | before(:all) do 19 | path = Dir.mktmpdir('neo4jrb_embedded_adaptor_spec') 20 | @adaptor = Neo4j::Core::CypherSession::Adaptors::Embedded.new(path) 21 | @adaptor.connect 22 | end 23 | 24 | let(:adaptor) { @adaptor } 25 | 26 | describe '#default_subscribe' do 27 | it 'makes the right subscription' do 28 | expect(adaptor).to receive(:subscribe_to_transaction) 29 | adaptor.default_subscribe 30 | end 31 | end 32 | 33 | it_behaves_like 'Neo4j::Core::CypherSession::Adaptor' 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/neo4j/core/cypher_session/adaptors/has_uri_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'neo4j/core/cypher_session/adaptors/has_uri' 3 | 4 | describe Neo4j::Core::CypherSession::Adaptors::HasUri do 5 | let(:uri_validation) {} 6 | let(:default_url) {} 7 | let!(:adaptor) do 8 | scoped_default_url = default_url 9 | scoped_uri_validation = uri_validation 10 | Class.new do 11 | include Neo4j::Core::CypherSession::Adaptors::HasUri 12 | default_url(scoped_default_url) if scoped_default_url 13 | validate_uri(&scoped_uri_validation) 14 | 15 | def initialize(url) 16 | self.url = url 17 | end 18 | end 19 | end 20 | 21 | subject { adaptor.new(url) } 22 | 23 | let_context default_url: 'foo://bar:baz@biz:1234' do 24 | let_context url: nil do 25 | its(:scheme) { should eq('foo') } 26 | its(:user) { should eq('bar') } 27 | its(:password) { should eq('baz') } 28 | its(:host) { should eq('biz') } 29 | its(:port) { should eq(1234) } 30 | its(:url_without_password) { should eq('foo://bar:...@biz:1234') } 31 | end 32 | 33 | let_context url: 'http://localhost:4321' do 34 | its(:scheme) { should eq('http') } 35 | its(:user) { should eq('bar') } 36 | its(:password) { should eq('baz') } 37 | its(:host) { should eq('localhost') } 38 | its(:port) { should eq(4321) } 39 | its(:url_without_password) { should eq('http://bar:...@localhost:4321') } 40 | end 41 | end 42 | 43 | let_context default_url: nil do 44 | let_context url: nil do 45 | subject_should_raise ArgumentError, /No URL or default URL/ 46 | end 47 | 48 | let_context url: 'http://localhost:7474' do 49 | its(:scheme) { should eq('http') } 50 | its(:user) { should be_nil } 51 | its(:password) { should be_nil } 52 | its(:host) { should eq('localhost') } 53 | its(:port) { should eq(7474) } 54 | its(:url_without_password) { should eq('http://localhost:7474') } 55 | end 56 | end 57 | 58 | let_context uri_validation: ->(uri) { uri.port == 3344 } do 59 | let_context url: 'http://localhost:7474' do 60 | subject_should_raise ArgumentError, /Invalid URL/ 61 | end 62 | let_context url: 'http://localhost:3344' do 63 | subject_should_not_raise 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/neo4j/core/cypher_session/adaptors/http_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'neo4j/core/cypher_session/adaptors/http' 3 | require './spec/neo4j/core/shared_examples/adaptor' 4 | 5 | describe Neo4j::Core::CypherSession::Adaptors::HTTP do 6 | before(:all) { setup_http_request_subscription } 7 | let(:adaptor_class) { Neo4j::Core::CypherSession::Adaptors::HTTP } 8 | 9 | let(:url) { ENV['NEO4J_URL'] } 10 | let(:adaptor) { adaptor_class.new(url) } 11 | subject { adaptor } 12 | 13 | describe '#initialize' do 14 | it 'validates URLs' do 15 | expect { adaptor_class.new('url').connect }.to raise_error ArgumentError, /Invalid URL:/ 16 | expect { adaptor_class.new('https://foo@localhost:7474').connect }.not_to raise_error 17 | 18 | expect { adaptor_class.new('http://localhost:7474').connect }.not_to raise_error 19 | expect { adaptor_class.new('https://localhost:7474').connect }.not_to raise_error 20 | expect { adaptor_class.new('https://localhost:7474/').connect }.not_to raise_error 21 | expect { adaptor_class.new('https://foo:bar@localhost:7474').connect }.not_to raise_error 22 | expect { adaptor_class.new('bolt://localhost:7474').connect }.to raise_error ArgumentError, /Invalid URL/ 23 | expect { adaptor_class.new('foo://localhost:7474').connect }.to raise_error ArgumentError, /Invalid URL/ 24 | end 25 | end 26 | 27 | describe '#supports_metadata?' do 28 | it 'supports in version 3.4.0' do 29 | expect(adaptor).to receive(:version).and_return('3.4.0') 30 | expect(adaptor.supports_metadata?).to be true 31 | end 32 | 33 | it 'does not supports in version 2.0.0' do 34 | expect(adaptor).to receive(:version).and_return('2.0.0') 35 | expect(adaptor.supports_metadata?).to be false 36 | end 37 | end 38 | 39 | let(:session_double) { double('session', adaptor: subject) } 40 | 41 | before do 42 | adaptor.connect 43 | adaptor.query(session_double, 'MATCH (n) OPTIONAL MATCH (n)-[r]-() DELETE n, r') 44 | end 45 | 46 | describe 'transactions' do 47 | it 'lets you execute a query in a transaction' do 48 | expect_http_requests(2) do 49 | tx = subject.transaction(session_double) 50 | tx.query('MATCH (n) RETURN n LIMIT 1') 51 | tx.close 52 | end 53 | end 54 | end 55 | 56 | context 'when connected' do 57 | before { subject.connect } 58 | 59 | describe 'transactions' do 60 | it 'lets you execute a query in a transaction' do 61 | expect_http_requests(2) do 62 | tx = subject.transaction(session_double) 63 | tx.query('MATCH (n) RETURN n LIMIT 1') 64 | tx.close 65 | end 66 | 67 | expect_http_requests(2) do 68 | subject.transaction(session_double) do |tx| 69 | tx.query('MATCH (n) RETURN n LIMIT 1') 70 | end 71 | end 72 | end 73 | end 74 | 75 | describe 'unwrapping' do 76 | it 'is not fooled by returned Maps with key expected for nodes/rels/paths' do 77 | result = subject.query(session_double, 'RETURN {labels: 1} AS r') 78 | expect(result.to_a[0].r).to eq(labels: 1) 79 | 80 | result = subject.query(session_double, 'RETURN {type: 1} AS r') 81 | expect(result.to_a[0].r).to eq(type: 1) 82 | 83 | result = subject.query(session_double, 'RETURN {start: 1} AS r') 84 | expect(result.to_a[0].r).to eq(start: 1) 85 | end 86 | end 87 | end 88 | 89 | it_behaves_like 'Neo4j::Core::CypherSession::Adaptor' 90 | end 91 | -------------------------------------------------------------------------------- /spec/neo4j/core/cypher_session/cypher_error_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'neo4j/core/cypher_session/adaptors' 3 | 4 | module Neo4j 5 | module Core 6 | describe CypherSession::CypherError do 7 | let(:code) { 'SomeError' } 8 | let(:message) { 'some fancy error' } 9 | let(:stack_trace) { "class1:1\nclass2:2\nclass3:3" } 10 | subject { described_class.new_from(code, message, stack_trace) } 11 | 12 | its(:class) { is_expected.to eq(described_class) } 13 | its(:inspect) { is_expected.to include(subject.message) } 14 | its(:message) { is_expected.to include(message, code, stack_trace) } 15 | 16 | its(:original_message) { is_expected.to eq(message) } 17 | its(:code) { is_expected.to eq(code) } 18 | its(:stack_trace) { is_expected.to eq(stack_trace) } 19 | 20 | let_context code: 'ConstraintValidationFailed' do 21 | it { is_expected.to be_a(CypherSession::SchemaErrors::ConstraintValidationFailedError) } 22 | end 23 | 24 | let_context code: 'ConstraintViolation' do 25 | it { is_expected.to be_a(CypherSession::SchemaErrors::ConstraintValidationFailedError) } 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/neo4j/core/cypher_session_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'neo4j/core/cypher_session' 3 | 4 | describe Neo4j::Core::CypherSession do 5 | describe '#initialize' do 6 | it 'fails with invalid adaptor' do 7 | expect do 8 | Neo4j::Core::CypherSession.new(Object.new) 9 | end.to raise_error ArgumentError, /^Invalid adaptor: / 10 | end 11 | 12 | it 'takes an Adaptors::Base object' do 13 | expect do 14 | http_adaptor = Neo4j::Core::CypherSession::Adaptors::HTTP.new(ENV['NEO4J_URL']) 15 | Neo4j::Core::CypherSession.new(http_adaptor) 16 | end.not_to raise_error 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/neo4j/core/query_parameters_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Neo4j::Core::Query::Parameters do 4 | let(:parameters) { Neo4j::Core::Query::Parameters.new } 5 | 6 | it 'lets you add params' do 7 | expect(parameters.add_param(:foo, 1)).to eq(:foo) 8 | 9 | expect(parameters.to_hash).to eq(foo: 1) 10 | end 11 | 12 | it 'lets you add a second param' do 13 | expect(parameters.add_param(:foo, 1)).to eq(:foo) 14 | expect(parameters.add_param(:bar, 'baz')).to eq(:bar) 15 | 16 | expect(parameters.to_hash).to eq(foo: 1, bar: 'baz') 17 | end 18 | 19 | it 'does not let the same parameter be used twice' do 20 | expect(parameters.add_param(:foo, 1)).to eq(:foo) 21 | expect(parameters.add_param(:foo, 2)).to eq(:foo2) 22 | 23 | expect(parameters.to_hash).to eq(foo: 1, foo2: 2) 24 | end 25 | 26 | it 'allows you to add multiple params at the same time' do 27 | expect(parameters.add_params(foo: 1)).to eq([:foo]) 28 | expect(parameters.add_params(foo: 2, bar: 'baz')).to eq(%i[foo2 bar]) 29 | 30 | expect(parameters.to_hash).to eq(foo: 1, foo2: 2, bar: 'baz') 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/neo4j/core/shared_examples/adaptor.rb: -------------------------------------------------------------------------------- 1 | # Requires that an `adaptor` let variable exist with the connected adaptor 2 | RSpec.shared_examples 'Neo4j::Core::CypherSession::Adaptor' do 3 | let(:real_session) do 4 | expect(adaptor).to receive(:connect) 5 | Neo4j::Core::CypherSession.new(adaptor) 6 | end 7 | let(:session_double) { double('session', adaptor: adaptor) } 8 | 9 | before { adaptor.query(session_double, 'MATCH (n) OPTIONAL MATCH (n)-[r]-() DELETE n, r') } 10 | 11 | subject { adaptor } 12 | 13 | describe '#query' do 14 | it 'Can make a query' do 15 | adaptor.query(session_double, 'MERGE path=(n)-[rel:r]->(o) RETURN n, rel, o, path LIMIT 1') 16 | end 17 | 18 | it 'can make a query with a large payload' do 19 | adaptor.query(session_double, 'CREATE (n:Test) SET n = {props} RETURN n', props: {text: 'a' * 10_000}) 20 | end 21 | end 22 | 23 | describe '#queries' do 24 | it 'allows for multiple queries' do 25 | result = adaptor.queries(session_double) do 26 | append 'CREATE (n:Label1) RETURN n' 27 | append 'CREATE (n:Label2) RETURN n' 28 | end 29 | 30 | expect(result[0].to_a[0].n).to be_a(Neo4j::Core::Node) 31 | expect(result[1].to_a[0].n).to be_a(Neo4j::Core::Node) 32 | if adaptor.supports_metadata? 33 | expect(result[0].to_a[0].n.labels).to eq([:Label1]) 34 | expect(result[1].to_a[0].n.labels).to eq([:Label2]) 35 | else 36 | expect(result[0].to_a[0].n.labels).to eq(nil) 37 | expect(result[1].to_a[0].n.labels).to eq(nil) 38 | end 39 | end 40 | 41 | it 'allows for building with Query API' do 42 | result = adaptor.queries(session_double) do 43 | append query.create(n: {Label1: {}}).return(:n) 44 | end 45 | 46 | expect(result[0].to_a[0].n).to be_a(Neo4j::Core::Node) 47 | expect(result[0].to_a[0].n.labels).to eq(adaptor.supports_metadata? ? [:Label1] : nil) 48 | end 49 | end 50 | 51 | describe 'transactions' do 52 | def create_object_by_id(id, tx) 53 | tx.query('CREATE (t:Temporary {id: {id}})', id: id) 54 | end 55 | 56 | def get_object_by_id(id, adaptor) 57 | first = adaptor.query(session_double, 'MATCH (t:Temporary {id: {id}}) RETURN t', id: id).first 58 | first && first.t 59 | end 60 | 61 | it 'logs one query per query_set in transaction' do 62 | expect_queries(1) do 63 | tx = adaptor.transaction(session_double) 64 | create_object_by_id(1, tx) 65 | tx.close 66 | end 67 | expect(get_object_by_id(1, adaptor)).to be_a(Neo4j::Core::Node) 68 | 69 | expect_queries(1) do 70 | adaptor.transaction(session_double) do |tx| 71 | create_object_by_id(2, tx) 72 | end 73 | end 74 | expect(get_object_by_id(2, adaptor)).to be_a(Neo4j::Core::Node) 75 | end 76 | 77 | it 'allows for rollback' do 78 | expect_queries(1) do 79 | tx = adaptor.transaction(session_double) 80 | create_object_by_id(3, tx) 81 | tx.mark_failed 82 | tx.close 83 | end 84 | expect(get_object_by_id(3, adaptor)).to be_nil 85 | 86 | expect_queries(1) do 87 | adaptor.transaction(session_double) do |tx| 88 | create_object_by_id(4, tx) 89 | tx.mark_failed 90 | end 91 | end 92 | expect(get_object_by_id(4, adaptor)).to be_nil 93 | 94 | expect_queries(1) do 95 | expect do 96 | adaptor.transaction(session_double) do |tx| 97 | create_object_by_id(5, tx) 98 | fail 'Failing transaction with error' 99 | end 100 | end.to raise_error 'Failing transaction with error' 101 | end 102 | expect(get_object_by_id(5, adaptor)).to be_nil 103 | 104 | # Nested transaction, error from inside inner transaction handled outside of inner transaction 105 | expect_queries(1) do 106 | adaptor.transaction(session_double) do |_tx| 107 | expect do 108 | adaptor.transaction(session_double) do |tx| 109 | create_object_by_id(6, tx) 110 | fail 'Failing transaction with error' 111 | end 112 | end.to raise_error 'Failing transaction with error' 113 | end 114 | end 115 | expect(get_object_by_id(6, adaptor)).to be_nil 116 | 117 | # Nested transaction, error from inside inner transaction handled outside of inner transaction 118 | expect_queries(2) do 119 | adaptor.transaction(session_double) do |tx| 120 | create_object_by_id(7, tx) 121 | expect do 122 | adaptor.transaction(session_double) do |tx| 123 | create_object_by_id(8, tx) 124 | fail 'Failing transaction with error' 125 | end 126 | end.to raise_error 'Failing transaction with error' 127 | end 128 | end 129 | expect(get_object_by_id(7, adaptor)).to be_nil 130 | expect(get_object_by_id(8, adaptor)).to be_nil 131 | 132 | # Nested transaction, error from inside inner transaction handled outside of outer transaction 133 | expect_queries(1) do 134 | expect do 135 | adaptor.transaction(session_double) do |_tx| 136 | adaptor.transaction(session_double) do |tx| 137 | create_object_by_id(9, tx) 138 | fail 'Failing transaction with error' 139 | end 140 | end 141 | end.to raise_error 'Failing transaction with error' 142 | end 143 | expect(get_object_by_id(9, adaptor)).to be_nil 144 | end 145 | 146 | describe 'after_commit hook' do 147 | it 'gets called when the root transaction is closed' do 148 | data = false 149 | tx1 = adaptor.transaction(session_double) 150 | tx2 = adaptor.transaction(session_double) 151 | tx3 = adaptor.transaction(session_double) 152 | tx3.root.after_commit { data = true } 153 | tx3.close 154 | tx2.close 155 | expect { tx1.close }.to change { data }.to(true) 156 | expect(data).to be_truthy 157 | end 158 | 159 | it 'is ignored when the root transaction fails' do 160 | data = false 161 | tx1 = adaptor.transaction(session_double) 162 | tx2 = adaptor.transaction(session_double) 163 | tx3 = adaptor.transaction(session_double) 164 | tx3.root.after_commit { data = true } 165 | tx1.mark_failed 166 | tx3.close 167 | tx2.close 168 | expect { tx1.close }.not_to change { data } 169 | expect(data).to be_falsey 170 | end 171 | 172 | it 'is ignored when a child transaction fails' do 173 | data = false 174 | tx1 = adaptor.transaction(session_double) 175 | tx2 = adaptor.transaction(session_double) 176 | tx3 = adaptor.transaction(session_double) 177 | tx3.root.after_commit { data = true } 178 | tx3.mark_failed 179 | tx3.close 180 | tx2.close 181 | expect { tx1.close }.not_to change { data } 182 | expect(data).to be_falsey 183 | end 184 | end 185 | # it 'does not allow transactions in the wrong order' do 186 | # expect { adaptor.end_transaction }.to raise_error(RuntimeError, /Cannot close transaction without starting one/) 187 | end 188 | 189 | describe 'results' do 190 | it 'handles array results' do 191 | result = adaptor.query(session_double, "CREATE (a {b: 'c'}) RETURN [a] AS arr") 192 | 193 | expect(result.hashes).to be_a(Array) 194 | expect(result.hashes.size).to be(1) 195 | expect(result.hashes[0][:arr]).to be_a(Array) 196 | expect(result.hashes[0][:arr][0]).to be_a(Neo4j::Core::Node) 197 | expect(result.hashes[0][:arr][0].properties).to eq(b: 'c') 198 | end 199 | 200 | it 'handles map results' do 201 | result = adaptor.query(session_double, "CREATE (a {b: 'c'}) RETURN {foo: a} AS map") 202 | 203 | expect(result.hashes).to be_a(Array) 204 | expect(result.hashes.size).to be(1) 205 | expect(result.hashes[0][:map]).to be_a(Hash) 206 | expect(result.hashes[0][:map][:foo]).to be_a(Neo4j::Core::Node) 207 | expect(result.hashes[0][:map][:foo].properties).to eq(b: 'c') 208 | end 209 | 210 | it 'handles map results with arrays' do 211 | result = adaptor.query(session_double, "CREATE (a {b: 'c'}) RETURN {foo: [a]} AS map") 212 | 213 | expect(result.hashes).to be_a(Array) 214 | expect(result.hashes.size).to be(1) 215 | expect(result.hashes[0][:map]).to be_a(Hash) 216 | expect(result.hashes[0][:map][:foo]).to be_a(Array) 217 | expect(result.hashes[0][:map][:foo][0]).to be_a(Neo4j::Core::Node) 218 | expect(result.hashes[0][:map][:foo][0].properties).to eq(b: 'c') 219 | end 220 | 221 | it 'symbolizes keys for Neo4j objects' do 222 | result = adaptor.query(session_double, 'RETURN {a: 1} AS obj') 223 | 224 | expect(result.hashes).to eq([{obj: {a: 1}}]) 225 | 226 | structs = result.structs 227 | expect(structs).to be_a(Array) 228 | expect(structs.size).to be(1) 229 | expect(structs[0].obj).to eq(a: 1) 230 | end 231 | 232 | describe 'parameter input and output' do 233 | subject { adaptor.query(session_double, 'WITH {param} AS param RETURN param', param: param).first.param } 234 | 235 | [ 236 | # Integers 237 | rand(10_000_000_000) * -1, 238 | rand(99_999_999) * -1, 239 | -1, 0, 1, 240 | rand(99_999_999), 241 | rand(10_000_000_000), 242 | # Floats 243 | rand * 10_000_000_000 * -1, 244 | rand * 99_999_999 * -1, 245 | -18.6288, 246 | -1.0, 0.0, 1.0, 247 | 18.6288, 248 | rand * 99_999_999, 249 | rand * 10_000_000_000, 250 | # Strings 251 | '', 252 | 'foo', 253 | # 'bar' * 10_000, # (16326 - 16329) 16,384 = 2^14 254 | 'bar' * 5442, 255 | # Arrays 256 | [], 257 | [1, 3, 5], 258 | %w[foo bar], 259 | # Hashes / Maps 260 | {}, 261 | {a: 1, b: 2}, 262 | {a: 'foo', b: 'bar'} 263 | ].each do |value| 264 | let_context(param: value) { it { should eq(value) } } 265 | end 266 | 267 | # Asymetric values 268 | # Symbols 269 | # Commented out because Embedded doesn't deal with this well... 270 | # let_context(param: :foo) { it { should eq('foo') } } 271 | # Sets 272 | # Commented out because, while Bolt supports this, the default `to_json` 273 | # makes Sets into strings (like "#"), not arrays when serializing 274 | # let_context(param: Set.new([1, 2, 3])) { it { should eq([1, 2, 3]) } } 275 | # let_context(param: Set.new([1, 2, 3])) { it { should eq([1, 2, 3]) } } 276 | end 277 | 278 | describe 'wrapping' do 279 | let(:query) do 280 | "MERGE path=(n:Foo {a: 1})-[r:foo {b: 2}]->(b:Foo) 281 | RETURN #{return_clause} AS result" 282 | end 283 | subject { adaptor.query(session_double, query, {}, wrap_level: wrap_level).to_a[0].result } 284 | 285 | # `wrap_level: nil` should resolve to `wrap_level: :core_entity` 286 | [nil, :core_entity].each do |type| 287 | let_context wrap_level: type do 288 | let_context return_clause: 'n' do 289 | it { should be_a(Neo4j::Core::Node) } 290 | its(:properties) { should eq(a: 1) } 291 | end 292 | 293 | let_context return_clause: 'r' do 294 | it { should be_a(Neo4j::Core::Relationship) } 295 | its(:properties) { should eq(b: 2) } 296 | end 297 | 298 | let_context return_clause: 'path' do 299 | it { should be_a(Neo4j::Core::Path) } 300 | end 301 | 302 | let_context(return_clause: '{c: 3}') { it { should eq(c: 3) } } 303 | let_context(return_clause: '[1,3,5]') { it { should eq([1, 3, 5]) } } 304 | let_context(return_clause: '["foo", "bar"]') { it { should eq(%w[foo bar]) } } 305 | end 306 | end 307 | 308 | # Possible to return better data structure for :none? 309 | let_context wrap_level: :none do 310 | let_context(return_clause: 'n') { it { should eq(a: 1) } } 311 | let_context(return_clause: 'r') { it { should eq(b: 2) } } 312 | let_context(return_clause: 'path') { it { should eq([{a: 1}, {b: 2}, {}]) } } 313 | 314 | let_context(return_clause: '{c: 3}') { it { should eq(c: 3) } } 315 | let_context(return_clause: '[1,3,5]') { it { should eq([1, 3, 5]) } } 316 | let_context(return_clause: '["foo", "bar"]') { it { should eq(%w[foo bar]) } } 317 | end 318 | 319 | let_context wrap_level: :proc do 320 | before do 321 | # Normally I don't think you wouldn't wrap nodes/relationships/paths 322 | # with the same class. It's just expedient to do so in this spec 323 | stub_const 'WrapperClass', Struct.new(:wrapped_object) 324 | 325 | %i[Node Relationship Path].each do |core_class| 326 | Neo4j::Core.const_get(core_class).wrapper_callback(WrapperClass.method(:new)) 327 | end 328 | end 329 | 330 | after do 331 | %i[Node Relationship Path].each do |core_class| 332 | Neo4j::Core.const_get(core_class).clear_wrapper_callback 333 | end 334 | end 335 | 336 | let_context return_clause: 'n' do 337 | it { should be_a(WrapperClass) } 338 | its(:wrapped_object) { should be_a(Neo4j::Core::Node) } 339 | its(:'wrapped_object.properties') { should eq(a: 1) } 340 | end 341 | 342 | let_context return_clause: 'r' do 343 | it { should be_a(WrapperClass) } 344 | its(:wrapped_object) { should be_a(Neo4j::Core::Relationship) } 345 | its(:'wrapped_object.properties') { should eq(b: 2) } 346 | end 347 | 348 | let_context return_clause: 'path' do 349 | it { should be_a(WrapperClass) } 350 | its(:wrapped_object) { should be_a(Neo4j::Core::Path) } 351 | end 352 | 353 | let_context(return_clause: '{c: 3}') { it { should eq(c: 3) } } 354 | let_context(return_clause: '[1,3,5]') { it { should eq([1, 3, 5]) } } 355 | let_context(return_clause: '["foo", "bar"]') { it { should eq(%w[foo bar]) } } 356 | end 357 | end 358 | end 359 | 360 | describe 'cypher errors' do 361 | describe 'unique constraint error' do 362 | before { delete_schema(real_session) } 363 | before { create_constraint(real_session, :Album, :uuid, type: :unique) } 364 | 365 | it 'raises an error' do 366 | adaptor.query(real_session, "CREATE (:Album {uuid: 'dup'})").to_a 367 | expect do 368 | adaptor.query(real_session, "CREATE (:Album {uuid: 'dup'})").to_a 369 | end.to raise_error(::Neo4j::Core::CypherSession::SchemaErrors::ConstraintValidationFailedError) 370 | end 371 | end 372 | 373 | describe 'Invalid input error' do 374 | it 'raises an error' do 375 | expect do 376 | adaptor.query(real_session, "CRATE (:Album {uuid: 'dup'})").to_a 377 | end.to raise_error(::Neo4j::Core::CypherSession::CypherError, /Invalid input 'A'/) 378 | end 379 | end 380 | 381 | describe 'Clause ordering error' do 382 | it 'raises an error' do 383 | expect do 384 | adaptor.query(real_session, "RETURN a CREATE (a:Album {uuid: 'dup'})").to_a 385 | end.to raise_error(::Neo4j::Core::CypherSession::CypherError, /RETURN can only be used at the end of the query/) 386 | end 387 | end 388 | end 389 | 390 | describe 'schema inspection' do 391 | before { delete_schema(real_session) } 392 | before do 393 | create_constraint(real_session, :Album, :al_id, type: :unique) 394 | create_constraint(real_session, :Album, :name, type: :unique) 395 | create_constraint(real_session, :Song, :so_id, type: :unique) 396 | 397 | create_index(real_session, :Band, :ba_id) 398 | create_index(real_session, :Band, :fisk) 399 | create_index(real_session, :Person, :name) 400 | end 401 | 402 | describe 'constraints' do 403 | let(:label) {} 404 | subject { adaptor.constraints(real_session) } 405 | 406 | it do 407 | should match_array([ 408 | {type: :uniqueness, label: :Album, properties: [:al_id]}, 409 | {type: :uniqueness, label: :Album, properties: [:name]}, 410 | {type: :uniqueness, label: :Song, properties: [:so_id]} 411 | ]) 412 | end 413 | end 414 | 415 | describe 'indexes' do 416 | let(:label) {} 417 | subject { adaptor.indexes(real_session) } 418 | 419 | it do 420 | should match_array([ 421 | a_hash_including(label: :Band, properties: [:ba_id]), 422 | a_hash_including(label: :Band, properties: [:fisk]), 423 | a_hash_including(label: :Person, properties: [:name]), 424 | a_hash_including(label: :Album, properties: [:al_id]), 425 | a_hash_including(label: :Album, properties: [:name]), 426 | a_hash_including(label: :Song, properties: [:so_id]) 427 | ]) 428 | end 429 | end 430 | end 431 | end 432 | -------------------------------------------------------------------------------- /spec/neo4j/core/transaction_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'neo4j/transaction' 3 | 4 | describe Neo4j::Transaction do 5 | describe '.session_and_run_in_tx_from_args' do 6 | let(:session_double) do 7 | class_double(Neo4j::Core::CypherSession).tap do |double| 8 | allow(double).to receive(:is_a?).with(Class) { true } 9 | end 10 | end 11 | 12 | subject do 13 | final_args = args.map { |a| a == :session_double ? session_double : a } 14 | Neo4j::Transaction.session_and_run_in_tx_from_args(final_args) 15 | end 16 | 17 | let_context(args: []) { subject_should_raise ArgumentError, 'Too few arguments' } 18 | 19 | let_context(args: [true]) { subject_should_raise ArgumentError, 'Session must be specified' } 20 | let_context(args: [false]) { subject_should_raise ArgumentError, 'Session must be specified' } 21 | let_context(args: ['something else']) { subject_should_raise ArgumentError, 'Session must be specified' } 22 | 23 | let_context(args: [:session_double]) { it { should eq([session_double, true]) } } 24 | 25 | # This method doesn't care what you pass as the non-boolean value 26 | # so using a symbol here 27 | let_context(args: [:session_double]) { it { should eq([session_double, true]) } } 28 | 29 | let_context(args: [false, :session_double]) { it { should eq([session_double, false]) } } 30 | let_context(args: [true, :session_double]) { it { should eq([session_double, true]) } } 31 | 32 | let_context(args: [:session_double, false]) { it { should eq([session_double, false]) } } 33 | let_context(args: [:session_double, true]) { it { should eq([session_double, true]) } } 34 | 35 | let_context(args: [:session_double, true, :foo]) { subject_should_raise ArgumentError, /Too many arguments/ } 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/neo4j/core/wrappable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'neo4j/core/wrappable' 3 | 4 | describe Neo4j::Core::Wrappable do 5 | before do 6 | stub_const 'WrapperClass', (Class.new do 7 | attr_reader :wrapped_object 8 | 9 | def initialize(obj) 10 | @wrapped_object = obj 11 | end 12 | end) 13 | 14 | stub_const 'WrappableClass', (Class.new do 15 | include Neo4j::Core::Wrappable 16 | end) 17 | end 18 | 19 | describe '.wrapper_callback' do 20 | it 'does not allow for two callbacks' do 21 | WrappableClass.wrapper_callback(->(obj) { WrapperClass.new(obj) }) 22 | 23 | expect do 24 | WrappableClass.wrapper_callback(-> {}) 25 | end.to raise_error(/Callback already specified!/) 26 | end 27 | 28 | it 'returns the wrappable object if no callback is specified' do 29 | obj = WrappableClass.new 30 | expect(obj.wrap).to eq(obj) 31 | end 32 | 33 | it 'allow users to specify a callback which will create a wrapper object' do 34 | WrappableClass.wrapper_callback(->(obj) { WrapperClass.new(obj) }) 35 | 36 | obj = WrappableClass.new 37 | wrapper_obj = obj.wrap 38 | expect(wrapper_obj.wrapped_object).to eq(obj) 39 | end 40 | end 41 | 42 | # Should pass on method calls? 43 | end 44 | -------------------------------------------------------------------------------- /spec/neo4j_spec_helpers.rb: -------------------------------------------------------------------------------- 1 | module Neo4jSpecHelpers 2 | class << self 3 | attr_accessor :expect_queries_count 4 | end 5 | 6 | self.expect_queries_count = 0 7 | 8 | Neo4j::Core::CypherSession::Adaptors::Base.subscribe_to_query do |_message| 9 | self.expect_queries_count += 1 10 | end 11 | 12 | def expect_queries(count) 13 | start_count = Neo4jSpecHelpers.expect_queries_count 14 | yield 15 | expect(Neo4jSpecHelpers.expect_queries_count - start_count).to eq(count) 16 | end 17 | 18 | def delete_schema(session = nil) 19 | Neo4j::Core::Label.drop_uniqueness_constraints_for(session || current_session) 20 | Neo4j::Core::Label.drop_indexes_for(session || current_session) 21 | end 22 | 23 | def create_constraint(session, label_name, property, options = {}) 24 | label_object = Neo4j::Core::Label.new(label_name, session) 25 | label_object.create_constraint(property, options) 26 | end 27 | 28 | def create_index(session, label_name, property, options = {}) 29 | label_object = Neo4j::Core::Label.new(label_name, session) 30 | label_object.create_index(property, options) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # To run coverage via travis 2 | require 'simplecov' 3 | require 'coveralls' 4 | 5 | SimpleCov.formatter = Coveralls::SimpleCov::Formatter 6 | SimpleCov.start do 7 | add_filter 'spec' 8 | end 9 | 10 | # To run it manually via Rake 11 | if ENV['COVERAGE'] 12 | require 'simplecov' 13 | SimpleCov.formatter = SimpleCov::Formatter::HTMLFormatter 14 | SimpleCov.start 15 | end 16 | 17 | require 'dotenv' 18 | Dotenv.load 19 | 20 | require 'rubygems' 21 | require 'bundler/setup' 22 | require 'rspec' 23 | require 'fileutils' 24 | require 'tmpdir' 25 | require 'logger' 26 | require 'rspec/its' 27 | require 'neo4j/core' 28 | require 'neo4j/core/query' 29 | require 'ostruct' 30 | require 'openssl' 31 | require 'neo4j_ruby_driver' if RUBY_PLATFORM =~ /java/ 32 | 33 | if RUBY_PLATFORM == 'java' 34 | # for some reason this is not impl. in JRuby 35 | class OpenStruct 36 | def [](key) 37 | send(key) 38 | end 39 | end 40 | 41 | end 42 | 43 | Dir["#{File.dirname(__FILE__)}/shared_examples/**/*.rb"].each { |f| require f } 44 | 45 | EMBEDDED_DB_PATH = File.join(Dir.tmpdir, 'neo4j-core-java') 46 | 47 | require "#{File.dirname(__FILE__)}/helpers" 48 | 49 | require 'neo4j/core/cypher_session' 50 | require 'neo4j/core/cypher_session/adaptors/http' 51 | require 'neo4j/core/cypher_session/adaptors/bolt' 52 | require 'neo4j/core/cypher_session/adaptors/embedded' 53 | require 'neo4j_spec_helpers' 54 | 55 | module Neo4jSpecHelpers 56 | # def log_queries! 57 | # Neo4j::Server::CypherSession.log_with(&method(:puts)) 58 | # Neo4j::Core::CypherSession::Adaptors::Base.subscribe_to_query(&method(:puts)) 59 | # Neo4j::Core::CypherSession::Adaptors::HTTP.subscribe_to_request(&method(:puts)) 60 | # Neo4j::Core::CypherSession::Adaptors::Bolt.subscribe_to_request(&method(:puts)) 61 | # Neo4j::Core::CypherSession::Adaptors::Embedded.subscribe_to_transaction(&method(:puts)) 62 | # end 63 | 64 | def current_transaction 65 | Neo4j::Transaction.current_for(Neo4j::Session.current) 66 | end 67 | 68 | # rubocop:disable Style/GlobalVars 69 | def expect_http_requests(count) 70 | start_count = $expect_http_request_count 71 | yield 72 | expect($expect_http_request_count - start_count).to eq(count) 73 | end 74 | 75 | def setup_http_request_subscription 76 | $expect_http_request_count = 0 77 | 78 | Neo4j::Core::CypherSession::Adaptors::HTTP.subscribe_to_request do |_message| 79 | $expect_http_request_count += 1 80 | end 81 | end 82 | # rubocop:enable Style/GlobalVars 83 | 84 | def test_bolt_url 85 | ENV['NEO4J_BOLT_URL'] 86 | end 87 | 88 | def test_bolt_adaptor(url, extra_options = {}) 89 | options = {} 90 | options[:logger_level] = Logger::DEBUG if ENV['DEBUG'] 91 | 92 | cert_store = OpenSSL::X509::Store.new 93 | cert_path = ENV.fetch('TLS_CERTIFICATE_PATH', './db/neo4j/development/certificates/neo4j.cert') 94 | cert_store.add_file(cert_path) 95 | options[:ssl] = {cert_store: cert_store} 96 | # options[:ssl] = false 97 | 98 | Neo4j::Core::CypherSession::Adaptors::Bolt.new(url, options.merge(extra_options)) 99 | end 100 | 101 | def test_http_url 102 | ENV['NEO4J_URL'] 103 | end 104 | 105 | def test_http_adaptor(url, extra_options = {}) 106 | options = {} 107 | options[:logger_level] = Logger::DEBUG if ENV['DEBUG'] 108 | 109 | Neo4j::Core::CypherSession::Adaptors::HTTP.new(url, options.merge(extra_options)) 110 | end 111 | 112 | def delete_db(session) 113 | session.query('MATCH (n) OPTIONAL MATCH (n)-[r]-() DELETE n, r') 114 | end 115 | end 116 | 117 | require 'dryspec/helpers' 118 | 119 | FileUtils.rm_rf(EMBEDDED_DB_PATH) 120 | 121 | RSpec.configure do |config| 122 | config.include Neo4jSpecHelpers 123 | config.extend DRYSpec::Helpers 124 | # config.include Helpers 125 | 126 | config.exclusion_filter = { 127 | bolt: lambda do 128 | ENV['NEO4J_VERSION'].to_s.match(/^(community|enterprise)-2\./) 129 | end 130 | } 131 | end 132 | --------------------------------------------------------------------------------