Repository: florinpatrascu/bolt_sips Branch: master Commit: b21901a46ed1 Files: 153 Total size: 507.4 KB Directory structure: gitextract_6ctcdb7g/ ├── .credo.exs ├── .dialyzer_ignore.exs ├── .formatter.exs ├── .gitattributes ├── .gitignore ├── .iex.exs ├── .markdownlint.json ├── .prettierrc.yaml ├── .tool-versions ├── .travis.yml ├── CHANGELOG.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── benchees/ │ └── conn_to_local_bench.exs ├── config/ │ ├── config.exs │ ├── dev.exs │ └── test.exs ├── docker-compose.yml ├── docs/ │ ├── examples/ │ │ └── readme.md │ ├── features/ │ │ ├── about-encoding.md │ │ ├── about-transactions.md │ │ ├── configuration.md │ │ ├── multi-tenancy.md │ │ ├── role-based-connections.md │ │ ├── routing.md │ │ ├── using-cypher.md │ │ ├── using-temporal-and-spatial-types.md │ │ └── using-with-phoenix.md │ └── getting-started.md ├── lib/ │ ├── bolt_sips/ │ │ ├── application.ex │ │ ├── enumerable_response.ex │ │ ├── error.ex │ │ ├── exception.ex │ │ ├── internals/ │ │ │ ├── bolt_protocol.ex │ │ │ ├── bolt_protocol_helper.ex │ │ │ ├── bolt_protocol_v1.ex │ │ │ ├── bolt_protocol_v2.ex │ │ │ ├── bolt_protocol_v3.ex │ │ │ ├── bolt_version_helper.ex │ │ │ ├── error.ex │ │ │ ├── logger.ex │ │ │ ├── pack_stream/ │ │ │ │ ├── decoder.ex │ │ │ │ ├── decoder_impl_v1.ex │ │ │ │ ├── decoder_impl_v2.ex │ │ │ │ ├── decoder_utils.ex │ │ │ │ ├── decoder_v1.ex │ │ │ │ ├── decoder_v2.ex │ │ │ │ ├── decoder_v3.ex │ │ │ │ ├── encoder.ex │ │ │ │ ├── encoder_helper.ex │ │ │ │ ├── encoder_v1.ex │ │ │ │ ├── encoder_v2.ex │ │ │ │ ├── encoder_v3.ex │ │ │ │ ├── error.ex │ │ │ │ ├── markers.ex │ │ │ │ ├── message/ │ │ │ │ │ ├── decoder.ex │ │ │ │ │ ├── encoder.ex │ │ │ │ │ ├── encoder_v1.ex │ │ │ │ │ ├── encoder_v2.ex │ │ │ │ │ ├── encoder_v3.ex │ │ │ │ │ └── signatures.ex │ │ │ │ ├── message.ex │ │ │ │ ├── utils.ex │ │ │ │ ├── v1.ex │ │ │ │ └── v2.ex │ │ │ └── pack_stream.ex │ │ ├── metadata.ex │ │ ├── protocol.ex │ │ ├── query.ex │ │ ├── query_statement.ex │ │ ├── response.ex │ │ ├── response_encoder/ │ │ │ ├── json/ │ │ │ │ ├── jason.ex │ │ │ │ └── poison.ex │ │ │ └── json.ex │ │ ├── response_encoder.ex │ │ ├── router.ex │ │ ├── routing/ │ │ │ ├── connection_supervisor.ex │ │ │ ├── load_balancer.ex │ │ │ └── routing_table.ex │ │ ├── socket.ex │ │ ├── types.ex │ │ ├── types_helper.ex │ │ └── utils.ex │ ├── bolt_sips.ex │ └── mix/ │ └── tasks/ │ └── cypher.ex ├── mix.exs ├── requirements.txt └── test/ ├── bolt_sips/ │ ├── internals/ │ │ ├── bolt_protocol_all_bolt_version_test.exs │ │ ├── bolt_protocol_bolt_v1_test.exs │ │ ├── bolt_protocol_bolt_v2_test.exs │ │ ├── bolt_protocol_bolt_v3_test.exs │ │ ├── bolt_protocol_v1_test.exs │ │ ├── bolt_protocol_v3_test.exs │ │ ├── bolt_version_helper_test.exs │ │ ├── logger_test.exs │ │ └── pack_stream/ │ │ ├── decoder_test.exs │ │ ├── decoder_v1_test.exs │ │ ├── decoder_v2_test.exs │ │ ├── encoder_helper_test.exs │ │ ├── encoder_test.exs │ │ ├── encoder_v1_test.exs │ │ ├── encoder_v2_test.exs │ │ ├── message/ │ │ │ ├── decoder_test.exs │ │ │ ├── encoder_test.exs │ │ │ ├── encoder_v1_test.exs │ │ │ └── encoder_v3_test.exs │ │ └── message_test.exs │ ├── metadata_test.exs │ ├── performance_test.exs │ ├── protocol_test.exs │ ├── response_encoder/ │ │ ├── json_implementations_test.exs │ │ └── json_test.exs │ ├── response_encoder_test.exs │ ├── types_helpers_test.exs │ └── types_test.exs ├── boltkit_test.exs ├── config_test.exs ├── errors_test.exs ├── invalid_param_type_test.exs ├── one_test.exs ├── query_bolt_v2_test.exs ├── query_test.exs ├── response_test.exs ├── router_test.exs ├── routing/ │ ├── connections_test.exs │ ├── crud_test.exs │ ├── routing_table_parser_test.exs │ ├── routing_test.exs │ └── transaction_test.exs ├── scripts/ │ ├── count.bolt │ ├── create_a.script │ ├── forbidden_on_read_only_database.script │ ├── get_routing_table.script │ ├── get_routing_table_with_context.script │ ├── non_router.script │ ├── return_1.script │ ├── return_1_in_tx_twice.script │ ├── return_1_twice.script │ ├── return_x.bolt │ ├── router.script │ ├── router_no_readers.script │ └── router_no_writers.script ├── support/ │ ├── boltkit_case.ex │ ├── conn_case.ex │ ├── conn_routing_case.ex │ ├── database.ex │ ├── fixture.ex │ └── internal_case.ex ├── test_helper.exs ├── test_large_param_set.exs ├── test_support.exs └── transaction_test.exs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .credo.exs ================================================ # This file contains the configuration for Credo. # # If you find anything wrong or unclear in this file, please report an # issue on GitHub: https://github.com/rrrene/credo/issues %{ # # You can have as many configs as you like in the `configs:` field. configs: [ %{ # # Run any config using `mix credo -C `. If no config name is given # "default" is used. name: "default", # # these are the files included in the analysis files: %{ # # you can give explicit globs or simply directories # in the latter case `**/*.{ex,exs}` will be used included: ["lib/", "src/", "web/", "apps/"], excluded: [] }, # # The `checks:` field contains all the checks that are run. You can # customize the parameters of any given check by adding a second element # to the tuple. # # There are two ways of deactivating a check: # 1. deleting the check from this list # 2. putting `false` as second element (to quickly "comment it out"): # # {Credo.Check.Consistency.ExceptionNames, false} # checks: [ {Credo.Check.Consistency.ExceptionNames}, {Credo.Check.Consistency.LineEndings, false}, {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, {Credo.Check.Consistency.ParameterPatternMatching}, {Credo.Check.Consistency.SpaceAroundOperators}, {Credo.Check.Consistency.SpaceInParentheses, false}, {Credo.Check.Consistency.TabsOrSpaces}, # For some checks, like AliasUsage, you can only customize the priority # Priority values are: `low, normal, high, higher` {Credo.Check.Design.AliasUsage, priority: :low}, # For others you can set parameters {Credo.Check.Design.DuplicatedCode, mass_threshold: 16, nodes_threshold: 2}, {Credo.Check.Design.TagTODO, false}, {Credo.Check.Design.TagFIXME, false}, {Credo.Check.Readability.FunctionNames}, {Credo.Check.Readability.LargeNumbers}, {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 80}, {Credo.Check.Readability.ModuleAttributeNames}, {Credo.Check.Readability.ModuleDoc}, {Credo.Check.Readability.ModuleNames}, {Credo.Check.Readability.ParenthesesInCondition}, {Credo.Check.Readability.PredicateFunctionNames}, {Credo.Check.Readability.PreferImplicitTry}, {Credo.Check.Readability.RedundantBlankLines}, {Credo.Check.Readability.Specs, false}, {Credo.Check.Readability.StringSigils}, {Credo.Check.Readability.TrailingBlankLine}, {Credo.Check.Readability.TrailingWhiteSpace}, {Credo.Check.Readability.VariableNames}, {Credo.Check.Refactor.DoubleBooleanNegation}, {Credo.Check.Refactor.ABCSize, max_size: 50}, {Credo.Check.Refactor.CaseTrivialMatches, false}, {Credo.Check.Refactor.CondStatements}, {Credo.Check.Refactor.CyclomaticComplexity}, {Credo.Check.Refactor.FunctionArity}, {Credo.Check.Refactor.MatchInCondition}, {Credo.Check.Refactor.NegatedConditionsInUnless}, {Credo.Check.Refactor.NegatedConditionsWithElse}, {Credo.Check.Refactor.Nesting}, {Credo.Check.Refactor.PipeChainStart}, {Credo.Check.Refactor.CyclomaticComplexity}, {Credo.Check.Refactor.NegatedConditionsInUnless}, {Credo.Check.Refactor.NegatedConditionsWithElse}, {Credo.Check.Refactor.Nesting}, {Credo.Check.Refactor.UnlessWithElse}, {Credo.Check.Refactor.VariableRebinding}, {Credo.Check.Warning.BoolOperationOnSameValues}, {Credo.Check.Warning.IExPry}, {Credo.Check.Warning.IoInspect}, {Credo.Check.Warning.OperationOnSameValues}, {Credo.Check.Warning.OperationWithConstantResult}, {Credo.Check.Warning.UnusedEnumOperation}, {Credo.Check.Warning.UnusedFileOperation}, {Credo.Check.Warning.UnusedKeywordOperation}, {Credo.Check.Warning.UnusedListOperation}, {Credo.Check.Warning.UnusedPathOperation}, {Credo.Check.Warning.UnusedRegexOperation}, {Credo.Check.Warning.UnusedStringOperation}, {Credo.Check.Warning.UnusedTupleOperation} ] } ] } ================================================ FILE: .dialyzer_ignore.exs ================================================ [ ~r/__impl__.*does\ not\ exist\./ ] ================================================ FILE: .formatter.exs ================================================ # Used by "mix format" and to export configuration. export_locals_without_parens = [ plug: 1, plug: 2, adapter: 1, adapter: 2 ] [ inputs: [ "lib/**/*.{ex,exs}", "test/**/*.{ex,exs}", "mix.exs" ], locals_without_parens: export_locals_without_parens, export: [locals_without_parens: export_locals_without_parens] ] ================================================ FILE: .gitattributes ================================================ * text=auto ================================================ FILE: .gitignore ================================================ ### Elixir template # The directory Mix will write compiled artifacts to. /_build # If you run "mix test --cover", coverage assets end up here. /cover # The directory Mix downloads your dependencies sources to. /deps # Where 3rd-party dependencies like ExDoc output generated docs. /doc # If the VM crashes, it generates a dump, let's ignore it too. erl_crash.dump # Also ignore archive artifacts (built via "mix archive.build"). *.ez /bench/snapshots /bench/graphs /cover ### Vim template [._]*.s[a-w][a-z] [._]s[a-w][a-z] *.un~ Session.vim .netrwhist *~ ### SublimeText template # cache files for sublime text *.tmlanguage.cache *.tmPreferences.cache *.stTheme.cache # workspace files are user-specific *.sublime-workspace *.sublime-project # project files should be checked into the repository, unless a significant # proportion of contributors will probably not be using SublimeText # *.sublime-project # sftp configuration file sftp-config.json ### OSX template .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### Tags template # Ignore tags created by etags, ctags, gtags (GNU global) and cscope TAGS !TAGS/ tags !tags/ gtags.files GTAGS GRTAGS GPATH cscope.files cscope.out cscope.in.out cscope.po.out /tmp ### JetBrains template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio ## Directory-based project format: .idea/ ## File-based project format: *.ipr *.iws ## Plugin-specific files: # IntelliJ /out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml .tags .tags_sorted_by_file ### Erlang template .eunit *.plt ebin rel/example_project .concrete/DEV_MODE logs/ .vscode/ .elixir_ls/ # py related *~ *.py[co] __pycache__ .pytest_cache *.egg-info ================================================ FILE: .iex.exs ================================================ try do Code.eval_file(".iex.exs", "~") rescue Code.LoadError -> :rescued end alias Bolt.Sips.{Utils, Protocol, Router, ConnectionSupervisor, Response} alias Bolt.Sips Application.put_env(:tzdata, :autoupdate, :disabled) # default port considered to be: 7687 test_config = [ # url: 'localhost', url: "bolt://localhost", basic_auth: [username: "neo4j", password: "test"], pool_size: 5, max_overflow: 1, # retry the request, in case of error - in the example below the retry will # linearly increase the delay from 150ms following a Fibonacci pattern, # cap the delay at 15 seconds (the value defined by the default `:timeout` # parameter) and giving up after 3 attempts retry_linear_backoff: [delay: 150, factor: 2, tries: 3], read: [pool_size: 5, pool_overflow: 0], write: [pool_size: 1, pool_overflow: 0] ] Mix.shell().info([ :green, """ Optional, if needed for development (Sips is the alias for Bolt.Sips): {:ok, _neo} = Sips.start_link(url: "bolt://neo4j:test@localhost") conn = Sips.conn() Examples: Sips.query!(conn, "UNWIND range(1, 10) AS n RETURN n") Sips.query!(conn, "RETURN 1 as n") --- ✄ ------------------------------------------------- """ ]) ================================================ FILE: .markdownlint.json ================================================ { "MD013": false, "MD030": false } ================================================ FILE: .prettierrc.yaml ================================================ # .prettierrc or .prettierrc.yaml trailingComma: "es5" tabWidth: 2 semi: false singleQuote: true MD013: false MD030: false ================================================ FILE: .tool-versions ================================================ erlang 24.1.7 elixir 1.13.0-otp-24 nodejs 12.6.0 #python 3.7.3 python 3.7.3 2.7.16 ruby 2.7.5 lua 5.3.5 terraform 0.15.4 direnv 2.20.0 ================================================ FILE: .travis.yml ================================================ sudo: required services: docker language: elixir matrix: include: - elixir: 1.7.4 otp_release: 21.2 env: - NEO4J_VERSION=3.2.14 - BOLT_V1_EXCLUDED=false - BOLT_V2_EXCLUDED=true - BOLT_V3_EXCLUDED=true - elixir: 1.7.4 otp_release: 21.2 env: - NEO4J_VERSION=3.2.14 - BOLT_V1_EXCLUDED=false - BOLT_V2_EXCLUDED=true - BOLT_V3_EXCLUDED=true - elixir: 1.7.4 otp_release: 21.2 env: - NEO4J_VERSION=3.4.17 - BOLT_V1_EXCLUDED=false - BOLT_V2_EXCLUDED=false - BOLT_V3_EXCLUDED=true - elixir: 1.7.4 otp_release: 21.2 env: - NEO4J_VERSION=3.5.14 - BOLT_V1_EXCLUDED=true - BOLT_V2_EXCLUDED=false - BOLT_V3_EXCLUDED=false - elixir: 1.8.2 otp_release: 21.2 env: - NEO4J_VERSION=3.2.14 - BOLT_V1_EXCLUDED=false - BOLT_V2_EXCLUDED=true - BOLT_V3_EXCLUDED=true - elixir: 1.8.2 otp_release: 21.2 env: - NEO4J_VERSION=3.2.14 - BOLT_V1_EXCLUDED=false - BOLT_V2_EXCLUDED=true - BOLT_V3_EXCLUDED=true - elixir: 1.8.2 otp_release: 21.2 env: - NEO4J_VERSION=3.4.17 - BOLT_V1_EXCLUDED=false - BOLT_V2_EXCLUDED=false - BOLT_V3_EXCLUDED=true - elixir: 1.8.2 otp_release: 21.2 env: - NEO4J_VERSION=3.5.14 - BOLT_V1_EXCLUDED=true - BOLT_V2_EXCLUDED=false - BOLT_V3_EXCLUDED=false - elixir: 1.8.2 otp_release: 21.2 env: - NEO4J_VERSION=4.2.1 - BOLT_V1_EXCLUDED=true - BOLT_V2_EXCLUDED=false - BOLT_V3_EXCLUDED=false exclude: - elixir: 1.8 otp_release: 19.3 - elixir: 1.8 otp_release: 18.3 - elixir: 1.7 otp_release: 18.3 - elixir: 1.6 otp_release: 18.3 - elixir: 1.5 otp_release: 21.2 - elixir: 1.4 otp_release: 21.2 - elixir: 1.3 otp_release: 21.2 - elixir: 1.3 otp_release: 20.3 - elixir: 1.2 otp_release: 21.2 - elixir: 1.2 otp_release: 20.3 addons: apt: sources: - ubuntu-toolchain-r-test packages: - g++-6 - ninja-build cache: directories: - $HOME/cmake env: global: - ELIXIR_ERL_OPTIONS="+T 9" - PATH=$HOME/cmake/bin:$PATH - CXX=g++-6 - CC=gcc-6 branches: only: - master before_install: - if [ ! -d "$HOME/cmake/bin" ]; then wget --no-check-certificate https://cmake.org/files/v3.5/cmake-3.5.2-Linux-x86_64.sh && sh cmake-3.5.2-Linux-x86_64.sh --prefix=$HOME/cmake --exclude-subdir; fi - docker run --name neo4j -d -p 7687:7687 -e 'NEO4J_AUTH=neo4j/test' neo4j:$NEO4J_VERSION - docker logs -f neo4j | sed /Bolt\ enabled/q script: - mix test --exclude routing --exclude bolt_v1:$BOLT_V1_EXCLUDED --exclude bolt_v2:$BOLT_V2_EXCLUDED --exclude bolt_v3:$BOLT_V3_EXCLUDED ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## 2.1.0 Thank you https://github.com/zediogoviana, for the following improvements: - Add configurable SSL options - Fix local connection - Keep the :ssl keyword to manage the options also Dependencies updated, paving the road to switching to latest Elixir/Erlang combo. ## 2.0.11 - Issue #100: Timeout set in config in now used by queries - DBConnection, bump dependencies ## 2.0.10 - Fix temporal types usage: microseconds are not fully available - Review to pass test on Neo4j 4 : - test and doctests to use new parameter syntax (using {} is deprecated in Neo4j 4) - `toUpper` instead of `upper` ## 2.0.9 - fix: (Bolt.Sips.Exception) unable to encode value: -128, see: https://boltprotocol.org/v1/#ints, for details. Closes #93 Thank you, @kalamarski-marcin ## 2.0.8 - Fix Response.profile not being properly filled. Closes #91 ## 2.0.7 - sometimes the server version is missing the patch number, and the router couldn't return the proper version. Thank you @barry-w-hill, for finding this bug and reporting it! - remove the `basic_auth` when using the `&Bolt.Sips.info/0` function. Thanks @dominique-vassard, for suggestion. Closes: #89 ## 2.0.6 - Fix 'unused alias' compilation warnings - Fix Bolt.Sips.Response type: `stats` was a `list` instead of `list|map` - Add typespec for Bolt.Sips.Types: Node, Relationship and UnboundRelationship ## 2.0.5 - fix #83. More details in commit: https://github.com/florinpatrascu/bolt_sips/commit/ebe17e62ab1d823e301b11d99d532663b0b25135 Thank you @kristofka! ## 2.0.4 - feature: support connection options in queries PR #82. Many thanks @tcrossland, for this contribution! This PR adds support for passing options through to DBConnection.execute/4 - fix some broken links, in the docs; closes #76 - update some dependencies, including the DBConnection package. - squashing some compile warnings; to be continued /attn: @team ;) - please use Elixir 1.9 or 1.10, for test and development - where possible. ## 2.0.3 - refactoring the internals for achieving a better performance, while improving the code readability and extensibility - many thanks to @kristofka and @dominique-vassard. You guys are awesome! - fix: Consistent bad connection state after malformed query [...] issue #78 ## === 2.0.2 === - The 2.0, stable release. Thank you all for your feedback and for contributing to making this driver better. w⦿‿⦿t! - fix: Simple Query taking too much time to process #73 ## 2.0.0-rc.2 - swapping the assets around, for better organizing the docs ## 2.0.0-rc.1 - more documentation - fix the TravisCi build - min versions erlang 21.2 elixir 1.7 ## === 2.0.0-rc === ## What's New? ### `bolt+routing://` is now supported Read more what this schema is, as defined by the [Neo4j team](https://neo4j.com/developer/kb/how-neo4j-browser-bolt-routing/) ### Role-based connections Until this version, Bolt.Sips was used for connecting to a single Neo4j server, aka: the "direct" mode. Basically you configure the driver with a url to a Neo4j server and Bolt.Sips will use that to attach itself to it, using a single configuration, remaining attached to that server until it is restarted (or reconfigured). In direct mode, bolt_sips "knows" only one server. Starting with this version you can have as many distinct connection configurations, each of them dedicated to different Neo4j servers, as/if needed. We call these connections: "role-based connections". For example, when you'll connect to a Neo4j cluster using the new protocol, i.e. by using a configuration like this: config :bolt_sips, Bolt, # default port considered to be: 7687 url: "bolt+routing://localhost", basic_auth: [username: "neo4j", password: "test"], pool_size: 10 Bolt.Sips will automatically create three pools of size 10, with the following **reserved** names: `:read`, `:write` and `:route`. Now you can specify what type of connection you want to use, by its name (role). For example: wconn = Bolt.Sips.conn(:write) ... = Bolt.Sips.query!(wconn, "CREATE (a:Person {name:'Bob'})") rconn = Bolt.Sips.conn(:read) ... = Bolt.Sips.query!(rconn, "MATCH (a:Person {name: 'Bob'}) RETURN a.name AS name") The roles above: `:read`, `:write` and `:route`, are reserved. Please do not name custom connections using the same names (atoms). And as you just realized, yes: now you can create as many Bolt.Sips **direct** "driver instances" as you want, or as many as your app/hardware supports. Please see the documentation for much more details. ### Main breaking changes introduced in version 2.x - the `hostname` config parameter is a string; used to be a charlist - the `url` config parameter must start with a valid schema i.e. `bolt`, `bolt+routing` or `neo4j`. Examples: url: "bolt://localhost" url: "bolt+routing://neo4j:password@neo01.graph.example.com:123456?policy=europe" - Bolt.Sips.Query, will return a Bolt.Sips.Response now; it used to be a simple data structure. ## === 1.5 === ## 1.5.1 - add a test alias for running the tests compatible with the most recent Neo4j server while disabling the older/legacy ones - cleanup some warning about unused aliases ## 1.5.0 - Bolt V3 support - Decompose tests by bolt version - Important note about transaction ## 1.4.0 - Encoding / Decoding types is now at the lowest possible level - Decompose encoders / decoders by bolt version - Expose only public API in docs ## 1.3.0 - 1.3.0 stable release. Many thanks to Dominique VASSARD, for his awesome contributions. ## 1.3.0-rc2 - Fix some typos - add json encoding capability ## 1.2.2-rc2 - Bug fix: Nanoseconds formating was erroneous. Example: 54 nanoseconds was formated to "PT0.54S" instead of "PT0.000000054S" - Bug fix: Large amount of nanoseconds (>= 1_000_000) wasn't treated and lead to Neo4j errors. Now large amount of nanoseconds are converted in seconds, with the remainder in nanoseconds. ## 1.2.1-rc2 - Bug fix: If a property contains a speciifc types (date, datetime, point, etc.), it wasn't decoded. see: https://github.com/florinpatrascu/bolt_sips/issues/55 ## 1.2.0-rc2 - support for the spatial and temporal types. ## 1.1.0-rc2 - removed the `boltex` dependency and added all its "low-level" code to `internals`. ## 1.0.0-rc2 ### Breaking changes introduced in version 1.x - non-closure based transactions are not supported anymore. This is a change introduced in DBConnection 2.x. `Bolt.Sips` version tagged `v0.5.10` is the last version supporting open transactions. - the support for ETLS was dropped. It was mostly used for development or hand-crafted deployments This version is using the official [DBConnection 2.0.0-rc2](https://hex.pm/packages/db_connection/2.0.0-rc.0), from [hex.pm](https://hex.pm) ## 0.5.10 - update the links referencing the Bolt protocol documentation (types, etc) ## 0.5.9 - upgrade dependencies - trading carefully around the new db_connection, as we're chasing the code from `master` currently, and there more changes in the pipe to come for the both projects; db_connection, and this one, respectively. ## 0.5.8 - dealing with negative integers see issue #42, for more details ## 0.5.7 - elixir 1.6 and code formatting, of course :) - minor test updates - update dependencies - pending code for the newest `db_connection` (currently using db_connection from the master branch) ## 0.5.5 - using the [DBConnection](https://hexdocs.pm/db_connection/DBConnection.html), thanks to the work done by Dmitriy Nesteryuk. ## 0.4.11 - using Elixir 1.5 - not using the ConCache anymore. I initially intended to use its support throughout the driver, but it is not needed. - README updated with a short snippet from a Phoenix web app demo, showing how to start Bolt.Sips, as a worker - dependencies update - minor code cleanup, to prep the code for receiving HA and Bolt routing capabilities ## v0.3.5 - better error messages; issue #33 - not retrying a connection when the server is not available/started - incorrect number of retries, performed by the driver in case of errors; was one extra ## v0.3.4 - dependencies update, minor code cleanup, listening to Credo :) and finally using a Markdown linter ## v0.3.3 - Add link to travis build; #31 by vic ## v0.3.2 - Use the project's own configuration file when executing the `bolt.cypher` mix task. Fixes issue #20 ## v0.3.1 Breaking changes - rollback/refactor to optionally allow external configuration options to be defined at runtime. You must start the Bolt.Sips manually, when needed, i.e. `Bolt.Sips.start_link(url: "localhost")`, or by changing your app's mix config file, i.e. ```elixir def application do [applications: [:logger, :bolt_sips], mod: {Bolt.Sips.Application, []}] end ``` You can also specify custom configuration settings in you app's mix config file. These may overwrite your config file: ```elixir def application do [extra_applications: [:logger], mod: {Bolt.Sips.Application, [url: 'localhost', pool_size: 15]} ] end ``` - code cleanup ## v0.2.6 (2017-04-21) - cleanup, and minor dependencies update ## v0.2.5 (2017-03-22) - split multi-line Cypher statements containing semicolons only if the `;` character is at the end of the line, followed by \r\n on Windows and \n on Unix like system, otherwise it may break the Cypher statement when the semicolon appears somewhere else ## v0.2.4 (2017-02-26) - add the fuzzyurl to the list of apps, for project using Elixir < 1.4 (thank you, @dnesteryuk!) ## v0.2.3 (2017-02-26) - improved connection handling ## v0.2.2 (2017-02-24) - PR #18; Bring up `:boltex` and `:retry` in `applications`, for Elixir < 1.4 (from: @wli0503, thank you!) - PR #19; test for error message on invalid parameter types (from: @vic, thank you!). ## v0.2.1 (2017-02-20) - stop retrying a request if the failure is an internal one (driver, or driver dependencies related). - update the Boltex driver containing two important bug fixes: one where Boltex will fail when receiving too much data (florinpatrascu/bolt_sips/issues/16) and the other one, an improvement, make Boltex.Error.get_id/1 more resilient for new transports (details here: mschae/boltex/issues/14) - changed the pool strategy to :fifo, and its timeout to :infinity, and let the (:gen_server) call timeout expire according to the user's :timeout configuration parameter - added a test unit provided by @adri (thank you), for executing a Cypher query, with large set of parameters ## v0.2.0 Breaking changes - Elixir 1.4 is now required. - Using Boltex 0.2.0 - bugfix: invalid Cypher statements will now be properly handled when the request is retried automatically ## v0.1.11 - With a larger amount of parameters it seems like generating chunks isn't working correctly. This is a patch imported from Boltex, see: https://github.com/mschae/boltex/issues/13, for more info ## v0.1.10 (2017-02-11) - accept Map and Struct for query parameters, transparently. Thank you [@wli0503], for the PR. ## v0.1.9 (2017-01-27) Some of the users are encountering difficulties when trying to compile bolt_sips on Windows. This release is addressing their concern. `Bolt.Sips` will use the optional System variable: `BOLT_WITH_ETLS`, for depending on the [ETLS](https://hex.pm/packages/etls) package. If that variable is not defined, then `Bolt.Sips` will use the standard Erlang [`:ssl` module](http://erlang.org/doc/man/ssl.html), for the SSL/TLS protocol; the default behavior, starting with this version. Therefore, if you want the **much** faster ssl/tls support offered by ETLS, then use this: `export BOLT_WITH_ETLS=true` on Linux/OSX, for example. Then: ```elixir mix deps.get mix test ``` and so on. (Don't forget to `mix deps.unlock --all`, if you plan to plan to further debugging/developing w/ or w/o the `BOLT_WITH_ETLS` support) Many thanks to: [Ben Wilson](https://elixir-lang.slack.com/team/benwilson512), for advices. ## v0.1.8 (2017-01-07) - using Elixir 1.4 - add more details to the README, about the components required to build ETLS, the TCP/TLS layer - added newer Elixirs to the Travis CI configuration file - minor code cleanups ## v0.1.7 (2017-01-02) - Connection code refactored for capturing the errors when the remote server is not responding on the first request, or if the driver is misconfigured i.e. wrong port number, bad hostname ... - updated the test configuration file with detailed info about the newly introduced option: `:retry_linear_backoff`, mostly as a reminder ## v0.1.6 (2017-01-01) - we're already using configurable timeouts, when executing requests from the connection pool. But with Bolt, the initial handshake sequence (happening before sending any commands to the server) is represented by two important calls, executed in sequence: `handshake` and `init`, and they must both succeed, before sending any (Cypher) requests. You can see the details in the [Bolt protocol](http://boltprotocol.org/v1/#handshake) specs. This sequence is also sensitive to latencies, such as: network latencies, busy servers, etc., and because of that we're introducing a simple support for retrying the handshake (and the subsequent requests) with a linear backoff, and try the handshake sequence (or the request) a couple of times before giving up - all these as part of the exiting pool management, of course. This retry is configurable via a new configuration parameter, the: `:retry_linear_backoff`, respectively. For example: ```elixir config :bolt_sips, Bolt, url: "bolt://Bilbo:Baggins@hobby-hobbits.dbs.graphenedb.com:24786", ssl: true, timeout: 15_000, retry_linear_backoff: [delay: 150, factor: 2, tries: 3] ``` In the example above the retry will linearly increase the delay from 150ms following a Fibonacci pattern, cap the delay at 15 seconds (the value defined by the `:timeout` parameter) and giving up after 3 attempts. The same retry mechanism (and configuration parameters) is also honored when we send requests to the neo4j server. ## v0.1.5 (2016-12-30) - as requested by many users, this version is introducing the optional `url` configuration parameter. If present, it will be used for extracting the host name, the port and the authentication details. Please see the README, for a couple of examples. For brevity: ```elixir config :bolt_sips, Bolt, url: 'bolt://demo:demo@hobby-wowsoeasy.dbs.graphenedb.com:24786', ssl: true ``` ## v0.1.4 (Merry Christmas) - add support for connecting to Neo4j servers on encrypted sockets. Currently only TLSv1.2 is supported, using the default [BoringSSL](https://boringssl.googlesource.com/boringssl/) cipher; via [:etls](https://github.com/kzemek/etls). To connect securely to a remote Neo4j server, such as the ones provided by graphenedb.com, modify your Bolt.Sips config file like this (example): ```elixir config :bolt_sips, Bolt, hostname: 'bolt://hobby-blah.dbs.graphenedb.com', basic_auth: [username: "wow", password: "of_course_this_is_the_password"], port: 24786, pool_size: 5, ssl: true, max_overflow: 1 ``` Observe the new flag: `ssl: true` Please note this is work in progress ## v0.1.2 (2016-11-06) - integrate the Boltex code from https://github.com/mschae/boltex, and let the Bolt.Sips wrapper to manage the connectivity, using a simple Poolboy implementation for connection pooling ## v0.1.1 (2016-09-09) - a temporary solution for dealing with negative values while extracting a graph walk-through from a Path. Dealing with this in Boltex instead, but this fix should work for now. ## v0.1.0 (2016-08-31) First release! ================================================ FILE: ISSUE_TEMPLATE.md ================================================ # Precheck * For bugs, do a quick search and make sure the bug has not yet been reported. * Finally, be nice and have fun! ## Environment * Elixir version (elixir -v): * Neo4j and version (Neo4j 3.5.3, etc.): * Connection scheme (`bolt://`, `bolt+routing://` or `neo4j://`): * Bolt.Sips version (mix deps): * Operating system: ## Current behavior Include code samples, errors and stacktraces if appropriate. ## Expected behavior ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ logo # Neo4j driver for Elixir. [![Build Status](https://travis-ci.org/florinpatrascu/bolt_sips.svg?branch=master)](https://travis-ci.org/florinpatrascu/bolt_sips) [![Hex.pm](https://img.shields.io/hexpm/dt/bolt_sips.svg?maxAge=2592000)](https://hex.pm/packages/bolt_sips) [![Hexdocs.pm](https://img.shields.io/badge/api-hexdocs-brightgreen.svg)](https://hexdocs.pm/bolt_sips) `Bolt.Sips` is an Elixir driver for [Neo4j](https://neo4j.com/developer/graph-database/), providing many useful features: - using the Bolt protocol, the Elixir implementation - the Neo4j's newest network protocol, designed for high-performance; latest Bolt versions, are supported. - Can connect to a standalone Neo4j server (`:direct` mode) or to a Neo4j causal cluster, using the `bolt+routing` or the newer `neo4j` schemes; connecting in `:routing` mode. - Provides the user with the ability to create and manage distinct ad-hoc `role-based` connections to one or more Neo4j servers/databases - Supports transactions, simple and complex Cypher queries with or w/o parameters - Multi-tenancy - Supports Neo4j versions: 3.0.x/3.1.x/3.2.x/3.4.x/3.5.x/4.0.x/4.1.x/4.2.x Notes: - Regarding Neo4j 4, stream capabilities are not yet supported. - If you're seeking a substitute driver, here's a compilation of repositories: - https://github.com/sagastume/boltx ## Table of Contents - [Installation](#installation) - [Getting Started](docs/getting-started.md#starting-the-driver) - [Basic usage](docs/getting-started.md#usage) - [Configuration](docs/features/configuration.md) - [Direct mode](docs/features/configuration.md#direct-mode) - [Routing](docs/features/configuration.md#routing-mode) - [Role-based connections](docs/features/configuration.md#role-based-connections) - [Multi tenancy](docs/features/configuration.md#multi-tenancy) - [Using Cypher](docs/features/using-cypher.md) - [Temporal and spatial types](docs/features/using-temporal-and-spatial-types.md) - [Transactions](docs/features/about-transactions.md) - [Encoding](docs/features/about-encoding.md) - [Routing, in detail](docs/features/routing.md) - [Multi tenancy, in detail](docs/features/multi-tenancy.md) - [Using Bolt.Sips with Phoenix](docs/features/using-with-phoenix.md) - [More examples](docs/examples/readme.md) ### Installation [Available in Hex](https://hex.pm/packages/bolt_sips), the package can be added to your list of dependencies, in the: `mix.exs`: ```elixir def deps do [{:bolt_sips, "~> 2.0"}] end ``` ### Basic usage Provided you have access to a running Neo4j server, and a project where you just added the `:bolt_sips` dependency, run an `iex` session inside the project's folder, and once inside the shell, follow this simple step-by-step example. Start an iex session: ```elixir Erlang/OTP 21 [erts-10.2.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe] Interactive Elixir (1.8.1) - press Ctrl+C to exit (type h() ENTER for help) iex> {:ok, _neo} = Bolt.Sips.start_link(url: "bolt://neo4j:test@localhost") {:ok, #PID<0.237.0>} iex> conn = Bolt.Sips.conn() #PID<0.242.0> iex> Bolt.Sips.query!(conn, "return 1 as n") |> ...> Bolt.Sips.Response.first() %{"n" => 1} ``` Please see the docs for more examples and details about this driver. ### Testing You'll need a running Neo4j server, for running the tests. Please verify that you do not store critical data on this server, as its contents will be wiped clean when the tests are running. If you have docker available on your system, you can start an instance before running the test suite: ```shell docker run --rm -p 7687:7687 -e 'NEO4J_AUTH=neo4j/test' neo4j:3.0.6 ``` Neo4j versions used for test: 3.0, 3.1, 3.4, 3.5 ```shell mix test ``` For the stubs using [boltkit](https://github.com/neo4j-drivers/boltkit/), you will have to install Python 3.7 and run: `pip install boltkit`. After this you can run any tests tagged with `:boltkit`. Example: ```shell mix test test/boltkit_test.exs --include boltkit ``` or: ```shell mix test --only boltkit ``` ### Special thanks - Michael Schaefermeyer (@mschae), for the initial version of the Bolt protocol in Elixir: [mschae/boltex](https://github.com/mschae/boltex) ### Contributors As reported by Github: [contributions to master, excluding merge commits](https://github.com/florinpatrascu/bolt_sips/graphs/contributors) ### Contributing - [Fork it](https://github.com/florinpatrascu/bolt_sips/fork) - Create your feature branch (`git checkout -b my-new-feature`) - Test (`mix test`) - Commit your changes (`git commit -am 'Add some feature'`) - Push to the branch (`git push origin my-new-feature`) - Create new Pull Request ### License ```txt Copyright 2016-2020 the original author or authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` ================================================ FILE: benchees/conn_to_local_bench.exs ================================================ # benchmark the time it takes to open connections to a local neo4j server. # Misc... # # What I want: # # - I want to measure the time it takes to open a connection and run # a simple query # - I want to demonstrate how saving and reusing a connection (where # applicable) takes less time compared with a similar code where # I'm creating a new connection for every query # # Sample from a quick run: # # $ mix run benchees/conn_to_local_bench.exs # # Operating System: macOS # CPU Information: Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz # Number of Available Cores: 8 # Available memory: 17.179869184 GB # Elixir 1.5.0 # Erlang 20.0 # Benchmark suite executing with the following configuration: # warmup: 2.00 s # time: 1.00 s # parallel: 1 # inputs: none specified # Estimated total run time: 6.00 s # # Benchmarking new conn... # Benchmarking same conn... # # Name ips average deviation median # same conn 1.46 K 0.68 ms ±20.73% 0.64 ms # new conn 0.47 K 2.15 ms ±67.17% 1.98 ms # # Comparison: # same conn 1.46 K # new conn 0.47 K - 3.14x slower {:ok, _pid} = Bolt.Sips.start_link(url: "localhost") simple_cypher = """ MATCH (p:Person)-[r:WROTE]->(b:Book {title: 'The Name of the Wind'}) RETURN p """ query = fn (conn, cypher) -> Bolt.Sips.Query.query(conn, cypher) end conn = Bolt.Sips.conn() Benchee.run( %{ "same conn" => fn -> query.(conn, simple_cypher) end, " new conn" => fn -> query.(Bolt.Sips.conn(), simple_cypher) end }, time: 1) ================================================ FILE: config/config.exs ================================================ # This file is responsible for configuring your application # and its dependencies with the aid of the Mix.Config module. import Config # This configuration is loaded before any dependency and is restricted # to this project. If another project depends on this project, this # file won't be loaded nor affect the parent project. For this reason, # if you want to provide default values for your application for # 3rd-party users, it should be done in your "mix.exs" file. # You can configure for your application as: # # config :bolt_sips, key: :value # # And access this configuration in your application as: # # Application.get_env(:bolt_sips, :key) # # Or configure a 3rd-party app: # # config :logger, level: :info # # It is also possible to import configuration files, relative to this # directory. For example, you can emulate configuration per environment # by uncommenting the line below and defining dev.exs, test.exs and such. # Configuration from the imported file will override the ones defined # here (which is why it is important to import them last). # import_config "#{Mix.env()}.exs" ================================================ FILE: config/dev.exs ================================================ import Config config :mix_test_watch, clear: true level = if System.get_env("DEBUG") do :debug else :info end config :bolt_sips, log: false, log_hex: false config :logger, :console, level: level, format: "$date $time [$level] $metadata$message\n" config :tzdata, :autoupdate, :disabled config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase config :eye_drops, tasks: [ %{ id: :docs, name: "docs", run_on_start: true, cmd: "mix docs", paths: ["lib/*", "README.md", "examples/*", "mix.exs"] } ] ================================================ FILE: config/test.exs ================================================ import Config config :bolt_sips, Bolt, # default port considered to be: 7687 url: "bolt://localhost", basic_auth: [username: "neo4j", password: "test"], pool_size: 10, max_overflow: 2, queue_interval: 500, queue_target: 1500, prefix: :default level = if System.get_env("DEBUG") do :debug else :info end config :bolt_sips, log: true, log_hex: false config :logger, :console, level: level, format: "$date $time [$level] $metadata$message\n" config :mix_test_watch, clear: true config :tzdata, :autoupdate, :disabled config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase config :porcelain, driver: Porcelain.Driver.Basic ================================================ FILE: docker-compose.yml ================================================ version: "3.0" networks: lan: services: core1: container_name: core1 image: neo4j:3.5.3-enterprise networks: - lan ports: - 7474:7474 - 6477:6477 - 7687:7687 environment: - NEO4J_AUTH=neo4j/test - NEO4J_dbms_mode=CORE - NEO4J_ACCEPT_LICENSE_AGREEMENT=yes - NEO4J_causalClustering_expectedCoreClusterSize=3 - NEO4J_causalClustering_initialDiscoveryMembers=core1:5000,core2:5000,core3:5000 - NEO4J_dbms_connector_http_listen__address=:7474 - NEO4J_dbms_connector_https_listen__address=:6477 - NEO4J_dbms_connector_bolt_listen__address=:7687 - NEO4J_dbms_memory_heap_initial__size=300m - NEO4J_dbms_memory_heap_max__size=300m - NEO4J_dbms_logs_query_enabled=true - NEO4J_dbms_logs_query_page__logging__enabled=false - NEO4J_dbms_logs_query_parameter__logging__enabled=true - NEO4J_dbms_logs_query_threshold=0 core2: container_name: core2 image: neo4j:3.5.3-enterprise networks: - lan ports: - 7475:7475 - 6478:6478 - 7688:7688 environment: - NEO4J_AUTH=neo4j/test - NEO4J_dbms_mode=CORE - NEO4J_ACCEPT_LICENSE_AGREEMENT=yes - NEO4J_causalClustering_expectedCoreClusterSize=3 - NEO4J_causalClustering_initialDiscoveryMembers=core1:5000,core2:5000,core3:5000 - NEO4J_dbms_connector_http_listen__address=:7475 - NEO4J_dbms_connector_https_listen__address=:6478 - NEO4J_dbms_connector_bolt_listen__address=:7688 - NEO4J_dbms_memory_heap_initial__size=300m - NEO4J_dbms_memory_heap_max__size=300m - NEO4J_dbms_logs_query_enabled=true - NEO4J_dbms_logs_query_page__logging__enabled=false - NEO4J_dbms_logs_query_parameter__logging__enabled=true - NEO4J_dbms_logs_query_threshold=0 core3: container_name: core3 image: neo4j:3.5.3-enterprise networks: - lan ports: - 7476:7476 - 6479:6479 - 7689:7689 environment: - NEO4J_AUTH=neo4j/test - NEO4J_dbms_mode=CORE - NEO4J_ACCEPT_LICENSE_AGREEMENT=yes - NEO4J_causalClustering_expectedCoreClusterSize=3 - NEO4J_causalClustering_initialDiscoveryMembers=core1:5000,core2:5000,core3:5000 - NEO4J_dbms_connector_http_listen__address=:7476 - NEO4J_dbms_connector_https_listen__address=:6479 - NEO4J_dbms_connector_bolt_listen__address=:7689 - NEO4J_dbms_memory_heap_initial__size=300m - NEO4J_dbms_memory_heap_max__size=300m - NEO4J_dbms_logs_query_enabled=true - NEO4J_dbms_logs_query_page__logging__enabled=false - NEO4J_dbms_logs_query_parameter__logging__enabled=true - NEO4J_dbms_logs_query_threshold=0 ================================================ FILE: docs/examples/readme.md ================================================ ================================================ FILE: docs/features/about-encoding.md ================================================ # About encoding Bolt.Sips provides support for encoding your query result in different formats. For now, only JSON is supported. There is two way of encoding data to json: - By using the helpers provided by the module `Bolt.Sips.ResponseEncoder` - Using your usual JSON encoding library. `Bolt.Sips` have implementation for: Jason and Poison. With this the query results can be automatically encoded by one of the libraries available: Jason or Poison. No further work is required when using a framework like: Phoenix, for example. A few examples around the encoding suport: ```elixir iex> query_result = [ %{ "t" => %Bolt.Sips.Types.Node{ id: 26, labels: ["Test"], properties: %{ "created_at" => "2019-08-03T12:34:56+01:00", "name" => "A test node", "uid" => 12345 } } } ] # Using Bolt.Sips.ResponseEncoder iex> Bolt.Sips.ResponseEncoder.encode(query_result, :json) {:ok, "[{\"t\":{\"id\":26,\"labels\":[\"Test\"],\"properties\":{\"created_at\":\"2019-08-03T12:34:56+01:00\",\"name\":\"A test node\",\"uid\":12345}}}]"} iex(11)> Bolt.Sips.ResponseEncoder.encode!(query_result, :json) "[{\"t\":{\"id\":26,\"labels\":[\"Test\"],\"properties\":{\"created_at\":\"2019-08-03T12:34:56+01:00\",\"name\":\"A test node\",\"uid\":12345}}}]" # Using Jason iex(14)> Jason.encode!(query_result) "[{\"t\":{\"id\":26,\"labels\":[\"Test\"],\"properties\":{\"created_at\":\"2019-08-03T12:34:56+01:00\",\"name\":\"A test node\",\"uid\":12345}}}]" # Using Poison iex(13)> Poison.encode!(query_result) "[{\"t\":{\"properties\":{\"uid\":12345,\"name\":\"A test node\",\"created_at\":\"2019-08-03T12:34:56+01:00\"},\"labels\":[\"Test\"],\"id\":26}}]" ``` Both solutions rely on protocols, then they can be easily overridden if needed. More info in the modules `Bolt.Sips.ResponseEncoder.Json`, `Bolt.Sips.ResponseEncoder.Json.Jason`, `Bolt.Sips.ResponseEncoder.Json.Poison` ================================================ FILE: docs/features/about-transactions.md ================================================ # About transactions Transaction management in Neo4j 3.5+ differs from what it was in prior versions. The cypher keyword `BEGIN`, `COMMIT` and `ROLLBACK` are no longer available. In order to have a query that runs fine in all versions, you should use the following pattern: ```elixir # Commit is performed automatically if everythings went fine conn = Bolt.Sips.conn() Bolt.Sips.transaction(conn, fn conn -> result = Bolt.Sips.query!(conn, "CREATE (m:Movie {title: "Matrix"}) RETURN m") end) # Rollback is performed automatically in case of error Bolt.Sips.transaction(conn, fn conn -> result =Bolt.Sips.query!(conn, "Invalid query") end) # Rollback can stil be forced Bolt.Sips.transaction(conn, fn conn -> result = Bolt.Sips.query!(conn, "CREATE (m:Movie {title: "Matrix"}) RETURN m") Bolt.Sips.rollback(conn, :dont_save) end) ``` ================================================ FILE: docs/features/configuration.md ================================================ # Configuration Bolt.Sips can be configured using the well known Mix config files, or by using simple keyword lists. This is the most basic configuration: ```elixir config :bolt_sips, Bolt, url: "bolt://localhost:7687" ``` It tells Bolt.Sips your Neo4j server is available locally, and it listens on port 7687, expecting bolt commands. These are the values you can configure, and their default values: - `:url`- a full url to pointing to a running Neo4j server. Please remember you must specify the scheme used to connect to the server. Valid schemes:`bolt`,`bolt+routing`and`neo4j` - the last two being used for connecting to a Neo4j causal cluster. - `:pool_size` - the size of the connection pool. Default: 15 - `:timeout` - a connection timeout value defined in milliseconds. Default: 15_000 - `:ssl`-`true`, if the connection must be encrypted. If the user wants to specify custom ssl options, just pass a keyword list with the options. More info [here](https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/ssl) Default:`false` - `:prefix`- used for differentiating between multiple connections available in the same app. Default:`:default` ## Examples of configurations Connecting to remote (hosted) Neo4j servers, such as the ones available (also for free) at [Neo4j/Sandbox](https://neo4j.com/sandbox-v2/): ```elixir config :bolt_sips, Bolt, url: "bolt://:", basic_auth: [username: "neo4j", password: "#######"] ssl: true # or for example `[verify: :verify_none]` if custom ssl options ``` ## Direct mode Until this version, `Bolt.Sips` was used for connecting to a single Neo4j server from the moment the hosting app started, until the hosting app was terminated/restarted. This is known as the: `direct` mode. In `direct` mode, the `Bolt.Sips` driver has one configuration describing the connection to a single Neo4j server. Since this connection mode is well known to our users, we'll not spend time on talking about it. It is sufficient to say that in direct mode, you have one configurable pool of connections, and the settings governing them i.e. timeout, size, etc., are all about this single connection. Because starting with version 2.0 `Bolt.Sips` is supporting a new type of connectivity: `routing`, for connecting to multiple servers or to a Neo4j causal cluster, you must specify the `scheme` in the `url` parameter, of your configuration. Example, for configuring a connection in `direct` mode: url: "bolt://localhost:7687" We'll spend more ink on talking about the `routing` mode, next. ## Routing mode With the 2.0 version, `Bolt.Sips` is implementing the ability to connect your app to a Neo4j causal cluster. You can read more about this, here: [Neo4j Causal Clustering](https://neo4j.com/docs/operations-manual/current/clustering/introduction/) The features of using a causal cluster, in Neo4j's own words: > Neo4j’s Causal Clustering provides three main features: > > - Safety: Core Servers provide a fault tolerant platform for transaction processing which will remain available while a simple majority of those Core Servers are functioning. > - Scale: Read Replicas provide a massively scalable platform for graph queries that enables very large graph workloads to be executed in a widely distributed topology. > - Causal consistency: when invoked, a client application is guaranteed to read at least its own writes. To configure `Bolt.Sips` for connecting to a Neo4j Causal Cluster, you only need the specify the appropriate scheme, in the `url` configuration parameter: url: "bolt+routing://localhost:7687" or: url: "neo4j://localhost:7687" Prefer the latter, since `bolt+routing` appears to be soon deprecated, by Neo4j. We'll use `neo4j://` throughout the docs for referring to the `routing` mode, for brevity. Read more about `routing`, [here](routing.md). ## Role based connections When we implemented the routing mode, we realized we could extend this ability to letting you define any number of connections, identified by a role name of your choice. For example, say your default configuration for `Bolt.Sips` looks like this: ```elixir config :bolt_sips, Bolt, url: "bolt://localhost:7687", basic_auth: [username: "neo4j", password: "test"], pool_size: 10, max_overflow: 2, ``` `Bolt.Sips` will load it by default, when your application starts. And with a configuration like that, the default mode, you will continue to obtain connections using the default `Bolt.Sips.conn()` function. However, if you require to have different connections, say: to a different Neo4j server that has some specific role, you could add a new configuration, for example: ```elixir config :bolt_sips, :hidden_gems, url: "bolt://localhost:1234", pool_size: 50, role: :hidden_gems ``` You'd have to load this config separately, after the starting the `Bolt.Sips`driver. Like this: ```elixir iex> Bolt.Sips.start_link(Application.get_env(:bolt_sips, :hidden_gems)) {:ok, #PID<0.266.0>} ``` and the you can use connections from this new configuration, as easy as this: ```elixir iex> conn = Bolt.Sips.conn(:hidden_gems) #PID<0.324.0> ``` while for obtaing the connections from your default configuration, is business as usual: ```elixir iex> conn = Bolt.Sips.conn() #PID<0.309.0> ``` The new connection pool is supervised by the main `Bolt.Sips.ConnectionSupervisor`, you don't have to do anythings special for that. ![](assets/role_based_connections.png?raw=true) In the final release, we'll add a friendlier api for adding role-based connections. More details about role-based-connections, [here](role-based-connections.md) ## Multi tenancy Another important feature of the 2.0 version, is: **multi-tenancy**. Starting with this version, your app can connect to any number of Neo4j servers, in `direct` mode or `routing`. We introduced a new configurable parameter, named: `prefix`. At this time, the only way to configure the driver for multi-tenancy, is programmatically, not via the configuration file. Example: ```elixir my_secret_cluster_config [ url: "neo4j://localhost:9001", basic_auth: [username: "neo4j", password: "test"], pool_size: 10, max_overflow: 2, queue_interval: 500, queue_target: 1500, prefix: :secret_cluster ] {:ok, _pid} = Bolt.Sips.start_link(@routing_connection_config) conn = Bolt.Sips.conn(:write, prefix: :secret_cluster) ``` And you can start as many connections as needed, for as long as the `:prefix` has different names. These connections can be used for connecting to the same or different Neo4j servers. More details about multi-tenancy, [here](multi-tenancy.md) ================================================ FILE: docs/features/multi-tenancy.md ================================================ # Multi tenancy Very similar to the role-based connections, with multi-tenancy you will be able to connect to servers where the type of the server (role) is defined by the server itself, such as the Neo4j causal cluster. This setting is sill in its infancy, it works, but you'll have to be careful when using it. For differentiating about Neo4j tenants, we introduced a new configurations parameter, named: `:prefix`. Example: ```elixir monster_cluster_conf = [ url: "neo4j://localhost", basic_auth: [username: "neo4j", password: "password"], pool_size: 50, prefix: :monster_cluster baby_monster_cluster_conf = [ url: "neo4j://raspberry_π", basic_auth: [username: "πs", password: "4VR"], pool_size: 50, prefix: :baby_monster_cluster ``` In the example above we defined two different connections, each of them pointing to different Neo4j clusters. As you know now, every cluster will have role-specific connections as defined by the routers, in those clusters. The connection roles will be: `:write`, `:read` and `:route`. To specify what connection you want and on what server, you will use the `:prefix` optional parameter of the new `Bolt.Sips.conn/2` method. Example: ```elixir Bolt.Sips.conn(:read, prefix: :monster_cluster) |> Bolt.Sips.query!("MATCH (n) RETURN n.name AS name") ``` or: ```elixir Bolt.Sips.conn(:read, prefix: :baby_monster_cluster) |> Bolt.Sips.query!("MATCH (n) RETURN n.name AS name") ``` (wip) ================================================ FILE: docs/features/role-based-connections.md ================================================ # Role-based connections Starting with the 2.0 version, you can have distinct configurations that you can use in your app, concurrently. These configurations can connect to connect in `:direct` mode to different Neo4j servers, or the same but with different credentials, pool sizes, etc. > This is not recommended for connecting to a causal cluster; the `:routing` mode, respectively. To differentiate between multiple `:direct` configurations, you'll use a new parameter: `:role`. Let's see a some code examples, for brevity. ```elixir frontend_config = [ url: "bolt://localhost", basic_auth: [username: "neo4j", password: "test"], pool_size: 10, max_overflow: 2, role: :frontend ] backend_config = [ url: "bolt://not_my_localhost:12345", basic_auth: [username: "xxxxx", password: "yyyyy"], pool_size: 10, max_overflow: 2, role: :backend ] {:ok, _pid} = Bolt.Sips.start_link(frontend_config) {:ok, _pid} = Bolt.Sips.start_link(backend_config) :frontend = Bolt.Sips.conn(:frontend) :backend = Bolt.Sips.conn(:backend) %Response{results: [%{"n" => 1}]} = Bolt.Sips.query!(:frontend, "RETURN 1 as n") %Response{results: [%{"n" => 1}]} = Bolt.Sips.query!(:backend, "RETURN 1 as n") ``` The last two Cypher queries above will be executed on two different servers. And yes you can run them concurrently since their respective pools will not compete for the same resources. If you desire to terminate a role-based connection, you can easily do so. Just like this: `:ok = Bolt.Sips.terminate_connections(:backend)`. ================================================ FILE: docs/features/routing.md ================================================ # Routing When connecting to a Neo4j cluster, `Bolt.Sips` will create 3 distinct connection pools, each of them dedicated to one of the following connection types (**connection roles**): - `:route` - used for getting information from the Neo4j router, such as: routing details about which server is handling what type of role: read/write, and more. - `:read` - used for read-only connections - `:write` - used for write-only connections. Having the `Bolt.Sips` configured in `routing` mode, will enforce your code to clarify what type of connections you want, type you **must** specify when requesting a `Bolt.Sips` connection. Example: ```elixir rconn = Bolt.Sips.conn(:read) wconn = Bolt.Sips.conn(:write) router_conn = Bolt.Sips.conn(:route) ``` Without being explicit about the connection type, you will receive errors, in case you'll attempt to execute a query that will say: create new nodes, on a server having the role: `read` or `route`. This is the only rule you must observe, when using the `Bolt.Sips` driver with a causal cluster. ## Routing walk-through Let's walk-through a simple experiment with using `Bolt.Sips` in routing mode and a Neo4j cluster. If you don't have a local server, or a remote Neo4j cluster available for your tests, you can easily setup your own local playground. All you need is Docker. We'll use a [docker-compose.yml](../../docker-compose.yml) file that you can find in the `Bolt.Sips` main source repo. If you have: - [Docker]() installed, and running. You can get Docker from here: https://docs.docker.com/installation/ - and a simple Elixir project having the `:bolt_sips` driver installer, as a dependency ### Start the Neo4j cluster In a folder where you have the `docker-compose.yml` file, start a new shell session and run the following command: docker-compose up If this is the first time you run this command, or use Neo4j as a Docker image, then based on the quality of your Internet connection, you'll wait a few seconds while Docker downloads a Neo4j Enterprise image. You'll see something like this: ```sh Creating network "neo4j_lan" with the default driver Pulling core1 (neo4j:3.5.3-enterprise)... 3.5.3-enterprise: Pulling from library/neo4j e7c96db7181b: Pull complete f910a506b6cb: Pull complete b6abafe80f63: Pull complete b95a7fd32595: Pull complete 6c09128ad074: Pull complete 648805e5f471: Pull complete e2790f69a70d: Pull complete Creating core2 ... done Creating core3 ... done Creating core1 ... done Attaching to core3, core1, core2 core3 | Changed password for user 'neo4j'. core1 | Changed password for user 'neo4j'. core2 | Changed password for user 'neo4j'. core3 | Active database: graph.db core3 | Directories in use: core3 | home: /var/lib/neo4j core3 | config: /var/lib/neo4j/conf ... ``` and towards the end of the starting sequence, this: ```sh core2 | 2019-06-17 12:37:59.078+0000 INFO Remote interface available at http://localhost:7475/ core3 | 2019-06-17 12:37:59.165+0000 INFO Remote interface available at http://localhost:7476/ ``` Check to see if you can connect to your local Neo4j cluster, as simple as pointing your Internet browser to this url: `http://localhost:7474`, and if everything was executed successfully, you'll be seeing the familiar Neo4j web interface. Now let's play with the `Bolt.Sips`driver and our local Neo4j cluster. Change your elixir test project configuration and modify the `config/config.exs` file like this (excerpt): ```elixir use Mix.Config config :bolt_sips, Bolt, # bolt+routing will be deprecated?! # url: "bolt+routing://localhost:7687", url: "neo4j://localhost:7687", basic_auth: [username: "neo4j", password: "test"], pool_size: 10 ``` then start a IEx shell session, from the projects'r main folder: `iex -S mix`. While inside the IEx session, let's see if our configuration is sound? ```elixir iex> Bolt.Sips.info() %{ default: %{ connections: %{ read: %{"localhost:7688" => 0, "localhost:7689" => 0}, route: %{ "localhost:7687" => 0, "localhost:7688" => 0, "localhost:7689" => 0 }, write: %{"localhost:7687" => 0}, routing_query: %{...}, ttl: 300, updated_at: 1560775628 }, user_options: [ url: "neo4j://localhost:7687", pool_size: 10, .... ] } } ``` if you see the response above, it means your settings are ready. Without going into much details about the data structure above, the routing details are these: ```elixir read: %{"localhost:7688" => 0, "localhost:7689" => 0}, write: %{"localhost:7687" => 0}, route: %{ "localhost:7687" => 0, "localhost:7688" => 0, "localhost:7689" => 0 ttl: 300, updated_at: ... ``` According to the routing information returned by our cluster, we have: - two nodes accepting `:read` commands: `localhost:7688` and`localhost:7689` - three nodes capabale of responding with routing specific details: `localhost:7687` `localhost:7688` and `localhost:7689` - one node accepting `:write` commands; the `localhost:7687`, respectively. But don't worry about the gory details, we got you covered :) Let's run some Cypher queries. ```elixir iex> alias Bolt.Sips.Response iex> alias Bolt.Sips, as: Neo # obtaining a read(only) connection: iex> rconn = Neo.conn(:read) #PID<0.324.0> # checking if there are any Person nodes "named": Bob? iex> %Response{results: r} = Neo.query!(rconn, "MATCH (p:Person{name: 'Bob'}) RETURN p") %Bolt.Sips.Response{ bookmark: "neo4j:bookmark:v1:tx2", fields: ["p"], notifications: [], plan: nil, profile: nil, records: [], results: [], stats: [], type: "r" } # r is [], meaning: our query found none. So let's create one. # First we obtain a connection suitable for `write` operations: iex> wconn = Neo.conn(:write) #PID<0.384.0> # and now we can use it for creating a new node: iex> %Response{results: r} = Neo.query!(wconn, "CREATE (p:Person{name:'Bob'})") %Bolt.Sips.Response{ ... stats: %{"labels-added" => 1, "nodes-created" => 1, "properties-set" => 1}, type: "w" } # our node was created and has one property set, w⦿‿⦿t! # but can we find it? Rerun the previous query using the `read` connection: iex> Neo.query!(rconn, "MATCH (p:Person{name: 'Bob'}) RETURN p") |> Response.first() %{ "p" => %Bolt.Sips.Types.Node{ id: 20, labels: ["Person"], properties: %{"name" => "Bob"} } } # and yessss, our new Person node is in the cluster! # Do you need its json form, instead? Easy: iex> Neo.query!(rconn, "MATCH (p:Person{name: 'Bob'}) RETURN p") |> ...> Response.first() |> ...> Bolt.Sips.ResponseEncoder.encode!(:json) "{\"p\":{\"id\":20,\"labels\":[\"Person\"],\"properties\":{\"name\":\"Bob\"}}}" ``` But what happens if we try to create a new Person, using our `read` connection? ```elixir iex> Neo.query!(rconn, "CREATE (p:Person{name:'Alice'})") ** (Bolt.Sips.Exception) ... No write operations are allowed directly on this database. Writes must pass through the leader. The role of this server is: FOLLOWER ``` Neo4j will promptly let us know we can't use that connection for write operations. This is the main difference that you must consider when coding. Same command executed on the proper (write) connection, will be successful: ```elixir iex> Neo.query!(wconn, "CREATE (p:Person{name:'Alice'})") %Bolt.Sips.Response{ ... stats: %{"labels-added" => 1, "nodes-created" => 1, "properties-set" => 1}, type: "w" } ``` ================================================ FILE: docs/features/using-cypher.md ================================================ # Using Bolt.Sips to query the Neo4j server Let's talk about the basics of querying a Neo4j server, using `Bolt.Sips`, and a few methods you could use for using the data returned by the server, using the `Bolt.Sips.Response`. You can learn so much more from the official docs, available at [Neo4j](https://neo4j.com/developer/graph-database/), you should start from there, if you want to get a deep understanding about the Neo4j graph database and its query language: Cypher. ## What you need? - a [Neo4j](https://neo4j.com/download/) server running locally and available at this `url`: `bolt://neo4j:test@localhost` - a mix project with `:bolt_sips` available ## Simple queries, using Cypher With the above prerequisites, let's drop into the IEx shell and start experimenting. ```sh cd my_neo4j iex -S mix Erlang/OTP 21 [erts-10.2.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe] Interactive Elixir (1.8.1) - press Ctrl+C to exit (type h() ENTER for help) iex> ``` First we need to start the driver with a minimalist configuration (unless it is already started by your project?): ```elixir iex> {:ok, _neo} = Bolt.Sips.start_link(url: "bolt://neo4j:test@localhost") {:ok, #PID<0.243.0>} iex> ``` Presuming your database is empty, you can still test your setup by running a simple Cypher query: ```elixir iex> conn = Bolt.Sips.conn() #PID<0.248.0> iex> Bolt.Sips.query!(conn, "RETURN 1 as n") |> ...> Bolt.Sips.Response.first() %{"n" => 1} ``` and we obtained our first response from the server: `%{"n" => 1}`, w⦿‿⦿t!! Now let's try some more complicated Cypher queries. We'll use examples that you may want to paste them in your `.exs/.ex` files rather than into the IEx shell, for readability. While most of the Cypher querier fit on a simple row, and they look compact, you might encounter situations where you need to send multiple queries in a single trip, to the server. `Bolt.Sips` allows you do that. Let's initialize our **test** database with some data. ```elixir cypher = """ CREATE (BoltSips:BoltSips {title:'Elixir sipping from Neo4j, using Bolt', released:2016, license:'MIT', bolt_sips: true}) CREATE (TNOTW:Book {title:'The Name of the Wind', released:2007, genre:'fantasy', bolt_sips: true}) CREATE (Patrick:Person {name:'Patrick Rothfuss', bolt_sips: true}) CREATE (Kvothe:Person {name:'Kote', bolt_sips: true}) CREATE (Denna:Person {name:'Denna', bolt_sips: true}) CREATE (Chandrian:Deamon {name:'Chandrian', bolt_sips: true}) CREATE (Kvothe)-[:ACTED_IN {roles:['sword fighter', 'magician', 'musician']}]->(TNOTW), (Denna)-[:ACTED_IN {roles:['many talents']}]->(TNOTW), (Chandrian)-[:ACTED_IN {roles:['killer']}]->(TNOTW), (Patrick)-[:WROTE]->(TNOTW) """ {:ok, response} = Bolt.Sips.conn() |> Bolt.Sips.query(cypher) ``` According to the response from the server, this is what we did: ```elixir iex> response %Bolt.Sips.Response{ results: [], stats: %{ "labels-added" => 6, "nodes-created" => 6, "properties-set" => 19, "relationships-created" => 4 }, type: "w" } ``` we have 6 new Nodes, 6 new labels and 4 new relationships. At any time, if you want to clean up the data we're creating, you can use this query: `MATCH (n {bolt_sips: true}) OPTIONAL MATCH (n)-[r]-() DELETE n,r` Observe we're adding a `bolt_sips` property to the Nodes we're adding, so that it's easier to refer them in our tests. Let's see how many nodes of "type" (`label`, according to Cypher's official terminology) `Person` having the property `bolt_sips` true, we have in our database: ```elixir iex> query = """ ...> MATCH (n:Person {bolt_sips: true}) ...> RETURN n.name AS Name ...> ORDER BY Name DESC ...> LIMIT 5 ...> """ iex> %Bolt.Sips.Response{} = response = Bolt.Sips.query!(conn, query) %Bolt.Sips.Response{ bookmark: "neo4j:bookmark:v1:tx21613", fields: ["Name"], notifications: [], plan: nil, profile: nil, records: [["Patrick Rothfuss"], ["Kote"], ["Denna"]], results: [ %{"Name" => "Patrick Rothfuss"}, %{"Name" => "Kote"}, %{"Name" => "Denna"} ], stats: [], type: "r" } ``` We have 3 of them, and we're only showing the `name` property! Above you see the full `Bolt.Sips.Response` returned by our driver based on the raw data returned by the Neo4j server. The `:results` key, contains the aggregated response you will use most of the time, and for that the `Bolt.Sips.Response` module has some useful helpers, for example: ```elixir iex> response |> ...> Bolt.Sips.Response.first() %{"Name" => "Patrick Rothfuss"} ``` and much more. Check the `Bolt.Sips.Response`'s own docs, for more. ================================================ FILE: docs/features/using-temporal-and-spatial-types.md ================================================ # Using temporal and spatial types Temporal and spatial types are supported since Neo4J 3.4. You can used the elixir structs: Time, NaiveDateTime, DateTime, as well as the Bolt Sips structs: DateTimeWithTZOffset, TimeWithTZOffset, Duration, Point. ```elixir $ MIX_ENV=test iex -S mix Erlang/OTP 21 [erts-10.0.5] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe] Interactive Elixir (1.7.3) - press Ctrl+C to exit (type h() ENTER for help) iex> alias Bolt.Sips.Types.{Duration, DateTimeWithTZOffset, Point, TimeWithTZOffset} [Bolt.Sips.Types.Duration, Bolt.Sips.Types.DateTimeWithTZOffset, Bolt.Sips.Types.Point, Bolt.Sips.Types.TimeWithTZOffset] iex> alias Bolt.Sips.TypesHelper Bolt.Sips.TypesHelper iex> {:ok, pid} = Bolt.Sips.start_link(url: "localhost", basic_auth: [username: "neo4j", password: "test"]) {:ok, #PID<0.236.0>} iex> conn = Bolt.Sips.conn :bolt_sips_pool # Date without timezone with Date iex(8)> Bolt.Sips.query!(conn, "RETURN date($d) AS d", %{d: ~D[2019-02-04]}) [%{"d" => ~D[2019-02-04]}] # Time without timezone with Time iex> Bolt.Sips.query!(conn, "RETURN localtime($t) AS t", %{t: ~T[13:26:08.543440]}) [%{"t" => ~T[13:26:08.543440]}] # Datetime without timezone with Naive DateTime iex> Bolt.Sips.query!(conn, "RETURN localdatetime($ldt) AS ldt", %{ldt: ~N[2016-05-24 13:26:08.543]}) [%{"ldt" => ~N[2016-05-24 13:26:08.543]}] # Datetime with timezone ID with DateTime (through Calendar) iex> date_time_with_tz_id = TypesHelper.datetime_with_micro(~N[2016-05-24 13:26:08.543], "Europe/Paris") #DateTime<2016-05-24 13:26:08.543+02:00 CEST Europe/Paris> iex> Bolt.Sips.query!(conn, "RETURN datetime($dt) AS dt", %{dt: date_time_with_tz_id}) [%{"dt" => #DateTime<2016-05-24 13:26:08.543+02:00 CEST Europe/Paris>}] # Datetime with timezone offset (seconds) with DateTimeWithTZOffset iex(17)> date_time_with_tz = DateTimeWithTZOffset.create(~N[2016-05-24 13:26:08.543], 7200) %Bolt.Sips.Types.DateTimeWithTZOffset{ naive_datetime: ~N[2016-05-24 13:26:08.543], timezone_offset: 7200 } iex(18)> Bolt.Sips.query!(conn, "RETURN datetime($dt) AS dt", %{dt: date_time_with_tz}) [ %{ "dt" => %Bolt.Sips.Types.DateTimeWithTZOffset{ naive_datetime: ~N[2016-05-24 13:26:08.543], timezone_offset: 7200 } } ] # Datetime with timezone offset (seconds) with TimeWithTZOffset iex> time_with_tz = TimeWithTZOffset.create(~T[12:45:30.250000], 3600) %Bolt.Sips.Types.TimeWithTZOffset{ time: ~T[12:45:30.250000], timezone_offset: 3600 } iex> Bolt.Sips.query!(conn, "RETURN time($t) AS t", %{t: time_with_tz}) [ %{ "t" => %Bolt.Sips.Types.TimeWithTZOffset{ time: ~T[12:45:30.250000], timezone_offset: 3600 } } ] # Cartesian 2D point with Point iex> point_cartesian_2D = Point.create(:cartesian, 50, 60.5) %Bolt.Sips.Types.Point{ crs: "cartesian", height: nil, latitude: nil, longitude: nil, srid: 7203, x: 50.0, y: 60.5, z: nil } iex> Bolt.Sips.query!(conn, "RETURN point($pt) AS pt", %{pt: point_cartesian_2D}) [ %{ "pt" => %Bolt.Sips.Types.Point{ crs: "cartesian", height: nil, latitude: nil, longitude: nil, srid: 7203, x: 50.0, y: 60.5, z: nil } } ] # Geographic 2D point with Point iex> point_geo_2D = Point.create(:wgs_84, 50, 60.5) %Bolt.Sips.Types.Point{ crs: "wgs-84", height: nil, latitude: 60.5, longitude: 50.0, srid: 4326, x: 50.0, y: 60.5, z: nil } iex> Bolt.Sips.query!(conn, "RETURN point($pt) AS pt", %{pt: point_geo_2D}) [ %{ "pt" => %Bolt.Sips.Types.Point{ crs: "wgs-84", height: nil, latitude: 60.5, longitude: 50.0, srid: 4326, x: 50.0, y: 60.5, z: nil } } ] # Cartesian 3D point with Point iex> point_cartesian_3D = Point.create(:cartesian, 50, 60.5, 12.34) %Bolt.Sips.Types.Point{ crs: "cartesian-3d", height: nil, latitude: nil, longitude: nil, srid: 9157, x: 50.0, y: 60.5, z: 12.34 } iex> Bolt.Sips.query!(conn, "RETURN point($pt) AS pt", %{pt: point_cartesian_3D}) [ %{ "pt" => %Bolt.Sips.Types.Point{ crs: "cartesian-3d", height: nil, latitude: nil, longitude: nil, srid: 9157, x: 50.0, y: 60.5, z: 12.34 } } ] # Geographic 2D point with Point iex> point_geo_3D = Point.create(:wgs_84, 50, 60.5, 12.34) %Bolt.Sips.Types.Point{ crs: "wgs-84-3d", height: 12.34, latitude: 60.5, longitude: 50.0, srid: 4979, x: 50.0, y: 60.5, z: 12.34 } iex> Bolt.Sips.query!(conn, "RETURN point($pt) AS pt", %{pt: point_geo_2D}) [ %{ "pt" => %Bolt.Sips.Types.Point{ crs: "wgs-84", height: nil, latitude: 60.5, longitude: 50.0, srid: 4326, x: 50.0, y: 60.5, z: nil } } ] ``` ================================================ FILE: docs/features/using-with-phoenix.md ================================================ # Using Bolt.Sips with Phoenix, or similar Don't forget to start the `Bolt.Sips` driver in your supervision tree. Example: ```elixir defmodule MoviesElixirPhoenix do use Application # See https://hexdocs.pm/elixir/Application.html # for more information on OTP Applications def start(_type, _args) do # Define workers and child supervisors to be supervised children = [ # Start the endpoint when the application starts {Bolt.Sips, Application.get_env(:bolt_sips, Bolt)}, %{ id: MoviesElixirPhoenix.Endpoint, start: {MoviesElixirPhoenix.Endpoint, :start_link, []} } ] # See https://hexdocs.pm/elixir/Supervisor.html # for other strategies and supported options opts = [strategy: :one_for_one, name: MoviesElixirPhoenix.Supervisor] Supervisor.start_link(children, opts) end # Tell Phoenix to update the endpoint configuration # whenever the application is updated. def config_change(changed, _new, removed) do MoviesElixirPhoenix.Endpoint.config_change(changed, removed) :ok end end ``` The code above was extracted from [the Neo4j Movies Demo](https://github.com/florinpatrascu/bolt_movies_elixir_phoenix), a Phoenix web application using this driver and the well known [Dataset - Movie Database](https://neo4j.com/developer/movie-database/). Note: as explained below, you don't need to convert your query result before having it encoded in JSON. BoltSips provides Jason and Poison implementation to tackle this problem automatically. ================================================ FILE: docs/getting-started.md ================================================ # Getting Started Let's start by creating a simple Elixir project, as a playground for our tests. ```sh mix new neo4j_demo --sup --app n4d --module N4D cd neo4j_demo ``` Open the `mix.exs` and add the bolt_sips dependency. ```elixir defmodule N4D.MixProject do use Mix.Project def project do [ app: :n4d, version: "0.1.0", elixir: "~> 1.8", start_permanent: Mix.env() == :prod, deps: deps() ] end # Run "mix help compile.app" to learn about applications. def application do [ extra_applications: [:logger], mod: {N4D.Application, []} ] end # Run "mix help deps" to learn about dependencies. defp deps do [ {:bolt_sips, "~> 2.0.0-rc"}, {:jason, "~> 1.1"} ] end end ``` we added the [jason](https://hex.pm/packages/jason) library too, for converting the server responses to json. And then run: ```sh mix do deps.get, compile ``` ## Starting the driver And our simple project is ready for us to start experimenting with it. Let's first configure the connection to a running Neo4j server. We presume a standalone community edition server is started and available on the `localhost` interface, and having its Bolt port open at: `7687`. For simplicity edit the `config/config.exs`, and modify it to look like this: ```elixir use Mix.Config config :bolt_sips, Bolt, url: "bolt://localhost:7687", basic_auth: [username: "neo4j", password: "test"], pool_size: 10 ``` With the project configured to connect to a Neo4j server, in direct mode, we can add `Bolt.Sips` to the app's main supervision tree, and let the OTP manage it. ```elixir # lib/n4_d/application.ex defmodule N4D.Application do @moduledoc false use Application def start(_type, _args) do children = [ {Bolt.Sips, Application.get_env(:bolt_sips, Bolt)} ] opts = [strategy: :one_for_one, name: N4D.Supervisor] Supervisor.start_link(children, opts) end end ``` There are a couple of different other ways to start the driver but let's keep it simple for now. The easiest way to start playing with the driver, in the current configuration, is to drop into the IEx shell and run simple Cypher commands through it. ```sh cd neo4j_demo iex -S mix ``` ## Usage A few examples: ```elixir iex> alias Bolt.Sips, as: Neo iex> alias Bolt.Sips.Response # check the driver is up and running: iex> Neo.info() %{ default: %{ connections: %{direct: %{"localhost:7687" => 0}, routing_query: nil}, user_options: [ socket: Bolt.Sips.Socket, port: 7687, url: "bolt://localhost:7687", # ... basic_auth: [username: "neo4j", password: "test"], pool_size: 10 ] } } # in direct mode, our current configuration, all the operations such as: read/write or # delete, are sent to the Neo4j server using a common connection (pool). # Let's obtain a connection: iex> conn = Neo.conn() #PID<0.308.0> # a few examples: iex> response = Neo.query!(conn, "CREATE (p:Person)-[:LIKES]->(t:Technology)") %Response{ bookmark: nil, fields: [], notifications: [], plan: nil, profile: nil, records: [], results: [], stats: %{ "labels-added" => 2, "nodes-created" => 2, "relationships-created" => 1 }, type: "w" } # query with undirected relationship unless sure of direction %Bolt.Sips.Response{results: results} = response = Neo.query!(conn, "MATCH (p:Person)-[:LIKES]-(t:Technology) RETURN p") # where `results` contain: [%{"p" => %Bolt.Sips.Types.Node{id: 355, labels: ["Person"], properties: %{}}}] # and we can also encode them to json, as simple as this: iex> Jason.encode!(results) "[{\"p\":{\"id\":355,\"labels\":[\"Person\"],\"properties\":{}}}]" # of course you can do more: iex> Bolt.Sips.query!(Bolt.Sips.conn(), "RETURN [10,11,21] AS arr", %{}, timeout: 19_000) |> ...> Enum.reduce(0, &(Enum.sum(&1["arr"]) + &2)) 42 # see more examples and the tests, for getting familiar with what is possible. # Enjoy! ``` Follow this link: [Cypher Basics](https://neo4j.com/developer/cypher-query-language/), for a gentle introduction to Cypher; Neo4j's query language. Throughout the code snippets we are often using examples copied from the original documentation published by Neo4j, so that you can feel comfortable with them. ================================================ FILE: lib/bolt_sips/application.ex ================================================ defmodule Bolt.Sips.Application do @moduledoc false use Application alias Bolt.Sips def start(_, start_args) do Sips.start_link(start_args) end def stop(_state) do :ok end end ================================================ FILE: lib/bolt_sips/enumerable_response.ex ================================================ defimpl Enumerable, for: Bolt.Sips.Response do alias Bolt.Sips.Response def count(%Response{results: nil}), do: {:ok, 0} def count(%Response{results: []}), do: {:ok, 0} def count(%Response{results: results}), do: {:ok, length(results)} def member?(%Response{fields: fields}, field), do: {:ok, Enum.member?(fields, field)} def slice(_response), do: {:error, __MODULE__} def reduce(%Response{results: []}, acc, _fun), do: acc def reduce(%Response{results: results}, acc, fun) when is_list(results), do: reduce_list(results, acc, fun) defp reduce_list(_, {:halt, acc}, _fun), do: {:halted, acc} defp reduce_list(list, {:suspend, acc}, fun), do: {:suspended, acc, &reduce_list(list, &1, fun)} defp reduce_list([], {:cont, acc}, _fun), do: {:done, acc} defp reduce_list([h | t], {:cont, acc}, fun), do: reduce_list(t, fun.(h, acc), fun) @doc false def slice(%Response{results: []}, _start, _count), do: [] def slice(_response, _start, 0), do: [] def slice(%Response{results: [head | tail]}, 0, count), do: [head | slice(tail, 0, count - 1)] def slice(%Response{results: [_head | tail]}, start, count), do: slice(tail, start - 1, count) end ================================================ FILE: lib/bolt_sips/error.ex ================================================ defmodule Bolt.Sips.Error do @moduledoc """ represents an error message """ alias __MODULE__ @type t :: %__MODULE__{} defstruct [:code, :message] def new(%Bolt.Sips.Internals.Error{ code: code, connection_id: cid, function: f, message: message, type: t }) do {:error, %Error{ code: code, message: "Details: #{message}; connection_id: #{inspect(cid)}, function: #{inspect(f)}, type: #{ inspect(t) }" }} end def new({:ignored, f} = _r), do: new({:error, f}) def new({:failure, %{"code" => code, "message" => message}} = _r) do {:error, %Error{code: code, message: message}} end def new(r), do: r end ================================================ FILE: lib/bolt_sips/exception.ex ================================================ defmodule Bolt.Sips.Exception do @moduledoc """ This module defines a `Bolt.Sips.Exception` structure containing two fields: * `code` - the error code * `message` - the error details """ @type t :: %Bolt.Sips.Exception{} defexception [:code, :message] end ================================================ FILE: lib/bolt_sips/internals/bolt_protocol.ex ================================================ defmodule Bolt.Sips.Internals.BoltProtocol do @moduledoc false # A library that handles Bolt Protocol (v1 and v2). # Note that for now, only Neo4j implements Bolt v2. # It handles all the protocol specific steps (i.e. # handshake, init) as well as sending and receiving messages and wrapping # them in chunks. # It abstracts transportation, expecting the transport layer to define # `send/2` and `recv/3` analogous to `:gen_tcp`. # ## Logging configuration # Logging can be enable / disable via config files (e.g, `config/config.exs`). # - `:log`: (bool) wether Bolt.Sips.Internals. should produce logs or not. Defaults to `false` # - `:log_hex`: (bool) wether Bolt.Sips.Internals. should produce logs hexadecimal counterparts. While this may be interesting, # note that all the hexadecimal data will be written and this can be very long, and thus can seriously impact performances. Defaults to `false` # For example, configuration to see the logs and their hexadecimal counterparts: # ``` # config :Bolt.Sips.Internals., # log: true, # log_hex: true # ``` # # #### Examples of logging (without log_hex) # iex> Bolt.Sips.Internals.test('localhost', 7687, "RETURN 1 as num", %{}, {"neo4j", "password"}) # C: HANDSHAKE ~ "<<0x60, 0x60, 0xB0, 0x17>> [2, 1, 0, 0]" # S: HANDSHAKE ~ 2 # C: INIT ~ ["BoltSips/1.1.0.rc2", %{credentials: "password", principal: "neo4j", scheme: "basic"}] # S: SUCCESS ~ %{"server" => "Neo4j/3.4.1"} # C: RUN ~ ["RETURN 1 as num", %{}] # S: SUCCESS ~ %{"fields" => ["num"], "result_available_after" => 1} # C: PULL_ALL ~ [] # S: RECORD ~ [1] # S: SUCCESS ~ %{"result_consumed_after" => 0, "type" => "r"} # [ # success: %{"fields" => ["num"], "result_available_after" => 1}, # record: [1], # success: %{"result_consumed_after" => 0, "type" => "r"} # ] # #### Examples of logging (with log_hex) # iex> Bolt.Sips.Internals.test('localhost', 7687, "RETURN 1 as num", %{}, {"neo4j", "password"}) # 13:32:23.882 [debug] C: HANDSHAKE ~ "<<0x60, 0x60, 0xB0, 0x17>> [2, 1, 0, 0]" # S: HANDSHAKE ~ <<0x0, 0x0, 0x0, 0x2>> # S: HANDSHAKE ~ 2 # C: INIT ~ ["BoltSips/1.1.0.rc2", %{credentials: "password", principal: "neo4j", scheme: "basic"}] # C: INIT ~ <<0x0, 0x42, 0xB2, 0x1, 0x8C, 0x42, 0x6F, 0x6C, 0x74, 0x65, 0x78, 0x2F, 0x30, 0x2E, 0x35, 0x2E, 0x30, 0xA3, 0x8B, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6E, 0x74, 0x69, 0x61, 0x6C, 0x73, 0x88, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6F, 0x72, 0x64, 0x89, 0x70, 0x72, 0x69, 0x6E, 0x63, 0x69, 0x70, 0x61, 0x6C, 0x85, 0x6E, 0x65, 0x6F, 0x34, 0x6A, 0x86, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x65, 0x85, 0x62, 0x61, 0x73, 0x69, 0x63, 0x0, 0x0>> # S: SUCCESS ~ <<0xA1, 0x86, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x8B, 0x4E, 0x65, 0x6F, 0x34, 0x6A, 0x2F, 0x33, 0x2E, 0x34, 0x2E, 0x31>> # S: SUCCESS ~ %{"server" => "Neo4j/3.4.1"} # C: RUN ~ ["RETURN 1 as num", %{}] # C: RUN ~ <<0x0, 0x13, 0xB2, 0x10, 0x8F, 0x52, 0x45, 0x54, 0x55, 0x52, 0x4E, 0x20, 0x31, 0x20, 0x61, 0x73, 0x20, 0x6E, 0x75, 0x6D, 0xA0, 0x0, 0x0>> # S: SUCCESS ~ <<0xA2, 0xD0, 0x16, 0x72, 0x65, 0x73, 0x75, 0x6C, 0x74, 0x5F, 0x61, 0x76, 0x61, 0x69, 0x6C, 0x61, 0x62, 0x6C, 0x65, 0x5F, 0x61, 0x66, 0x74, 0x65, 0x72, 0x1, 0x86, 0x66, 0x69, 0x65, 0x6C, 0x64, 0x73, 0x91, 0x83, 0x6E, 0x75, 0x6D>> # S: SUCCESS ~ %{"fields" => ["num"], "result_available_after" => 1} # C: PULL_ALL ~ [] # C: PULL_ALL ~ <<0x0, 0x2, 0xB0, 0x3F, 0x0, 0x0>> # S: RECORD ~ <<0x91, 0x1>> # S: RECORD ~ [1] # S: SUCCESS ~ <<0xA2, 0xD0, 0x15, 0x72, 0x65, 0x73, 0x75, 0x6C, 0x74, 0x5F, 0x63, 0x6F, 0x6E, 0x73, 0x75, 0x6D, 0x65, 0x64, 0x5F, 0x61, 0x66, 0x74, 0x65, 0x72, 0x0, 0x84, 0x74, 0x79, 0x70, 0x65, 0x81, 0x72>> # S: SUCCESS ~ %{"result_consumed_after" => 0, "type" => "r"} # [ # success: %{"fields" => ["num"], "result_available_after" => 1}, # record: [1], # success: %{"result_consumed_after" => 0, "type" => "r"} # ] # ## Shared options # Functions that allow for options accept these default options: # * `recv_timeout`: The timeout for receiving a response from the Neo4J s # server (default: #{@recv_timeout}) alias Bolt.Sips.Metadata alias Bolt.Sips.Internals.BoltProtocolV1 alias Bolt.Sips.Internals.BoltProtocolV3 defdelegate handshake(transport, port, options \\ []), to: BoltProtocolV1 defdelegate init(transport, port, version, auth \\ {}, options \\ []), to: BoltProtocolV1 defdelegate hello(transport, port, version, auth \\ {}, options \\ []), to: BoltProtocolV3 defdelegate goodbye(transport, port, version), to: BoltProtocolV3 defdelegate ack_failure(transport, port, bolt_version, options \\ []), to: BoltProtocolV1 defdelegate reset(transport, port, bolt_version, options \\ []), to: BoltProtocolV1 defdelegate discard_all(transport, port, bolt_version, options \\ []), to: BoltProtocolV1 defdelegate begin(transport, port, bolt_version, metadata \\ %Metadata{}, options \\ []), to: BoltProtocolV3 defdelegate commit(transport, port, bolt_version, options \\ []), to: BoltProtocolV3 defdelegate rollback(transport, port, bolt_version, options \\ []), to: BoltProtocolV3 defdelegate pull_all(transport, port, bolt_version, options \\ []), to: BoltProtocolV1 @doc """ run for all Bolt version, but call differs. For Bolt <= 2, use: run_statement(transport, port, bolt_version, statement, params, options) For Bolt >=3: run_statement(transport, port, bolt_version, statement, params, metadata, options) Note that Bolt V2 calls works with Bolt V3, but it is preferrable to update them. """ @spec run( atom(), port(), integer(), String.t(), map(), nil | Keyword.t() | Bolt.Sips.Metadata.t(), nil | Keyword.t() ) :: {:ok, tuple()} | Bolt.Sips.Internals.Error.t() def run( transport, port, bolt_version, statement, params \\ %{}, options_or_metadata \\ [], options \\ [] ) def run(transport, port, bolt_version, statement, params, options_or_metadata, _) when bolt_version <= 2 do BoltProtocolV1.run( transport, port, bolt_version, statement, params, options_or_metadata || [] ) end def run(transport, port, bolt_version, statement, params, metadata, options) when bolt_version >= 2 do metadata = case metadata do [] -> %{} metadata -> metadata end {metadata, options} = manage_metadata_and_options(metadata, options) BoltProtocolV3.run(transport, port, bolt_version, statement, params, metadata, options) end defp manage_metadata_and_options([], options) do {:ok, empty_metadata} = Metadata.new(%{}) {empty_metadata, options} end defp manage_metadata_and_options([_ | _] = metadata, options) do {:ok, empty_metadata} = Metadata.new(%{}) {empty_metadata, metadata ++ options} end defp manage_metadata_and_options(metadata, options) do {metadata, options} end @doc """ run_statement for all Bolt version, but call differs. For Bolt <= 2, use: run_statement(transport, port, bolt_version, statement, params, options) For Bolt >=3: run_statement(transport, port, bolt_version, statement, params, metadata, options) Note that Bolt V2 calls works with Bolt V3, but it is preferrable to update them. """ @spec run_statement( atom(), port(), integer(), String.t(), map(), nil | Keyword.t() | Bolt.Sips.Metadata.t(), nil | Keyword.t() ) :: list() | Bolt.Sips.Internals.Error.t() def run_statement( transport, port, bolt_version, statement, params \\ %{}, options_v2_or_metadata_v3 \\ [], options_v3 \\ [] ) def run_statement(transport, port, bolt_version, statement, params, options_or_metadata, _) when bolt_version <= 2 do BoltProtocolV1.run_statement( transport, port, bolt_version, statement, params, options_or_metadata || [] ) end def run_statement(transport, port, bolt_version, statement, params, metadata, options) when bolt_version >= 2 do metadata = case metadata do [] -> %{} metadata -> metadata end BoltProtocolV3.run_statement( transport, port, bolt_version, statement, params, metadata, options ) end end ================================================ FILE: lib/bolt_sips/internals/bolt_protocol_helper.ex ================================================ defmodule Bolt.Sips.Internals.BoltProtocolHelper do @moduledoc false alias Bolt.Sips.Internals.PackStream.Message alias Bolt.Sips.Internals.Error @recv_timeout :infinity #10_000 @zero_chunk <<0x00, 0x00>> @summary ~w(success ignored failure)a @doc """ Sends a message using the Bolt protocol and PackStream encoding. Message have to be in the form of {message_type, [data]}. """ @spec send_message(atom(), port(), integer(), Bolt.Sips.Internals.PackStream.Message.raw()) :: :ok | {:error, any()} def send_message(transport, port, bolt_version, message) do message |> Message.encode(bolt_version) |> (fn data -> transport.send(port, data) end).() end @doc """ Receives data. This function is supposed to be called after a request to the server has been made. It receives data chunks, mends them (if they were split between frames) and decodes them using PackStream. When just a single message is received (i.e. to acknowledge a command), this function returns a tuple with two items, the first being the signature and the second being the message(s) itself. If a list of messages is received it will return a list of the former. The same goes for the messages: If there was a single data point in a message said data point will be returned by itself. If there were multiple data points, the list will be returned. The signature is represented as one of the following: * `:success` * `:record` * `:ignored` * `:failure` ## Options See "Shared options" in the documentation of this module. """ @spec receive_data(atom(), port(), integer(), Keyword.t(), list()) :: {atom(), Bolt.Sips.Internals.PackStream.value()} | {:error, any()} | Bolt.Sips.Internals.Error.t() def receive_data(transport, port, bolt_version, options \\ [], previous \\ []) do with {:ok, data} <- do_receive_data(transport, port, options) do case Message.decode(data, bolt_version) do {:record, _} = data -> receive_data(transport, port, bolt_version, options, [data | previous]) {status, _} = data when status in @summary and previous == [] -> data {status, _} = data when status in @summary -> Enum.reverse([data | previous]) other -> {:error, Error.exception(other, port, :receive_data)} end else other -> # Should be the line below to have a cleaner typespec # Keep the old return value to not break usage # {:error, Error.exception(other, port, :receive_data)} Error.exception(other, port, :receive_data) end end @spec do_receive_data(atom(), port(), Keyword.t()) :: {:ok, binary()} defp do_receive_data(transport, port, options) do #recv_timeout = get_recv_timeout(options) case transport.recv(port, 2, :infinity) do {:ok, <>} -> do_receive_data_(transport, port, chunk_size, options, <<>>) other -> other end end @spec do_receive_data_(atom(), port(), integer(), Keyword.t(), binary()) :: {:ok, binary()} defp do_receive_data_(transport, port, chunk_size, options, old_data) do recv_timeout = get_recv_timeout(options) with {:ok, data} <- transport.recv(port, chunk_size, recv_timeout), {:ok, marker} <- transport.recv(port, 2, recv_timeout) do case marker do @zero_chunk -> {:ok, <>} <> -> data = <> do_receive_data_(transport, port, chunk_size, options, data) end else other -> Error.exception(other, port, :recv) end end @doc """ Define timeout """ @spec get_recv_timeout(Keyword.t()) :: integer() def get_recv_timeout(options) do Keyword.get(options, :recv_timeout, @recv_timeout) end @doc """ Deal with message without data. ## Example iex> BoltProtocolHelper.treat_simple_message(:reset, :gen_tcp, port, 1, []) :ok """ @spec treat_simple_message( Bolt.Sips.Internals.Message.out_signature(), atom(), port(), integer(), Keyword.t() ) :: :ok | Error.t() def treat_simple_message(message, transport, port, bolt_version, options) do send_message(transport, port, bolt_version, {message, []}) case receive_data(transport, port, bolt_version, options) do {:success, %{}} -> :ok error -> Error.exception(error, port, message) end end end ================================================ FILE: lib/bolt_sips/internals/bolt_protocol_v1.ex ================================================ defmodule Bolt.Sips.Internals.BoltProtocolV1 do @moduledoc false alias Bolt.Sips.Internals.BoltProtocolHelper alias Bolt.Sips.Internals.BoltVersionHelper alias Bolt.Sips.Internals.Error @hs_magic <<0x60, 0x60, 0xB0, 0x17>> @doc """ Initiates the handshake between the client and the server. See [http://boltprotocol.org/v1/#handshake](http://boltprotocol.org/v1/#handshake) ## Options See "Shared options" in `Bolt.Sips.Internals.BoltProtocolHelper` documentation. ## Example iex> BoltProtocolV1.handshake(:gen_tcp, port, []) {:ok, bolt_version} """ @spec handshake(atom(), port(), Keyword.t()) :: {:ok, integer()} | {:error, Bolt.Sips.Internals.Error.t()} def handshake(transport, port, options \\ [recv_timeout: 15_000]) do recv_timeout = BoltProtocolHelper.get_recv_timeout(options) max_version = BoltVersionHelper.last() # Define version list. Should be a 4 integer list # Example: [1, 0, 0, 0] versions = ((max_version..0 |> Enum.into([])) ++ [0, 0, 0]) |> Enum.take(4) Bolt.Sips.Internals.Logger.log_message( :client, :handshake, "#{inspect(@hs_magic, base: :hex)} #{inspect(versions)}" ) data = @hs_magic <> Enum.into(versions, <<>>, fn version_ -> <> end) transport.send(port, data) case transport.recv(port, 4, recv_timeout) do {:ok, <> = packet} when version <= max_version -> Bolt.Sips.Internals.Logger.log_message(:server, :handshake, packet, :hex) Bolt.Sips.Internals.Logger.log_message(:server, :handshake, version) {:ok, version} {:ok, other} -> {:error, Error.exception(other, port, :handshake)} other -> {:error, Error.exception(other, port, :handshake)} end end @doc """ Initialises the connection. Expects a transport module (i.e. `gen_tcp`) and a `Port`. Accepts authorisation params in the form of {username, password}. See [https://boltprotocol.org/v1/#message-init](https://boltprotocol.org/v1/#message-init) ## Options See "Shared options" in `Bolt.Sips.Internals.BoltProtocolHelper` documentation. ## Examples iex> Bolt.Sips.Internals.BoltProtocol.init(:gen_tcp, port, 1, {}, []) {:ok, info} iex> Bolt.Sips.Internals.BoltProtocol.init(:gen_tcp, port, 1, {"username", "password"}, []) {:ok, info} """ @spec init(atom(), port(), integer(), tuple(), Keyword.t()) :: {:ok, any()} | {:error, Bolt.Sips.Internals.Error.t()} def init(transport, port, bolt_version, auth, options \\ [recv_timeout: 15_000]) do BoltProtocolHelper.send_message(transport, port, bolt_version, {:init, [auth]}) case BoltProtocolHelper.receive_data(transport, port, bolt_version, options) do {:success, info} -> {:ok, info} {:failure, response} -> {:error, Error.exception(response, port, :init)} other -> {:error, Error.exception(other, port, :init)} end end @doc """ Implementation of Bolt's RUN. It passes a statement for execution on the server. Note that this message doesn't return the statemetn result. For this purpose, use PULL_ALL. See [https://boltprotocol.org/v1/#message-run](https://boltprotocol.org/v1/#message-run) ## Options See "Shared options" in `Bolt.Sips.Internals.BoltProtocolHelper` documentation. ## Example iex> BoltProtocolV1.run(:gen_tcp, port, 1, "RETURN $num AS num", %{num: 5}, []) {:ok, {:success, %{"fields" => ["num"]}}} """ @spec run(atom(), port(), integer(), String.t(), map(), Keyword.t()) :: {:ok, any()} | {:error, Bolt.Sips.Internals.Error.t()} def run(transport, port, bolt_version, statement, params, options) do BoltProtocolHelper.send_message(transport, port, bolt_version, {:run, [statement, params]}) case BoltProtocolHelper.receive_data(transport, port, bolt_version, options) do {:success, _} = result -> {:ok, result} {:failure, response} -> {:error, Error.exception(response, port, :run)} %Error{} = error -> {:error, error} other -> {:error, Error.exception(other, port, :run)} end end @doc """ Implementation of Bolt's PULL_ALL. It retrieves all remaining items from the active result stream. See [https://boltprotocol.org/v1/#message-run](https://boltprotocol.org/v1/#message-run) ## Options See "Shared options" in `Bolt.Sips.Internals.BoltProtocolHelper` documentation. ## Example iex> BoltProtocolV1.run(:gen_tcp, port, 1, "RETURN $num AS num", %{num: 5}, []) {:ok, {:success, %{"fields" => ["num"]}}} iex> BoltProtocolV1.pull_all(:gen_tcp, port_, 1, []) {:ok, [ record: [5], success: %{"type" => "r"} ]} """ @spec pull_all(atom(), port(), integer(), Keyword.t()) :: {:ok, list()} | {:failure, Bolt.Sips.Internals.Error.t()} | {:failure, Bolt.Sips.Internals.Error.t()} def pull_all(transport, port, bolt_version, options) do BoltProtocolHelper.send_message(transport, port, bolt_version, {:pull_all, []}) with data <- BoltProtocolHelper.receive_data(transport, port, bolt_version, options), data <- List.wrap(data), {:success, _} <- List.last(data) do {:ok, data} else {:failure, response} -> {:failure, Error.exception(response, port, :pull_all)} other -> {:error, Error.exception(other, port, :pull_all)} end end @doc """ Runs a statement (most likely Cypher statement) and returns a list of the records and a summary (Act as as a RUN + PULL_ALL). Records are represented using PackStream's record data type. Their Elixir representation is a Keyword with the indexes `:sig` and `:fields`. ## Options See "Shared options" in `Bolt.Sips.Internals.BoltProtocolHelper` documentation. ## Examples iex> Bolt.Sips.Internals.BoltProtocol.run_statement(:gen_tcp, port, 1, "MATCH (n) RETURN n") [ {:success, %{"fields" => ["n"]}}, {:record, [sig: 1, fields: [1, "Example", "Labels", %{"some_attribute" => "some_value"}]]}, {:success, %{"type" => "r"}} ] """ @spec run_statement(atom(), port(), integer(), String.t(), map(), Keyword.t()) :: [ Bolt.Sips.Internals.PackStream.Message.decoded() ] | Bolt.Sips.Internals.Error.t() def run_statement(transport, port, bolt_version, statement, params, options) do with {:ok, run_data} <- run(transport, port, bolt_version, statement, params, options), {:ok, result} <- pull_all(transport, port, bolt_version, options) do [run_data | result] else {:error, %Error{} = error} -> error other -> Error.exception(other, port, :run_statement) end end @doc """ Implementation of Bolt's DISCARD_ALL. It discards all remaining items from the active result stream. See [https://boltprotocol.org/v1/#message-discard-all](https://boltprotocol.org/v1/#message-discard-all) See http://boltprotocol.org/v1/#message-ack-failure ## Options See "Shared options" in `Bolt.Sips.Internals.BoltProtocolHelper` documentation. ## Example iex> BoltProtocolV1.discard_all(:gen_tcp, port, 1, []) :ok """ @spec discard_all(atom(), port(), integer(), Keyword.t()) :: :ok | Bolt.Sips.Internals.Error.t() def discard_all(transport, port, bolt_version, options) do BoltProtocolHelper.treat_simple_message(:discard_all, transport, port, bolt_version, options) end @doc """ Implementation of Bolt's ACK_FAILURE. It acknowledges a failure while keeping transactions alive. See [http://boltprotocol.org/v1/#message-ack-failure](http://boltprotocol.org/v1/#message-ack-failure) ## Options See "Shared options" in `Bolt.Sips.Internals.BoltProtocolHelper` documentation. ## Example iex> BoltProtocolV1.ack_failure(:gen_tcp, port, 1, []) :ok """ @spec ack_failure(atom(), port(), integer(), Keyword.t()) :: :ok | Bolt.Sips.Internals.Error.t() def ack_failure(transport, port, bolt_version, options) do BoltProtocolHelper.treat_simple_message(:ack_failure, transport, port, bolt_version, options) end @doc """ Implementation of Bolt's RESET message. It resets a session to a "clean" state. See [http://boltprotocol.org/v1/#message-reset](http://boltprotocol.org/v1/#message-reset) ## Options See "Shared options" in `Bolt.Sips.Internals.BoltProtocolHelper` documentation. ## Example iex> BoltProtocolV1.reset(:gen_tcp, port, 1, []) :ok """ @spec reset(atom(), port(), integer(), Keyword.t()) :: :ok | Bolt.Sips.Internals.Error.t() def reset(transport, port, bolt_version, options) do BoltProtocolHelper.treat_simple_message(:reset, transport, port, bolt_version, options) end end ================================================ FILE: lib/bolt_sips/internals/bolt_protocol_v2.ex ================================================ defmodule Bolt.Sips.Internals.BoltProtocolV2 do @moduledoc false # There's no specific messagee for Bolt V2 # This file exists only to fill the gap between the 2 bolt protocol versions end ================================================ FILE: lib/bolt_sips/internals/bolt_protocol_v3.ex ================================================ defmodule Bolt.Sips.Internals.BoltProtocolV3 do alias Bolt.Sips.Internals.BoltProtocol alias Bolt.Sips.Internals.BoltProtocolHelper alias Bolt.Sips.Internals.Error @doc """ Implementation of Bolt's HELLO. It initialises the connection. Expects a transport module (i.e. `gen_tcp`) and a `Port`. Accepts authorisation params in the form of {username, password}. ## Options See "Shared options" in `Bolt.Sips.Internals.BoltProtocolHelper` documentation. ## Examples iex> Bolt.Sips.Internals.BoltProtocolV3.hello(:gen_tcp, port, 3, {}, []) {:ok, info} iex> Bolt.Sips.Internals.BoltProtocolV3.hello(:gen_tcp, port, 3, {"username", "password"}, []) {:ok, info} """ @spec hello(atom(), port(), integer(), tuple(), Keyword.t()) :: {:ok, any()} | {:error, Bolt.Sips.Internals.Error.t()} def hello(transport, port, bolt_version, auth, options \\ [recv_timeout: 15_000]) do BoltProtocolHelper.send_message(transport, port, bolt_version, {:hello, [auth]}) case BoltProtocolHelper.receive_data(transport, port, bolt_version, options) do {:success, info} -> {:ok, info} {:failure, response} -> {:error, Error.exception(response, port, :hello)} other -> {:error, Error.exception(other, port, :hello)} end end @doc """ Implementation of Bolt's RUN. It closes the connection. ## Options See "Shared options" in `Bolt.Sips.Internals.BoltProtocolHelper` documentation. ## Examples iex> Bolt.Sips.Internals.BoltProtocolV3.goodbye(:gen_tcp, port, 3) :ok iex> Bolt.Sips.Internals.BoltProtocolV3.goodbye(:gen_tcp, port, 3) :ok """ def goodbye(transport, port, bolt_version) do BoltProtocolHelper.send_message(transport, port, bolt_version, {:goodbye, []}) try do Port.close(port) :ok rescue ArgumentError -> Error.exception("Can't close port", port, :goodbye) end end @doc """ Implementation of Bolt's RUN. It passes a statement for execution on the server. Note that this message doesn't return the statement result. For this purpose, use PULL_ALL. In bolt >= 3, run has an additional paramters; metadata ## Options See "Shared options" in `Bolt.Sips.Internals.BoltProtocolHelper` documentation. ## Example iex> BoltProtocolV1.run(:gen_tcp, port, 1, "RETURN $num AS num", %{num: 5}, %{}, []) {:ok, {:success, %{"fields" => ["num"]}}} """ @spec run(atom(), port(), integer(), String.t(), map(), Bolt.Sips.Metadata.t(), Keyword.t()) :: {:ok, any()} | {:error, Bolt.Sips.Internals.Error.t()} def run(transport, port, bolt_version, statement, params, metadata, options) do BoltProtocolHelper.send_message( transport, port, bolt_version, {:run, [statement, params, metadata]} ) case BoltProtocolHelper.receive_data(transport, port, bolt_version, options) do {:success, _} = result -> {:ok, result} {:failure, response} -> {:error, Error.exception(response, port, :run)} %Error{} = error -> {:error, error} other -> {:error, Error.exception(other, port, :run)} end end @doc """ Runs a statement (most likely Cypher statement) and returns a list of the records and a summary (Act as as a RUN + PULL_ALL). Records are represented using PackStream's record data type. Their Elixir representation is a Keyword with the indexes `:sig` and `:fields`. ## Options See "Shared options" in `Bolt.Sips.Internals.BoltProtocolHelper` documentation. ## Examples iex> Bolt.Sips.Internals.BoltProtocol.run_statement(:gen_tcp, port, 1, "MATCH (n) RETURN n") [ {:success, %{"fields" => ["n"]}}, {:record, [sig: 1, fields: [1, "Example", "Labels", %{"some_attribute" => "some_value"}]]}, {:success, %{"type" => "r"}} ] """ @spec run_statement( atom(), port(), integer(), String.t(), map(), Bolt.Sips.Metadata.t(), Keyword.t() ) :: [ Bolt.Sips.Internals.PackStream.Message.decoded() ] | Bolt.Sips.Internals.Error.t() def run_statement(transport, port, bolt_version, statement, params, metadata, options) do with {:ok, run_data} <- run(transport, port, bolt_version, statement, params, metadata, options), {:ok, result} <- BoltProtocol.pull_all(transport, port, bolt_version, options) do [run_data | result] else {:error, %Error{} = error} -> error other -> Error.exception(other, port, :run_statement) end end @doc """ Implementation of Bolt's BEGIN. It opens a transaction. ## Options See "Shared options" in `Bolt.Sips.Internals.BoltProtocolHelper` documentation. ## Example iex> BoltProtocolV3.begin(:gen_tcp, port, 3, []) {:ok, metadata} """ @spec begin(atom(), port(), integer(), Bolt.Sips.Metadata.t() | map(), Keyword.t()) :: {:ok, any()} | Bolt.Sips.Internals.Error.t() def begin(transport, port, bolt_version, metadata, options) do BoltProtocolHelper.send_message(transport, port, bolt_version, {:begin, [metadata]}) case BoltProtocolHelper.receive_data(transport, port, bolt_version, options) do {:success, info} -> {:ok, info} {:failure, response} -> {:error, Error.exception(response, port, :begin)} other -> {:error, Error.exception(other, port, :begin)} end end @doc """ Implementation of Bolt's COMMIT. It commits the open transaction. ## Options See "Shared options" in `Bolt.Sips.Internals.BoltProtocolHelper` documentation. ## Example iex> BoltProtocolV3.commit(:gen_tcp, port, 3, []) :ok """ @spec commit(atom(), port(), integer(), Keyword.t()) :: {:ok, any()} | Bolt.Sips.Internals.Error.t() def commit(transport, port, bolt_version, options) do BoltProtocolHelper.send_message(transport, port, bolt_version, {:commit, []}) case BoltProtocolHelper.receive_data(transport, port, bolt_version, options) do {:success, info} -> {:ok, info} {:failure, response} -> {:error, Error.exception(response, port, :commit)} other -> {:error, Error.exception(other, port, :commit)} end end @doc """ Implementation of Bolt's ROLLBACK. It rollbacks the open transaction. ## Options See "Shared options" in `Bolt.Sips.Internals.BoltProtocolHelper` documentation. ## Example iex> BoltProtocolV3.rollback(:gen_tcp, port, 3, []) :ok """ @spec rollback(atom(), port(), integer(), Keyword.t()) :: :ok | Bolt.Sips.Internals.Error.t() def rollback(transport, port, bolt_version, options) do BoltProtocolHelper.treat_simple_message(:rollback, transport, port, bolt_version, options) end end ================================================ FILE: lib/bolt_sips/internals/bolt_version_helper.ex ================================================ defmodule Bolt.Sips.Internals.BoltVersionHelper do @moduledoc false @available_bolt_versions [1, 2, 3] @doc """ List bolt versions. Only bolt version that have specific encoding functions are listed. """ @spec available_versions() :: [integer()] def available_versions(), do: @available_bolt_versions @doc """ Retrieve previous valid version. Return nil if there is no previous version. ## Example iex> Bolt.Sips.Internals.BoltVersionHelper.previous(2) 1 iex> Bolt.Sips.Internals.BoltVersionHelper.previous(1) nil iex> Bolt.Sips.Internals.BoltVersionHelper.previous(15) 3 """ @spec previous(integer()) :: nil | integer() def previous(version) do @available_bolt_versions |> Enum.take_while(&(&1 < version)) |> List.last() end @doc """ Return the last available bolt version. ## Example: iex> Bolt.Sips.Internals.BoltVersionHelper.last() 3 """ def last() do List.last(@available_bolt_versions) end end ================================================ FILE: lib/bolt_sips/internals/error.ex ================================================ defmodule Bolt.Sips.Internals.Error do @moduledoc false defexception [:message, :code, :connection_id, :function, :type] @type t :: %__MODULE__{ message: String.t(), code: nil | any(), connection_id: nil | integer(), function: atom(), type: atom() } @doc false # Produce a Bolt.Sips.Internals.Error depending on the context. @spec exception(any(), nil | port(), atom()) :: Bolt.Sips.Internals.Error.t() def exception(%{"message" => message, "code" => code}, pid, function) do %Bolt.Sips.Internals.Error{ message: message, code: code, connection_id: get_id(pid), function: function, type: :cypher_error } end def exception({:error, :closed}, pid, function) do %Bolt.Sips.Internals.Error{ message: "Port #{inspect(pid)} is closed", connection_id: get_id(pid), function: function, type: :connection_error } end def exception({:failure, %Bolt.Sips.Internals.Error{message: _message, code: _code} = err}, _pid, _function) do err end def exception(%Bolt.Sips.Internals.Error{} = err, _pid, _function), do: err def exception(message, pid, function) do %Bolt.Sips.Internals.Error{ message: message_for(function, message), connection_id: get_id(pid), function: function, type: :protocol_error } end @spec message_for(nil | atom(), any()) :: String.t() defp message_for(:handshake, "HTTP") do """ Handshake failed. The port expected a HTTP request. This happens when trying to Neo4J using the REST API Port (default: 7474) instead of the Bolt Port (default: 7687). """ end defp message_for(:handshake, bin) when is_binary(bin) do """ Handshake failed. Expected 01:00:00:00 as a result, received: #{inspect(bin, base: :hex)}. """ end defp message_for(:handshake, other) do """ Handshake failed. Expected 01:00:00:00 as a result, received: #{inspect(other)}. """ end defp message_for(nil, message) do """ Unknown failure: #{inspect(message)} """ end defp message_for(_function, {:error, error}) do case error |> :inet.format_error() |> to_string do "unknown POSIX error" -> to_string(error) other -> other end end defp message_for(_function, {:ignored, []}) do """ The session is in a failed state and ignores further messages. You need to `ACK_FAILURE` or `RESET` in order to send new messages. """ end defp message_for(function, message) do """ #{function}: Unknown failure: #{inspect(message)} """ end @spec get_id(any()) :: nil | integer() defp get_id({:sslsocket, {:gen_tcp, port, _tls, _unused_yet}, _pid}) do get_id(port) end defp get_id(port) when is_port(port) do case Port.info(port, :id) do {:id, id} -> id nil -> nil end end defp get_id(_), do: nil end ================================================ FILE: lib/bolt_sips/internals/logger.ex ================================================ defmodule Bolt.Sips.Internals.Logger do @moduledoc false # Designed to log Bolt protocol message between Client and Server. # # The `from` parameter must be a atom, either `:client` or `:server` require Logger @doc """ Produces a formatted Log for a message ## Example iex> Logger.log_message(:client, {:init, []}) """ def log_message(from, {type, data}) do msg_type = type |> Atom.to_string() |> String.upcase() do_log_message(from, fn -> "#{msg_type} ~ #{inspect(data)}" end) end @doc """ Produces a formatted Log ## Example iex> Logger.log_message(:server, :handshake, 2) """ def log_message(from, type, data) do if Application.get_env(:bolt_sips, :log) do log_message(from, {type, data}) end end @doc """ Produces a formatted Log for a message Data will be output in hexadecimal ## Example iex> Logger.log_message(:server, :handshake, <<0x02>>) """ def log_message(from, type, data, :hex) do if Application.get_env(:bolt_sips, :log_hex, false) do msg_type = type |> Atom.to_string() |> String.upcase() do_log_message(from, fn -> "#{msg_type} ~ #{inspect(data, base: :hex, limit: :infinity)}" end) end end defp do_log_message(from, func) when is_function(func) do from_txt = case from do :server -> "S" :client -> "C" end Logger.debug(fn -> "#{from_txt}: #{func.()}" end) end end ================================================ FILE: lib/bolt_sips/internals/pack_stream/decoder.ex ================================================ defmodule Bolt.Sips.Internals.PackStream.Decoder do @moduledoc false _moduledoc = """ This module is responsible for dispatching decoding amongst decoder depending on the used bolt version. Most of the documentation regarding Bolt binary format can be found in `Bolt.Sips.Internals.PackStream.EncoderV1` and `Bolt.Sips.Internals.PackStream.EncoderV2`. Here will be found ocumenation about data that are only availalbe for decoding:: - Node - Relationship - Unbound relationship - Path """ use Bolt.Sips.Internals.PackStream.DecoderImplV1 use Bolt.Sips.Internals.PackStream.DecoderImplV2 use Bolt.Sips.Internals.PackStream.DecoderUtils end ================================================ FILE: lib/bolt_sips/internals/pack_stream/decoder_impl_v1.ex ================================================ defmodule Bolt.Sips.Internals.PackStream.DecoderImplV1 do alias Bolt.Sips.Types defmacro __using__(_options) do quote do import unquote(__MODULE__) @last_version Bolt.Sips.Internals.BoltVersionHelper.last() # Null @null_marker 0xC0 # Boolean @true_marker 0xC3 @false_marker 0xC2 # String @tiny_bitstring_marker 0x8 @bitstring8_marker 0xD0 @bitstring16_marker 0xD1 @bitstring32_marker 0xD2 # Integer @int8_marker 0xC8 @int16_marker 0xC9 @int32_marker 0xCA @int64_marker 0xCB # Float @float_marker 0xC1 # List @tiny_list_marker 0x9 @list8_marker 0xD4 @list16_marker 0xD5 @list32_marker 0xD6 # Map @tiny_map_marker 0xA @map8_marker 0xD8 @map16_marker 0xD9 @map32_marker 0xDA # Structure @tiny_struct_marker 0xB @struct8_marker 0xDC @struct16_marker 0xDD # Node @node_marker 0x4E # Relationship @relationship_marker 0x52 # Unbounded relationship @unbounded_relationship_marker 0x72 # Path @path_marker 0x50 @spec decode(binary() | {integer(), binary(), integer()}, integer()) :: list() | {:error, :not_implemented} def decode(<<@null_marker, rest::binary>>, bolt_version) when bolt_version <= @last_version do [nil | decode(rest, bolt_version)] end # Boolean def decode(<<@true_marker, rest::binary>>, bolt_version) when bolt_version <= @last_version do [true | decode(rest, bolt_version)] end def decode(<<@false_marker, rest::binary>>, bolt_version) when bolt_version <= @last_version do [false | decode(rest, bolt_version)] end # Float def decode(<<@float_marker, number::float, rest::binary>>, bolt_version) when bolt_version <= @last_version do [number | decode(rest, bolt_version)] end # Strings def decode(<<@tiny_bitstring_marker::4, str_length::4, rest::bytes>>, bolt_version) when bolt_version <= @last_version do decode_string(rest, str_length, bolt_version) end def decode(<<@bitstring8_marker, str_length, rest::bytes>>, bolt_version) when bolt_version <= @last_version do decode_string(rest, str_length, bolt_version) end def decode(<<@bitstring16_marker, str_length::16, rest::bytes>>, bolt_version) when bolt_version <= @last_version do decode_string(rest, str_length, bolt_version) end def decode(<<@bitstring32_marker, str_length::32, rest::binary>>, bolt_version) when bolt_version <= @last_version do decode_string(rest, str_length, bolt_version) end # Lists def decode(<<@tiny_list_marker::4, list_size::4>> <> bin, bolt_version) when bolt_version <= @last_version do decode_list(bin, list_size, bolt_version) end def decode(<<@list8_marker, list_size::8>> <> bin, bolt_version) when bolt_version <= @last_version do decode_list(bin, list_size, bolt_version) end def decode(<<@list16_marker, list_size::16>> <> bin, bolt_version) when bolt_version <= @last_version do decode_list(bin, list_size, bolt_version) end def decode(<<@list32_marker, list_size::32>> <> bin, bolt_version) when bolt_version <= @last_version do decode_list(bin, list_size, bolt_version) end # Maps def decode(<<@tiny_map_marker::4, entries::4>> <> bin, bolt_version) when bolt_version <= @last_version do decode_map(bin, entries, bolt_version) end def decode(<<@map8_marker, entries::8>> <> bin, bolt_version) when bolt_version <= @last_version do decode_map(bin, entries, bolt_version) end def decode(<<@map16_marker, entries::16>> <> bin, bolt_version) when bolt_version <= @last_version do decode_map(bin, entries, bolt_version) end def decode(<<@map32_marker, entries::32>> <> bin, bolt_version) when bolt_version <= @last_version do decode_map(bin, entries, bolt_version) end # Struct def decode(<<@tiny_struct_marker::4, struct_size::4, sig::8>> <> struct, bolt_version) when bolt_version <= @last_version do decode({sig, struct, struct_size}, bolt_version) end def decode(<<@struct8_marker, struct_size::8, sig::8>> <> struct, bolt_version) when bolt_version <= @last_version do decode({sig, struct, struct_size}, bolt_version) end def decode(<<@struct16_marker, struct_size::16, sig::8>> <> struct, bolt_version) when bolt_version <= @last_version do decode({sig, struct, struct_size}, bolt_version) end ######### SPECIAL STRUCTS # Node def decode({@node_marker, struct, struct_size}, bolt_version) when bolt_version <= @last_version do {[id, labels, props], rest} = decode_struct(struct, struct_size, bolt_version) node = %Types.Node{id: id, labels: labels, properties: props} [node | rest] end # Relationship def decode({@relationship_marker, struct, struct_size}, bolt_version) when bolt_version <= @last_version do {[id, start_node, end_node, type, props], rest} = decode_struct(struct, struct_size, bolt_version) relationship = %Types.Relationship{ id: id, start: start_node, end: end_node, type: type, properties: props } [relationship | rest] end # UnboundedRelationship def decode({@unbounded_relationship_marker, struct, struct_size}, bolt_version) when bolt_version <= @last_version do {[id, type, props], rest} = decode_struct(struct, struct_size, bolt_version) unbounded_relationship = %Types.UnboundRelationship{ id: id, type: type, properties: props } [unbounded_relationship | rest] end # Path def decode({@path_marker, struct, struct_size}, bolt_version) when bolt_version <= @last_version do {[nodes, relationships, sequence], rest} = decode_struct(struct, struct_size, bolt_version) path = %Types.Path{ nodes: nodes, relationships: relationships, sequence: sequence } [path | rest] end # Manage the end of data def decode("", bolt_version) when bolt_version <= @last_version do [] end # Integers def decode(<<@int8_marker, int::signed-integer, rest::binary>>, bolt_version) when bolt_version <= @last_version do [int | decode(rest, bolt_version)] end def decode(<<@int16_marker, int::signed-integer-16, rest::binary>>, bolt_version) when bolt_version <= @last_version do [int | decode(rest, bolt_version)] end def decode(<<@int32_marker, int::signed-integer-32, rest::binary>>, bolt_version) when bolt_version <= @last_version do [int | decode(rest, bolt_version)] end def decode(<<@int64_marker, int::signed-integer-64, rest::binary>>, bolt_version) when bolt_version <= @last_version do [int | decode(rest, bolt_version)] end def decode(<>, bolt_version) when bolt_version <= @last_version do [int | decode(rest, bolt_version)] end end end end ================================================ FILE: lib/bolt_sips/internals/pack_stream/decoder_impl_v2.ex ================================================ defmodule Bolt.Sips.Internals.PackStream.DecoderImplV2 do alias Bolt.Sips.Types.{TimeWithTZOffset, DateTimeWithTZOffset, Duration, Point} defmacro __using__(_options) do quote do import unquote(__MODULE__) @last_version Bolt.Sips.Internals.BoltVersionHelper.last() # Null @null_marker 0xC0 # Boolean @true_marker 0xC3 @false_marker 0xC2 # String @tiny_bitstring_marker 0x8 @bitstring8_marker 0xD0 @bitstring16_marker 0xD1 @bitstring32_marker 0xD2 # Integer @int8_marker 0xC8 @int16_marker 0xC9 @int32_marker 0xCA @int64_marker 0xCB # Float @float_marker 0xC1 # List @tiny_list_marker 0x9 @list8_marker 0xD4 @list16_marker 0xD5 @list32_marker 0xD6 # Map @tiny_map_marker 0xA @map8_marker 0xD8 @map16_marker 0xD9 @map32_marker 0xDA # Structure @tiny_struct_marker 0xB @struct8_marker 0xDC @struct16_marker 0xDD # Node @node_marker 0x4E # Relationship @relationship_marker 0x52 # Unbounded relationship @unbounded_relationship_marker 0x72 # Path @path_marker 0x50 # Local Time @local_time_signature 0x74 @local_time_struct_size 1 # Time With TZ Offset @time_with_tz_signature 0x54 @time_with_tz_struct_size 2 # Date @date_signature 0x44 @date_struct_size 1 # Local DateTime @local_datetime_signature 0x64 @local_datetime_struct_size 2 # Datetime with TZ offset @datetime_with_zone_offset_signature 0x46 @datetime_with_zone_offset_struct_size 3 # Datetime with TZ id @datetime_with_zone_id_signature 0x66 @datetime_with_zone_id_struct_size 3 # Duration @duration_signature 0x45 @duration_struct_size 4 # Point 2D @point2d_signature 0x58 @point2d_struct_size 3 # Point 3D @point3d_signature 0x59 @point3d_struct_size 4 # Local Date def decode({@date_signature, struct, @date_struct_size}, bolt_version) when bolt_version >= 2 and bolt_version <= @last_version do {[date], rest} = decode_struct(struct, @date_struct_size, bolt_version) [Date.add(~D[1970-01-01], date) | rest] end # Local Time def decode({@local_time_signature, struct, @local_time_struct_size}, bolt_version) when bolt_version >= 2 and bolt_version <= @last_version do {[time], rest} = decode_struct(struct, @local_time_struct_size, bolt_version) [Time.add(~T[00:00:00.000000], time, :nanosecond) | rest] end # Local DateTime def decode({@local_datetime_signature, struct, @local_datetime_struct_size}, bolt_version) when bolt_version >= 2 and bolt_version <= @last_version do {[seconds, nanoseconds], rest} = decode_struct(struct, @local_datetime_struct_size, bolt_version) ndt = NaiveDateTime.add( ~N[1970-01-01 00:00:00.000000000], seconds * 1_000_000_000 + nanoseconds, :nanosecond ) [ndt | rest] end # Time with Zone Offset def decode({@time_with_tz_signature, struct, @time_with_tz_struct_size}, bolt_version) when bolt_version >= 2 and bolt_version <= @last_version do {[time, offset], rest} = decode_struct(struct, @time_with_tz_struct_size, bolt_version) t = TimeWithTZOffset.create(Time.add(~T[00:00:00.000000], time, :nanosecond), offset) [t | rest] end # Datetime with zone Id def decode( {@datetime_with_zone_id_signature, struct, @datetime_with_zone_id_struct_size}, bolt_version ) when bolt_version >= 2 and bolt_version <= @last_version do {[seconds, nanoseconds, zone_id], rest} = decode_struct(struct, @datetime_with_zone_id_struct_size, bolt_version) naive_dt = NaiveDateTime.add( ~N[1970-01-01 00:00:00.000000], seconds * 1_000_000_000 + nanoseconds, :nanosecond ) dt = Bolt.Sips.TypesHelper.datetime_with_micro(naive_dt, zone_id) [dt | rest] end # Datetime with zone offset def decode( {@datetime_with_zone_offset_signature, struct, @datetime_with_zone_offset_struct_size}, bolt_version ) when bolt_version >= 2 and bolt_version <= @last_version do {[seconds, nanoseconds, zone_offset], rest} = decode_struct(struct, @datetime_with_zone_id_struct_size, bolt_version) naive_dt = NaiveDateTime.add( ~N[1970-01-01 00:00:00.000000], seconds * 1_000_000_000 + nanoseconds, :nanosecond ) dt = DateTimeWithTZOffset.create(naive_dt, zone_offset) [dt | rest] end # Duration def decode({@duration_signature, struct, @duration_struct_size}, bolt_version) when bolt_version >= 2 and bolt_version <= @last_version do {[months, days, seconds, nanoseconds], rest} = decode_struct(struct, @duration_struct_size, bolt_version) duration = Duration.create(months, days, seconds, nanoseconds) [duration | rest] end # Point2D def decode({@point2d_signature, struct, @point2d_struct_size}, bolt_version) when bolt_version >= 2 and bolt_version <= @last_version do {[srid, x, y], rest} = decode_struct(struct, @point2d_struct_size, bolt_version) point = Point.create(srid, x, y) [point | rest] end # Point3D def decode({@point3d_signature, struct, @point3d_struct_size}, bolt_version) when bolt_version >= 2 and bolt_version <= @last_version do {[srid, x, y, z], rest} = decode_struct(struct, @point3d_struct_size, bolt_version) point = Point.create(srid, x, y, z) [point | rest] end end end end ================================================ FILE: lib/bolt_sips/internals/pack_stream/decoder_utils.ex ================================================ defmodule Bolt.Sips.Internals.PackStream.DecoderUtils do alias Bolt.Sips.Internals.PackStreamError defmacro __using__(_options) do quote do import unquote(__MODULE__) @last_version Bolt.Sips.Internals.BoltVersionHelper.last() def decode(data, bolt_version) when is_integer(bolt_version) do if bolt_version > @last_version do decode(data, @last_version) else raise PackStreamError, data: data, bolt_version: bolt_version, message: "Unsupported decoder version" end end def decode(_, _) do {:error, :not_implemented} end @doc """ Decodes a struct """ @spec decode_struct(binary(), integer(), integer()) :: {list(), list()} def decode_struct(struct, struct_size, bolt_version) do struct |> decode(bolt_version) |> Enum.split(struct_size) end @spec to_map(list()) :: map() defp to_map(map) do map |> Enum.chunk_every(2) |> Enum.map(&List.to_tuple/1) |> Map.new() end @spec decode_string(binary(), integer(), integer()) :: list() defp decode_string(bytes, str_length, bolt_version) do <> = bytes [string | decode(rest, bolt_version)] end @spec decode_list(binary(), integer(), integer()) :: list() defp decode_list(list, list_size, bolt_version) do {list, rest} = list |> decode(bolt_version) |> Enum.split(list_size) [list | rest] end @spec decode_map(binary(), integer(), integer()) :: list() defp decode_map(map, entries, bolt_version) do {map, rest} = map |> decode(bolt_version) |> Enum.split(entries * 2) [to_map(map) | rest] end end end end ================================================ FILE: lib/bolt_sips/internals/pack_stream/decoder_v1.ex ================================================ defmodule Bolt.Sips.Internals.PackStream.DecoderV1 do @moduledoc false _moduledoc = """ Bolt V1 can decode: - Null - Boolean - Integer - Float - String - List - Map - Struct Functions from this module are not meant to be used directly. Use `Decoder.decode(data, bolt_version)` for all decoding purposes. """ use Bolt.Sips.Internals.PackStream.Markers alias Bolt.Sips.Internals.PackStream.Decoder @spec decode(binary() | {integer(), binary(), integer()}, integer()) :: list() | {:error, :not_implemented} def decode(data, bolt_version), do: Decoder.decode(data, bolt_version) end ================================================ FILE: lib/bolt_sips/internals/pack_stream/decoder_v2.ex ================================================ defmodule Bolt.Sips.Internals.PackStream.DecoderV2 do @moduledoc false _module_doc = """ Bolt V2 has specification for decoding: - Temporal types: - Local Date - Local Time - Local DateTime - Time with Timezone Offset - DateTime with Timezone Id - DateTime with Timezone Offset - Duration - Spatial types: - Point2D - Point3D For documentation about those typs representation in Bolt binary, please see `Bolt.Sips.Internals.PackStream.EncoderV2`. Functions from this module are not meant to be used directly. Use `Decoder.decode(data, bolt_version)` for all decoding purposes. """ use Bolt.Sips.Internals.PackStream.Markers alias Bolt.Sips.Internals.PackStream.Decoder # Local Date @spec decode({integer(), binary(), integer()}, integer()) :: list() | {:error, :not_implemented} def decode(data, bolt_version), do: Decoder.decode(data, bolt_version) end ================================================ FILE: lib/bolt_sips/internals/pack_stream/decoder_v3.ex ================================================ defmodule Bolt.Sips.Internals.PackStream.DecoderV3 do def decode(_, _) do {:error, :not_implemented} end end ================================================ FILE: lib/bolt_sips/internals/pack_stream/encoder.ex ================================================ alias Bolt.Sips.Internals.PackStream alias Bolt.Sips.Internals.PackStream.EncoderHelper defprotocol Bolt.Sips.Internals.PackStream.Encoder do @moduledoc false # Encodes an item to its binary PackStream Representation # # Implementation exists for following types: # - Integer # - Float # - List # - Map # - Struct (defined in the Bolt protocol) @fallback_to_any true @doc """ Encode entity into its Bolt binary represenation depending of the used bolt version """ @spec encode(any(), integer()) :: binary() def encode(entity, bolt_version) end defimpl PackStream.Encoder, for: Atom do def encode(data, bolt_version), do: EncoderHelper.call_encode(:atom, data, bolt_version) end defimpl PackStream.Encoder, for: BitString do def encode(data, bolt_version), do: EncoderHelper.call_encode(:string, data, bolt_version) end defimpl PackStream.Encoder, for: Integer do def encode(data, bolt_version), do: EncoderHelper.call_encode(:integer, data, bolt_version) end defimpl PackStream.Encoder, for: Float do def encode(data, bolt_version), do: EncoderHelper.call_encode(:float, data, bolt_version) end defimpl PackStream.Encoder, for: List do def encode(data, bolt_version), do: EncoderHelper.call_encode(:list, data, bolt_version) end defimpl PackStream.Encoder, for: Map do def encode(data, bolt_version), do: EncoderHelper.call_encode(:map, data, bolt_version) end defimpl PackStream.Encoder, for: Time do def encode(data, bolt_version), do: EncoderHelper.call_encode(:local_time, data, bolt_version) end defimpl PackStream.Encoder, for: Bolt.Sips.Types.TimeWithTZOffset do def encode(data, bolt_version) do EncoderHelper.call_encode(:time_with_tz, data, bolt_version) end end defimpl PackStream.Encoder, for: Date do def encode(data, bolt_version), do: EncoderHelper.call_encode(:date, data, bolt_version) end defimpl PackStream.Encoder, for: NaiveDateTime do def encode(data, bolt_version) do EncoderHelper.call_encode(:local_datetime, data, bolt_version) end end defimpl PackStream.Encoder, for: DateTime do def encode(data, version) do EncoderHelper.call_encode(:datetime_with_tz_id, data, version) end end defimpl PackStream.Encoder, for: Bolt.Sips.Types.DateTimeWithTZOffset do def encode(data, version) do EncoderHelper.call_encode(:datetime_with_tz_offset, data, version) end end defimpl PackStream.Encoder, for: Bolt.Sips.Types.Duration do def encode(data, version), do: EncoderHelper.call_encode(:duration, data, version) end defimpl PackStream.Encoder, for: Bolt.Sips.Types.Point do def encode(data, version), do: EncoderHelper.call_encode(:point, data, version) end defimpl PackStream.Encoder, for: Any do @spec encode({integer(), list()} | %{:__struct__ => String.t()}, integer()) :: Bolt.Sips.Internals.PackStream.value() | <<_::16, _::_*8>> def encode({signature, data}, bolt_version) when is_list(data) do valid_signatures = PackStream.Message.Encoder.valid_signatures(bolt_version) ++ Bolt.Sips.Internals.PackStream.MarkersHelper.valid_signatures() if signature in valid_signatures do EncoderHelper.call_encode(:struct, {signature, data}, bolt_version) else raise Bolt.Sips.Internals.PackStreamError, message: "Unable to encode", data: data, bolt_version: bolt_version end end # Elixir structs just need to be convertedd to map befoare being encoded def encode(%{__struct__: _} = data, bolt_version) do map = Map.from_struct(data) PackStream.Encoder.encode(map, bolt_version) end def encode(data, bolt_version) do raise Bolt.Sips.Internals.PackStreamError, message: "Unable to encode", data: data, bolt_version: bolt_version end end ================================================ FILE: lib/bolt_sips/internals/pack_stream/encoder_helper.ex ================================================ defmodule Bolt.Sips.Internals.PackStream.EncoderHelper do @moduledoc false alias Bolt.Sips.Internals.BoltVersionHelper alias Bolt.Sips.Internals.PackStreamError use Bolt.Sips.Internals.PackStream.V1 use Bolt.Sips.Internals.PackStream.V2 use Bolt.Sips.Internals.PackStream.Utils @available_bolt_versions BoltVersionHelper.available_versions() @last_version BoltVersionHelper.last() @doc """ For the given `data_type` and `bolt_version`, determine the right enconding function and call it agains `data` """ @spec call_encode(atom(), any(), any()) :: binary() | PackStreamError.t() def call_encode(data_type, data, bolt_version) when is_integer(bolt_version) and bolt_version in @available_bolt_versions do do_call_encode(data_type, data, bolt_version) end def call_encode(data_type, data, bolt_version) when is_integer(bolt_version) do if bolt_version > @last_version do call_encode(data_type, data, @last_version) else raise PackStreamError, data_type: data_type, data: data, bolt_version: bolt_version, message: "Unsupported encoder version" end end def call_encode(data_type, data, bolt_version) do raise PackStreamError, data_type: data_type, data: data, bolt_version: bolt_version, message: "Unsupported encoder version" end end ================================================ FILE: lib/bolt_sips/internals/pack_stream/encoder_v1.ex ================================================ defmodule Bolt.Sips.Internals.PackStream.EncoderV1 do @moduledoc false alias Bolt.Sips.Internals.PackStream.EncoderHelper use Bolt.Sips.Internals.PackStream.Markers @doc """ Encode an atom into Bolt binary format. Encoding: `Marker` with | Value | Marker | | ------- | -------- | | nil | `0xC0` | | false | `0xC2` | | true | `0xC3` | Other atoms are converted to string before encoding. ## Example iex> alias Bolt.Sips.Internals.PackStream.EncoderV1 iex> :erlang.iolist_to_binary(EncoderV1.encode_atom(nil, 1)) <<0xC0>> iex> :erlang.iolist_to_binary(EncoderV1.encode_atom(true, 1)) <<0xC3>> iex> :erlang.iolist_to_binary(EncoderV1.encode_atom(:guten_tag, 1)) <<0x89, 0x67, 0x75, 0x74, 0x65, 0x6E, 0x5F, 0x74, 0x61, 0x67>> """ @spec encode_atom(atom(), integer()) :: Bolt.Sips.Internals.PackStream.value() def encode_atom(atom , bolt_version), do: EncoderHelper.call_encode(:atom, atom, bolt_version) @doc """ Encode a string into Bolt binary format. Encoding: `Marker` `Size` `Content` with | Marker | Size | Max data size | |--------|------|---------------| | `0x80`..`0x8F` | None (contained in marker) | 15 bytes | | `0xD0` | 8-bit integer | 255 bytes | | `0xD1` | 16-bit integer | 65_535 bytes | | `0xD2` | 32-bit integer | 4_294_967_295 bytes | ## Example iex> alias Bolt.Sips.Internals.PackStream.EncoderV1 iex> :erlang.iolist_to_binary(EncoderV1.encode_string("guten tag", 1)) <<0x89, 0x67, 0x75, 0x74, 0x65, 0x6E, 0x20, 0x74, 0x61, 0x67>> """ @spec encode_string(String.t(), integer()) :: Bolt.Sips.Internals.PackStream.value() def encode_string(string, bolt_version), do: EncoderHelper.call_encode(:string, string, bolt_version) @doc """ Encode an integer into Bolt binary format. Encoding: `Marker` `Value` with | | Marker | |---|--------| | tiny int | `0x2A` | | int8 | `0xC8` | | int16 | `0xC9` | | int32 | `0xCA` | | int64 | `0xCB` | ## Example iex> alias Bolt.Sips.Internals.PackStream.EncoderV1 iex> :erlang.iolist_to_binary(EncoderV1.encode_integer(74, 1)) <<0x4A>> iex> :erlang.iolist_to_binary(EncoderV1.encode_integer(-74_789, 1)) <<0xCA, 0xFF, 0xFE, 0xDB, 0xDB>> """ @spec encode_integer(integer(), integer()) :: Bolt.Sips.Internals.PackStream.value() def encode_integer(integer, bolt_version), do: EncoderHelper.call_encode(:integer, integer, bolt_version) @doc """ Encode a float into Bolt binary format. Encoding: `Marker` `8 byte Content`. Marker: `0xC1` Formated according to the IEEE 754 floating-point "double format" bit layout. ## Example iex> alias Bolt.Sips.Internals.PackStream.EncoderV1 iex> :erlang.iolist_to_binary(EncoderV1.encode_float(42.42, 1)) <<0xC1, 0x40, 0x45, 0x35, 0xC2, 0x8F, 0x5C, 0x28, 0xF6>> """ @spec encode_float(float(), integer()) :: Bolt.Sips.Internals.PackStream.value() def encode_float(number, bolt_version), do: EncoderHelper.call_encode(:float, number, bolt_version) @doc """ Encode a list into Bolt binary format. Encoding: `Marker` `Size` `Content` with | Marker | Size | Max list size | |--------|------|---------------| | `0x90`..`0x9F` | None (contained in marker) | 15 bytes | | `0xD4` | 8-bit integer | 255 items | | `0xD5` | 16-bit integer | 65_535 items | | `0xD6` | 32-bit integer | 4_294_967_295 items | ## Example iex> alias Bolt.Sips.Internals.PackStream.EncoderV1 iex> :erlang.iolist_to_binary(EncoderV1.encode_list(["hello", "world"], 1)) <<0x92, 0x85, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x85, 0x77, 0x6F, 0x72, 0x6C, 0x64>> """ @spec encode_list(list(), integer()) :: Bolt.Sips.Internals.PackStream.value() def encode_list(list, bolt_version), do: EncoderHelper.call_encode(:list, list, bolt_version) @doc """ Encode a map into Bolt binary format. Note that Elixir structs are converted to map for encoding purpose. Encoding: `Marker` `Size` `Content` with | Marker | Size | Max map size | |--------|------|---------------| | `0xA0`..`0xAF` | None (contained in marker) | 15 entries | | `0xD8` | 8-bit integer | 255 entries | | `0xD9` | 16-bit integer | 65_535 entries | | `0xDA` | 32-bit integer | 4_294_967_295 entries | ## Example iex> alias Bolt.Sips.Internals.PackStream.EncoderV1 iex> :erlang.iolist_to_binary(EncoderV1.encode_map(%{id: 345, value: "hello world"}, 1)) <<0xA2, 0x82, 0x69, 0x64, 0xC9, 0x1, 0x59, 0x85, 0x76, 0x61, 0x6C, 0x75, 0x65, 0x8B, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64>> """ @spec encode_map(map(), integer()) :: Bolt.Sips.Internals.PackStream.value() def encode_map(map, bolt_version), do: EncoderHelper.call_encode(:map, map, bolt_version) @doc """ Encode a struct into Bolt binary format. This concerns Bolt Structs as defined in [](). Elixir structs are just converted to regular maps before encoding Encoding: `Marker` `Size` `Signature` `Content` with | Marker | Size | Max structure size | |--------|------|---------------| | `0xB0`..`0xBF` | None (contained in marker) | 15 fields | | `0xDC` | 8-bit integer | 255 fields | | `0xDD` | 16-bit integer | 65_535 fields | ## Example iex> alias Bolt.Sips.Internals.PackStream.EncoderV1 iex> :erlang.iolist_to_binary(EncoderV1.encode_struct({0x01, ["two", "params"]}, 1)) <<0xB2, 0x1, 0x83, 0x74, 0x77, 0x6F, 0x86, 0x70, 0x61, 0x72, 0x61, 0x6D, 0x73>> """ @spec encode_struct({integer(), list()}, integer()) :: Bolt.Sips.Internals.PackStream.value() def encode_struct(struct, bolt_version) , do: EncoderHelper.call_encode(:struct, struct, bolt_version) end ================================================ FILE: lib/bolt_sips/internals/pack_stream/encoder_v2.ex ================================================ defmodule Bolt.Sips.Internals.PackStream.EncoderV2 do @moduledoc false use Bolt.Sips.Internals.PackStream.Markers alias Bolt.Sips.Internals.PackStream.EncoderHelper alias Bolt.Sips.Types.{TimeWithTZOffset, DateTimeWithTZOffset, Duration, Point} @doc """ Encode a Time (represented by Time) into Bolt binary format. Encoded in a structure. Signature: `0x74` Encoding: `Marker` `Size` `Signature` ` Content` where `Content` is: `Nanoseconds_from_00:00:00` ## Example iex> :erlang.iolist_to_binary(Bolt.Sips.Internals.PackStream.EncoderV2.encode_local_time(~T[06:54:32.453], 2)) <<0xB1, 0x74, 0xCB, 0x0, 0x0, 0x16, 0x9F, 0x11, 0xB9, 0xCB, 0x40>> """ @spec encode_local_time(Time.t(), integer()) :: Bolt.Sips.Internals.PackStream.value() def encode_local_time(local_time, bolt_version), do: EncoderHelper.call_encode(:local_time, local_time, bolt_version) @doc """ Encode a TIME WITH TIMEZONE OFFSET (represented by TimeWithTZOffset) into Bolt binary format. Encoded in a structure. Signature: `0x54` Encoding: `Marker` `Size` `Signature` ` Content` where `Content` is: `Nanoseconds_from_00:00:00` `Offset_in_seconds` ## Example iex> time_with_tz = Bolt.Sips.Types.TimeWithTZOffset.create(~T[06:54:32.453], 3600) iex> :erlang.iolist_to_binary(Bolt.Sips.Internals.PackStream.EncoderV2.encode_time_with_tz(time_with_tz, 2)) <<0xB2, 0x54, 0xCB, 0x0, 0x0, 0x16, 0x9F, 0x11, 0xB9, 0xCB, 0x40, 0xC9, 0xE, 0x10>> """ def encode_time_with_tz(%TimeWithTZOffset{time: time, timezone_offset: offset}, bolt_version), do: EncoderHelper.call_encode(:time_with_tz, %TimeWithTZOffset{time: time, timezone_offset: offset}, bolt_version ) @doc """ Encode a DATE (represented by Date) into Bolt binary format. Encoded in a structure. Signature: `0x44` Encoding: `Marker` `Size` `Signature` ` Content` where `Content` is: `Nb_days_since_epoch` ## Example iex> :erlang.iolist_to_binary(Bolt.Sips.Internals.PackStream.EncoderV2.encode_date(~D[2019-04-23], 2)) <<0xB1, 0x44, 0xC9, 0x46, 0x59>> """ @spec encode_date(Date.t(), integer()) :: Bolt.Sips.Internals.PackStream.value() def encode_date(date, bolt_version), do: EncoderHelper.call_encode(:date, date, bolt_version) @doc """ Encode a LOCAL DATETIME (Represented by NaiveDateTime) into Bolt binary format. Encoded in a structure. WARNING: Nanoseconds are left off as NaiveDateTime doesn't handle them. A new Calendar should be implemented to manage them. Signature: `0x64` Encoding: `Marker` `Size` `Signature` ` Content` where `Content` is: `Nb_seconds_since_epoch` `Remainder_in_nanoseconds` ## Example iex> :erlang.iolist_to_binary(Bolt.Sips.Internals.PackStream.EncoderV2.encode_local_datetime(~N[2019-04-23 13:45:52.678], 2)) <<0xB2, 0x64, 0xCA, 0x5C, 0xBF, 0x17, 0x10, 0xCA, 0x28, 0x69, 0x75, 0x80>> """ @spec encode_local_datetime(Calendar.naive_datetime(), integer()) :: Bolt.Sips.Internals.PackStream.value() def encode_local_datetime(local_datetime, bolt_version), do: EncoderHelper.call_encode(:local_datetime, local_datetime, bolt_version) @doc """ Encode DATETIME WITH TIMEZONE ID (represented by Calendar.DateTime) into Bolt binary format. Encoded in a structure. WARNING: Nanoseconds are left off as NaiveDateTime doesn't handle them. A new Calendar should be implemented to manage them. Signature: `0x66` Encoding: `Marker` `Size` `Signature` ` Content` where `Content` is: `Nb_seconds_since_epoch` `Remainder_in_nanoseconds` `Zone_id` ## Example iex> d = Bolt.Sips.TypesHelper.datetime_with_micro(~N[2013-11-12 07:32:02.003], ...> "Europe/Berlin") #DateTime<2013-11-12 07:32:02.003+01:00 CET Europe/Berlin> iex> :erlang.iolist_to_binary(Bolt.Sips.Internals.PackStream.EncoderV2.encode_datetime_with_tz_id(d, 2)) <<0xB3, 0x66, 0xCA, 0x52, 0x81, 0xD9, 0x72, 0xCA, 0x0, 0x2D, 0xC6, 0xC0, 0x8D, 0x45, 0x75, 0x72, 0x6F, 0x70, 0x65, 0x2F, 0x42, 0x65, 0x72, 0x6C, 0x69, 0x6E>> """ @spec encode_datetime_with_tz_id(Calendar.datetime(), integer()) :: Bolt.Sips.Internals.PackStream.value() def encode_datetime_with_tz_id(datetime, bolt_version), do: EncoderHelper.call_encode(:datetime_with_tz_id, datetime, bolt_version) @doc """ Encode DATETIME WITH TIMEZONE OFFSET (represented by DateTimeWithTZOffset) into Bolt binary format. Encoded in a structure. WARNING: Nanoseconds are left off as NaiveDateTime doesn't handle them. A new Calendar should be implemented to manage them. Signature: `0x46` Encoding: `Marker` `Size` `Signature` ` Content` where `Content` is: `Nb_seconds_since_epoch` `Remainder_in_nanoseconds` `Zone_offset` ## Example iex> d = Bolt.Sips.Types.DateTimeWithTZOffset.create(~N[2013-11-12 07:32:02.003], 7200) %Bolt.Sips.Types.DateTimeWithTZOffset{ naive_datetime: ~N[2013-11-12 07:32:02.003], timezone_offset: 7200 } iex> :erlang.iolist_to_binary(Bolt.Sips.Internals.PackStream.EncoderV2.encode_datetime_with_tz_offset(d, 2)) <<0xB3, 0x46, 0xCA, 0x52, 0x81, 0xD9, 0x72, 0xCA, 0x0, 0x2D, 0xC6, 0xC0, 0xC9, 0x1C, 0x20>> """ @spec encode_datetime_with_tz_offset(DateTimeWithTZOffset.t(), integer()) :: Bolt.Sips.Internals.PackStream.value() def encode_datetime_with_tz_offset( %DateTimeWithTZOffset{naive_datetime: ndt, timezone_offset: tz_offset}, bolt_version ), do: EncoderHelper.call_encode(:datetime_with_tz_offset, %DateTimeWithTZOffset{naive_datetime: ndt, timezone_offset: tz_offset}, bolt_version ) @doc """ Encode DURATION (represented by Duration) into Bolt binary format. Encoded in a structure. Signature: `0x45` Encoding: `Marker` `Size` `Signature` ` Content` where `Content` is: `Months` `Days` `Seconds` `Nanoseconds` ## Example iex(60)> d = %Bolt.Sips.Types.Duration{ ...(60)> years: 3, ...(60)> months: 1, ...(60)> weeks: 7, ...(60)> days: 4, ...(60)> hours: 13, ...(60)> minutes: 2, ...(60)> seconds: 21, ...(60)> nanoseconds: 554 ...(60)> } %Bolt.Sips.Types.Duration{ days: 4, hours: 13, minutes: 2, months: 1, nanoseconds: 554, seconds: 21, weeks: 7, years: 3 } iex> :erlang.iolist_to_binary(Bolt.Sips.Internals.PackStream.EncoderV2.encode_duration(d, 2)) <<0xB4, 0x45, 0x25, 0x35, 0xCA, 0x0, 0x0, 0xB7, 0x5D, 0xC9, 0x2, 0x2A>> """ @spec encode_duration(Duration.t(), integer()) :: Bolt.Sips.Internals.PackStream.value() def encode_duration(%Duration{} = duration, bolt_version), do: EncoderHelper.call_encode(:duration, duration, bolt_version) @doc """ Encode POINT 2D & 3D (represented by Point) into Bolt binary format. Encoded in a structure. ## Point 2D Signature: `0x58` Encoding: `Marker` `Size` `Signature` ` Content` where `Content` is: `SRID` `x_or_longitude` `y_or_latitude` ## Example iex> p = Bolt.Sips.Types.Point.create(:wgs_84, 65.43, 12.54) %Bolt.Sips.Types.Point{ crs: "wgs-84", height: nil, latitude: 12.54, longitude: 65.43, srid: 4326, x: 65.43, y: 12.54, z: nil } iex> :erlang.iolist_to_binary(Bolt.Sips.Internals.PackStream.EncoderV2.encode_point(p, 2)) <<0xB3, 0x58, 0xC9, 0x10, 0xE6, 0xC1, 0x40, 0x50, 0x5B, 0x85, 0x1E, 0xB8, 0x51, 0xEC, 0xC1, 0x40, 0x29, 0x14, 0x7A, 0xE1, 0x47, 0xAE, 0x14>> ## Point 3D Signature: `0x58` Encoding: `Marker` `Size` `Signature` ` Content` where `Content` is: `SRID` `x_or_longitude` `y_or_latitude` `z_or_height` ## Example iex> p = Bolt.Sips.Types.Point.create(:wgs_84, 45.0003, 40.3245, 23.1) %Bolt.Sips.Types.Point{ crs: "wgs-84-3d", height: 23.1, latitude: 40.3245, longitude: 45.0003, srid: 4979, x: 45.0003, y: 40.3245, z: 23.1 } iex> :erlang.iolist_to_binary(Bolt.Sips.Internals.PackStream.EncoderV2.encode_point(p, 2)) <<0xB4, 0x59, 0xC9, 0x13, 0x73, 0xC1, 0x40, 0x46, 0x80, 0x9, 0xD4, 0x95, 0x18, 0x2B, 0xC1, 0x40, 0x44, 0x29, 0x89, 0x37, 0x4B, 0xC6, 0xA8, 0xC1, 0x40, 0x37, 0x19, 0x99, 0x99, 0x99, 0x99, 0x9A>> """ @spec encode_point(Point.t(), integer()) :: Bolt.Sips.Internals.PackStream.value() def encode_point(%Point{z: nil} = point, bolt_version), do: EncoderHelper.call_encode(:point, point, bolt_version) def encode_point(%Point{} = point, bolt_version), do: EncoderHelper.call_encode(:point, point, bolt_version) end ================================================ FILE: lib/bolt_sips/internals/pack_stream/encoder_v3.ex ================================================ defmodule Bolt.Sips.Internals.PackStream.EncoderV3 do end ================================================ FILE: lib/bolt_sips/internals/pack_stream/error.ex ================================================ defmodule Bolt.Sips.Internals.PackStreamError do @moduledoc false # Represents an error when encoding data for the Bolt protocol. defexception data_type: nil, data: nil, message: nil, bolt_version: nil @typedoc """ Send back the `item` that cannot be encoded with a `message` explaining the reason why it can't be successfully encoded. """ @type t :: %__MODULE__{ data_type: atom(), data: any(), message: String.t(), bolt_version: integer() } def message(%{data_type: nil, data: data, message: message, bolt_version: bolt_version}) do "#{message} [bolt_version: #{inspect(bolt_version)}, data: #{inspect(data)}]" end def message(%{data_type: data_type, data: data, message: message, bolt_version: bolt_version}) do "#{message} [bolt_version: #{inspect(bolt_version)}, data_type: #{data_type}, data: #{ inspect(data) }]" end end ================================================ FILE: lib/bolt_sips/internals/pack_stream/markers.ex ================================================ defmodule Bolt.Sips.Internals.PackStream.Markers do @moduledoc false defmacro __using__(_opts) do quote do # Null @null_marker 0xC0 # Boolean @true_marker 0xC3 @false_marker 0xC2 # String @tiny_bitstring_marker 0x8 @bitstring8_marker 0xD0 @bitstring16_marker 0xD1 @bitstring32_marker 0xD2 # Integer @int8_marker 0xC8 @int16_marker 0xC9 @int32_marker 0xCA @int64_marker 0xCB # Float @float_marker 0xC1 # List @tiny_list_marker 0x9 @list8_marker 0xD4 @list16_marker 0xD5 @list32_marker 0xD6 # Map @tiny_map_marker 0xA @map8_marker 0xD8 @map16_marker 0xD9 @map32_marker 0xDA # Structure @tiny_struct_marker 0xB @struct8_marker 0xDC @struct16_marker 0xDD # Node @node_marker 0x4E # Relationship @relationship_marker 0x52 # Unbounded relationship @unbounded_relationship_marker 0x72 # Path @path_marker 0x50 # Local Time @local_time_signature 0x74 @local_time_struct_size 1 # Time With TZ Offset @time_with_tz_signature 0x54 @time_with_tz_struct_size 2 # Date @date_signature 0x44 @date_struct_size 1 # Local DateTime @local_datetime_signature 0x64 @local_datetime_struct_size 2 # Datetime with TZ offset @datetime_with_zone_offset_signature 0x46 @datetime_with_zone_offset_struct_size 3 # Datetime with TZ id @datetime_with_zone_id_signature 0x66 @datetime_with_zone_id_struct_size 3 # Duration @duration_signature 0x45 @duration_struct_size 4 # Point 2D @point2d_signature 0x58 @point2d_struct_size 3 # Point 3D @point3d_signature 0x59 @point3d_struct_size 4 end end end defmodule Bolt.Sips.Internals.PackStream.MarkersHelper do @moduledoc false use Bolt.Sips.Internals.PackStream.Markers @doc """ Return the list of valid signatures (for data encoding). """ @spec valid_signatures() :: [integer()] def valid_signatures() do [ @local_time_signature, @time_with_tz_signature, @date_signature, @local_datetime_signature, @datetime_with_zone_offset_signature, @datetime_with_zone_id_signature, @duration_signature, @point2d_signature, @point3d_signature ] end end ================================================ FILE: lib/bolt_sips/internals/pack_stream/message/decoder.ex ================================================ defmodule Bolt.Sips.Internals.PackStream.Message.Decoder do @moduledoc false @tiny_struct_marker 0xB @success_signature 0x70 @failure_signature 0x7F @record_signature 0x71 @ignored_signature 0x7E # Decode SUCCESS message @spec decode(Bolt.Sips.Internals.PackStream.Message.encoded(), integer()) :: Bolt.Sips.Internals.PackStream.Message.decoded() def decode( <<@tiny_struct_marker::4, nb_entries::4, @success_signature, data::binary>>, bolt_version ) do build_response(:success, data, nb_entries, bolt_version) end # Decode FAILURE message def decode( <<@tiny_struct_marker::4, nb_entries::4, @failure_signature, data::binary>>, bolt_version ) do build_response(:failure, data, nb_entries, bolt_version) end # Decode RECORD message def decode( <<@tiny_struct_marker::4, nb_entries::4, @record_signature, data::binary>>, bolt_version ) do build_response(:record, data, nb_entries, bolt_version) end # Decode IGNORED message def decode( <<@tiny_struct_marker::4, nb_entries::4, @ignored_signature, data::binary>>, bolt_version ) do build_response(:ignored, data, nb_entries, bolt_version) end @spec build_response( Bolt.Sips.Internals.PackStream.Message.in_signature(), any(), integer(), integer() ) :: Bolt.Sips.Internals.PackStream.Message.decoded() defp build_response(message_type, data, nb_entries, bolt_version) do Bolt.Sips.Internals.Logger.log_message(:server, message_type, data, :hex) response = case Bolt.Sips.Internals.PackStream.decode(data, bolt_version) do response when nb_entries == 1 -> List.first(response) responses -> responses end Bolt.Sips.Internals.Logger.log_message(:server, message_type, response) {message_type, response} end end ================================================ FILE: lib/bolt_sips/internals/pack_stream/message/encoder.ex ================================================ defmodule Bolt.Sips.Internals.PackStream.Message.Encoder do @moduledoc false _module_doc = """ Manages the message encoding. A mesage is a tuple formated as: `{message_type, data}` with: - message_type: atom amongst the valid message type (:init, :discard_all, :pull_all, :ack_failure, :reset, :run) - data: a list of data to be used by the message Messages are passed in one or more chunk. The structure of a chunk is as follow: `chunk_size` `data` with `chunk_size` beign a 16-bit integer. A message always ends with the end marker `0x00 0x00`. Thus the possible typologies of messages are: - One-chunk message: `chunk_size` `message_data` `end_marker` - multiple-chunk message: `chunk_1_size` `message_data` `chunk_n_size` `message_data`...`end_marker` More documentation on message transfer encoding: [https://boltprotocol.org/v1/#message_transfer_encoding](https://boltprotocol.org/v1/#message_transfer_encoding) All messages are serialized structures. See `Bolt.Sips.Internals.PackStream.EncoderV1` for more information about structure encoding). An extensive documentation on messages can be found here: [https://boltprotocol.org/v1/#messages](https://boltprotocol.org/v1/#messages) """ alias Bolt.Sips.Metadata @max_chunk_size 65_535 @end_marker <<0x00, 0x00>> @ack_failure_signature 0x0E @begin_signature 0x11 @commit_signature 0x12 @discard_all_signature 0x2F @goodbye_signature 0x02 @hello_signature 0x01 @init_signature 0x01 @pull_all_signature 0x3F @reset_signature 0x0F @rollback_signature 0x13 @run_signature 0x10 # OUT Signature # TODO improve using macros? @valid_signatures [ @ack_failure_signature, @begin_signature, @commit_signature, @discard_all_signature, @goodbye_signature, @hello_signature, @pull_all_signature, @reset_signature, @rollback_signature, @run_signature ] @valid_v1_signatures [ @ack_failure_signature, @discard_all_signature, @init_signature, @pull_all_signature, @reset_signature, @run_signature ] @valid_message_types [ :ack_failure, :begin, :commit, :discard_all, :goodbye, :hello, :rollback, :pull_all, :reset, :run ] @valid_v1_message_types [ :ack_failure, :discard_all, :init, :pull_all, :reset, :run ] @last_bolt_version 3 @spec signature(Bolt.Sips.Internals.PackStream.Message.out_signature()) :: integer() defp signature(:ack_failure), do: @ack_failure_signature defp signature(:discard_all), do: @discard_all_signature defp signature(:pull_all), do: @pull_all_signature defp signature(:reset), do: @reset_signature defp signature(:begin), do: @begin_signature defp signature(:commit), do: @commit_signature defp signature(:goodbye), do: @goodbye_signature defp signature(:hello), do: @hello_signature defp signature(:rollback), do: @rollback_signature defp signature(:run), do: @run_signature defp signature(:init), do: @init_signature @doc """ Return client name (based on bolt_sips version) """ def client_name() do "BoltSips/" <> to_string(Application.spec(:bolt_sips, :vsn)) end @doc """ Return the valid message signatures depending on the Bolt version """ @spec valid_signatures(integer()) :: [integer()] def valid_signatures(bolt_version) when bolt_version <= 2 do @valid_v1_signatures end def valid_signatures(3) do @valid_signatures end # Encode messages for bolt version 3 # Encode HELLO message without auth token @spec encode({Bolt.Sips.Internals.PackStream.Message.out_signature(), list()}, integer()) :: Bolt.Sips.Internals.PackStream.Message.encoded() | {:error, :not_implemented} | {:error, :invalid_message} def encode({:hello, []}, 3) do encode({:hello, [{}]}, 3) end # Encode INIT message with a valid auth token. # The auth token is tuple formated as: {user, password} def encode({:hello, [auth]}, 3) do do_encode(:hello, [auth_params(auth)], 3) end # Encode BEGIN message without metadata. # BEGIN is used to open a transaction. def encode({:begin, []}, 3) do encode({:begin, [%{}]}, 3) end # Encode BEGIN message with metadata def encode({:begin, [%Metadata{} = metadata]}, 3) do do_encode(:begin, [Metadata.to_map(metadata)], 3) end def encode({:begin, [%{} = map]}, 3) when map_size(map) == 0 do {:ok, metadata} = Metadata.new(%{}) encode({:begin, [metadata]}, 3) end def encode({:begin, _}, _) do {:error, :invalid_data} end # Encode RUN without params nor metadata def encode({:run, [statement]}, 3) do do_encode(:run, [statement, %{}, %{}], 3) end # Encode RUN message with its data: statement and parameters def encode({:run, [statement]}, bolt_version) when bolt_version <= 2 do do_encode(:run, [statement, %{}], bolt_version) end # Encode RUN with params but without metadata def encode({:run, [statement, params]}, 3) do do_encode(:run, [statement, params, %{}], 3) end # Encode RUN with params and metadata def encode({:run, [statement, params, %Metadata{} = metadata]}, 3) do do_encode(:run, [statement, params, Metadata.to_map(metadata)], 3) end # INIT is no more a valid message in Bolt V3 def encode({:init, _}, 3) do {:error, :invalid_message} end # Encode INIT message without auth token def encode({:init, []}, bolt_version) when bolt_version <= 2 do encode({:init, [{}]}, bolt_version) end # Encode INIT message with a valid auth token. # The auth token is tuple formated as: {user, password} def encode({:init, [auth]}, bolt_version) when bolt_version <= 2 do do_encode(:init, [client_name(), auth_params_v1(auth)], bolt_version) end # Encode messages that don't need any data formating def encode({message_type, data}, 3) when message_type in @valid_message_types do do_encode(message_type, data, 3) end # Encode messages that don't need any data formating def encode({message_type, data}, bolt_version) when bolt_version <= 2 and message_type in @valid_v1_message_types do do_encode(message_type, data, bolt_version) end @doc """ Encode Bolt V3 messages Not that INIT is not valid in bolt V3, it is replaced by HELLO ## HELLO Usage: intialize the session. Signature: `0x01` (Same as INIT in previous bolt version) Struct: `auth_parms` with: | data | type | |-----|-----| |auth_token | map: {scheme: string, principal: string, credentials: string, user_agent: string}| Note: `user_agent` is equivalent to `client_name` in bolt previous version. Examples (excluded from doctest because client_name changes at each bolt_sips version) # without auth token diex> :erlang.iolist_to_binary(Encoder.encode({:hello, []}, 3)) <<0x0, 0x1D, 0xB1, 0x1, 0xA1, 0x8A, 0x75, 0x73, 0x65, 0x72, 0x5F, 0x61, 0x67, 0x65, 0x6E, 0x74, 0x8E, 0x42, 0x6F, 0x6C, 0x74, 0x53, 0x69, 0x70, 0x73, 0x2F, 0x31, 0x2E, 0x34, 0x2E, 0x30, 0x0, 0x0>> # with auth token diex(20)> :erlang.iolist_to_binary(Encoder.encode({:hello, [{"neo4j", "test"}]}, 3)) <<0x0, 0x4B, 0xB1, 0x1, 0xA4, 0x8B, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6E, 0x74, 0x69, 0x61, 0x6C, 0x73, 0x84, 0x74, 0x65, 0x73, 0x74, 0x89, 0x70, 0x72, 0x69, 0x6E, 0x63, 0x69, 0x70, 0x61, 0x6C, 0x85, 0x6E, 0x65, 0x6F, 0x34, 0x6A, 0x86, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x65, 0x85, 0x62, 0x61, 0x73, 0x69, ...>> ## GOODBYE Usage: close the connection with the server Signature: `0x02` Struct: no data Example iex> alias Bolt.Sips.Internals.PackStream.Message.Encoder iex> :erlang.iolist_to_binary(Encoder.encode({:goodbye, []}, 3)) <<0x0, 0x2, 0xB0, 0x2, 0x0, 0x0>> ## BEGIN Usage: Open a transaction Signature: `0x11` Struct: `metadata` with: | data | type | |------|------| | metadata | See Bolt.Sips.Metadata Example # without metadata # iex> alias Bolt.Sips.Internals.PackStream.Message.Encoder # iex> :erlang.iolist_to_binary(Encoder.encode({:begin, []}, 3)) # <<0x0, 0x3, 0xB1, 0x11, 0xA0, 0x0, 0x0>> # # with metadata # iex> alias Bolt.Sips.Internals.PackStream.Message.Encoder # iex> alias Bolt.Sips.Metadata # iex> {:ok, metadata} = Metadata.new(%{tx_timeout: 5000}) # {:ok, # %Bolt.Sips.Metadata{ # bookmarks: nil, # metadata: nil, # tx_timeout: 5000 # }} # iex> :erlang.iolist_to_binary(Encoder.encode({:begin, [metadata]}, 3)) # <<0x0, 0x11, 0xB1, 0x11, 0xA1, 0x8A, 0x74, 0x78, 0x5F, 0x74, 0x69, 0x6D, 0x65, 0x6F, 0x75, # 0x74, 0xC9, 0x13, 0x88, 0x0, 0x0>> ## COMMIT Usage: commit the currently open transaction Signature: `0x12` Struct: no data Example iex> alias Bolt.Sips.Internals.PackStream.Message.Encoder iex> :erlang.iolist_to_binary(Encoder.encode({:commit, []}, 3)) <<0x0, 0x2, 0xB0, 0x12, 0x0, 0x0>> ## ROLLBACK Usage: rollback the currently open transaction Signature: `0x13` Struct: no data Example iex> alias Bolt.Sips.Internals.PackStream.Message.Encoder iex> :erlang.iolist_to_binary(Encoder.encode({:rollback, []}, 3)) <<0x0, 0x2, 0xB0, 0x13, 0x0, 0x0>> ## RUN Usage: pass statement for execution to the server. Same as in bolt previous version. The only difference: `metadata` are passed as well since bolt v3. Signature: `0x10` Struct: `statement` `parameters` `metadata` with: | data | type | |-----|-----| | statement | string | | parameters | map | | metadata | See Bolt.Sips.Metadata Example # without params nor metadata iex> alias Bolt.Sips.Internals.PackStream.Message.Encoder iex> :erlang.iolist_to_binary(Encoder.encode({:run, ["RETURN 'hello' AS str"]}, 3)) <<0x0, 0x1B, 0xB3, 0x10, 0xD0, 0x15, 0x52, 0x45, 0x54, 0x55, 0x52, 0x4E, 0x20, 0x27, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x27, 0x20, 0x41, 0x53, 0x20, 0x73, 0x74, 0x72, 0xA0, 0xA0, 0x0, 0x0>> # without params but with metadata iex> alias Bolt.Sips.Internals.PackStream.Message.Encoder iex> alias Bolt.Sips.Metadata iex> {:ok, metadata} = Metadata.new(%{tx_timeout: 4500}) {:ok, %Bolt.Sips.Metadata{ bookmarks: nil, metadata: nil, tx_timeout: 4500 }} iex> :erlang.iolist_to_binary(Encoder.encode({:run, ["RETURN 'hello' AS str", %{}, metadata]}, 3)) <<0x0, 0x29, 0xB3, 0x10, 0xD0, 0x15, 0x52, 0x45, 0x54, 0x55, 0x52, 0x4E, 0x20, 0x27, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x27, 0x20, 0x41, 0x53, 0x20, 0x73, 0x74, 0x72, 0xA0, 0xA1, 0x8A, 0x74, 0x78, 0x5F, 0x74, 0x69, 0x6D, 0x65, 0x6F, 0x75, 0x74, 0xC9, 0x11, 0x94, 0x0, 0x0>> # with params but without metadata iex> alias Bolt.Sips.Internals.PackStream.Message.Encoder iex> :erlang.iolist_to_binary(Encoder.encode({:run, ["RETURN $str AS str", %{str: "hello"}]}, 3)) <<0x0, 0x22, 0xB3, 0x10, 0xD0, 0x12, 0x52, 0x45, 0x54, 0x55, 0x52, 0x4E, 0x20, 0x24, 0x73, 0x74, 0x72, 0x20, 0x41, 0x53, 0x20, 0x73, 0x74, 0x72, 0xA1, 0x83, 0x73, 0x74, 0x72, 0x85, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0xA0, 0x0, 0x0>> # with params and metadata iex> alias Bolt.Sips.Internals.PackStream.Message.Encoder iex> alias Bolt.Sips.Metadata iex> {:ok, metadata} = Metadata.new(%{tx_timeout: 4500}) {:ok, %Bolt.Sips.Metadata{ bookmarks: nil, metadata: nil, tx_timeout: 4500 }} iex> :erlang.iolist_to_binary(Encoder.encode({:run, ["RETURN $str AS str", %{str: "hello"}, metadata]}, 3)) <<0x0, 0x30, 0xB3, 0x10, 0xD0, 0x12, 0x52, 0x45, 0x54, 0x55, 0x52, 0x4E, 0x20, 0x24, 0x73, 0x74, 0x72, 0x20, 0x41, 0x53, 0x20, 0x73, 0x74, 0x72, 0xA1, 0x83, 0x73, 0x74, 0x72, 0x85, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0xA1, 0x8A, 0x74, 0x78, 0x5F, 0x74, 0x69, 0x6D, 0x65, 0x6F, 0x75, 0x74, 0xC9, 0x11, 0x94, 0x0, 0x0>> # Encode messages v1 # Supported messages ## INIT Usage: intialize the session. Signature: `0x01` Struct: `client_name` `auth_token` with: | data | type | |-----|-----| |client_name | string| |auth_token | map: {scheme: string, principal: string, credentials: string}| Examples (excluded from doctest because client_name changes at each bolt_sips version) # without auth token diex> alias Bolt.Sips.Internals.PackStream.Message.Encoder :erlang.iolist_to_binary(Encoder.encode({:init, []}, 1)) <<0x0, 0x10, 0xB2, 0x1, 0x8C, 0x42, 0x6F, 0x6C, 0x74, 0x65, 0x78, 0x2F, 0x30, 0x2E, 0x34, 0x2E, 0x30, 0xA0, 0x0, 0x0>> # with auth token # The auth token is tuple formated as: {user, password} diex> alias Bolt.Sips.Internals.PackStream.Message.Encoder diex> :erlang.iolist_to_binary(Encoder.encode({:init, [{"neo4j", "password"}]})) <<0x0, 0x42, 0xB2, 0x1, 0x8C, 0x42, 0x6F, 0x6C, 0x74, 0x65, 0x78, 0x2F, 0x30, 0x2E, 0x34, 0x2E, 0x30, 0xA3, 0x8B, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6E, 0x74, 0x69, 0x61, 0x6C, 0x73, 0x88, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6F, 0x72, 0x64, 0x89, 0x70, 0x72, 0x69, 0x6E, 0x63, 0x69, 0x70, 0x61, 0x6C, 0x85, ...>> ## RUN Usage: pass statement for execution to the server. Signature: `0x10` Struct: `statement` `parameters` with: | data | type | |-----|-----| | statement | string | | parameters | map | Examples # without parameters iex> alias Bolt.Sips.Internals.PackStream.Message.Encoder iex> :erlang.iolist_to_binary(Encoder.encode({:run, ["RETURN 1 AS num"]}, 1)) <<0x0, 0x13, 0xB2, 0x10, 0x8F, 0x52, 0x45, 0x54, 0x55, 0x52, 0x4E, 0x20, 0x31, 0x20, 0x41, 0x53, 0x20, 0x6E, 0x75, 0x6D, 0xA0, 0x0, 0x0>> # with parameters iex> :erlang.iolist_to_binary(Encoder.encode({:run, ["RETURN $num AS num", %{num: 1}]}, 1)) <<0x0, 0x1C, 0xB2, 0x10, 0xD0, 0x12, 0x52, 0x45, 0x54, 0x55, 0x52, 0x4E, 0x20, 0x24, 0x6E, 0x75, 0x6D, 0x20, 0x41, 0x53, 0x20, 0x6E, 0x75, 0x6D, 0xA1, 0x83, 0x6E, 0x75, 0x6D, 0x1, 0x0, 0x0>> ## ACK_FAILURE Usage: Acknowledge a failure the server has sent. Signature: `0x0E` Struct: no data Example iex> alias Bolt.Sips.Internals.PackStream.Message.Encoder iex> :erlang.iolist_to_binary(Encoder.encode({:ack_failure, []}, 1)) <<0x0, 0x2, 0xB0, 0xE, 0x0, 0x0>> ## DISCARD_ALL Uage: Discard all remaining items from the active result stream. Signature: `0x2F` Struct: no data Example iex> alias Bolt.Sips.Internals.PackStream.Message.Encoder iex> :erlang.iolist_to_binary(Encoder.encode({:discard_all, []}, 1)) <<0x0, 0x2, 0xB0, 0x2F, 0x0, 0x0>> ## PULL_ALL Usage: Retrieve all remaining items from the active result stream. Signature: `0x3F` Struct: no data Example iex> alias Bolt.Sips.Internals.PackStream.Message.Encoder iex> :erlang.iolist_to_binary(Encoder.encode({:pull_all, []}, 1)) <<0x0, 0x2, 0xB0, 0x3F, 0x0, 0x0>> ## RESET Usage: Return the current session to a "clean" state. Signature: `0x0F` Struct: no data Example iex> alias Bolt.Sips.Internals.PackStream.Message.Encoder iex> :erlang.iolist_to_binary(Encoder.encode({:reset, []}, 1)) <<0x0, 0x2, 0xB0, 0xF, 0x0, 0x0>> Check if the encoder for the given bolt version is capable of encoding the given message If it is the case, the encoding function will be called If not, fallback to previous bolt version If encoding function is not present in any of the bolt version, an error will be raised """ def encode(data, bolt_version) when is_integer(bolt_version) and bolt_version > @last_bolt_version do encode(data, @last_bolt_version) end def encode(_data, _bolt_version) do {:error, :not_implemented} end defp do_encode(message_type, data, bolt_version) do signature = signature(message_type) encode_message(message_type, signature, data, bolt_version) end # Format the auth params for v1 to v2 @spec auth_params_v1({} | {String.t(), String.t()}) :: map() defp auth_params_v1({}), do: %{} defp auth_params_v1({username, password}) do %{ scheme: "basic", principal: username, credentials: password } end # Format the auth params @spec auth_params({} | {String.t(), String.t()}) :: map() defp auth_params({}), do: user_agent() defp auth_params({username, password}) do %{ scheme: "basic", principal: username, credentials: password } |> Map.merge(user_agent()) end defp user_agent() do %{user_agent: client_name()} end @doc """ Perform the final message: - add header - manage chunk if necessary - add end marker """ @spec encode_message( Bolt.Sips.Internals.PackStream.Message.out_signature(), integer(), list(), integer() ) :: [[Bolt.Sips.Internals.PackStream.Message.encoded()]] def encode_message(message_type, signature, data, bolt_version) do Bolt.Sips.Internals.Logger.log_message(:client, message_type, data) encoded = {signature, data} |> Bolt.Sips.Internals.PackStream.encode(bolt_version) |> generate_chunks([]) Bolt.Sips.Internals.Logger.log_message(:client, message_type, encoded, :hex) encoded end @spec generate_chunks(Bolt.Sips.Internals.PackStream.value() | <<>>, list()) :: [[Bolt.Sips.Internals.PackStream.Message.encoded()]] defp generate_chunks(<<>>, chunks) do [chunks, [@end_marker], []] end defp generate_chunks(data, chunks) do data_size = :erlang.iolist_size(data) case data_size > @max_chunk_size do true -> bindata = :erlang.iolist_to_binary(data) <> = bindata new_chunk = format_chunk(chunk) # [new_chunk, generate_chunks(rest,[])] generate_chunks(rest, [chunks, new_chunk]) # generate_chunks(<>, [new_chunk, chunks]) _ -> generate_chunks(<<>>, [chunks, format_chunk(data)]) end end @spec format_chunk(Bolt.Sips.Internals.PackStream.value()) :: [Bolt.Sips.Internals.PackStream.Message.encoded()] defp format_chunk(chunk) do [<<:erlang.iolist_size(chunk)::16>>, chunk] end end ================================================ FILE: lib/bolt_sips/internals/pack_stream/message/encoder_v1.ex ================================================ defmodule Bolt.Sips.Internals.PackStream.Message.EncoderV1 do @moduledoc false use Bolt.Sips.Internals.PackStream.Message.Signatures alias Bolt.Sips.Internals.PackStream.Message.Encoder @doc """ Encode INIT message without auth token """ @spec encode({Bolt.Sips.Internals.PackStream.Message.out_signature(), list()}, integer()) :: Bolt.Sips.Internals.PackStream.Message.encoded() | {:error, :not_implemented} def encode(data, bolt_version) do Encoder.encode(data, bolt_version) end end ================================================ FILE: lib/bolt_sips/internals/pack_stream/message/encoder_v2.ex ================================================ defmodule Bolt.Sips.Internals.PackStream.Message.EncoderV2 do def encode(_, _) do {:error, :not_implemented} end end ================================================ FILE: lib/bolt_sips/internals/pack_stream/message/encoder_v3.ex ================================================ defmodule Bolt.Sips.Internals.PackStream.Message.EncoderV3 do @moduledoc false use Bolt.Sips.Internals.PackStream.Message.Signatures alias Bolt.Sips.Internals.PackStream.Message.Encoder @valid_signatures [ @begin_signature, @commit_signature, @discard_all_signature, @goodbye_signature, @hello_signature, @pull_all_signature, @reset_signature, @rollback_signature, @run_signature ] @doc """ Return the valid signatures for bolt V1 """ @spec valid_signatures() :: [integer()] def valid_signatures() do @valid_signatures end @doc """ Encode HELLO message without auth token """ @spec encode({Bolt.Sips.Internals.PackStream.Message.out_signature(), list()}, integer()) :: Bolt.Sips.Internals.PackStream.Message.encoded() | {:error, :not_implemented} | {:error, :invalid_message} def encode(data, bolt_version) do Encoder.encode(data, bolt_version) end end ================================================ FILE: lib/bolt_sips/internals/pack_stream/message/signatures.ex ================================================ defmodule Bolt.Sips.Internals.PackStream.Message.Signatures do @moduledoc false defmacro __using__(_opts) do quote do # Message OUT @ack_failure_signature 0x0E @begin_signature 0x11 @commit_signature 0x12 @discard_all_signature 0x2F @goodbye_signature 0x02 @hello_signature 0x01 @init_signature 0x01 @pull_all_signature 0x3F @reset_signature 0x0F @rollback_signature 0x13 @run_signature 0x10 # Message IN @success_signature 0x70 @failure_signature 0x7F @record_signature 0x71 @ignored_signature 0x7E end end end ================================================ FILE: lib/bolt_sips/internals/pack_stream/message.ex ================================================ defmodule Bolt.Sips.Internals.PackStream.Message do @moduledoc false # Manage the message encoding and decoding. # # Message encoding / decoding is the first step of encoding / decoding. # The next step is the message data encoding /decoding (which is handled by packstream.ex) alias Bolt.Sips.Internals.PackStream.Message.Encoder alias Bolt.Sips.Internals.PackStream.Message.Decoder @type in_signature :: :failure | :ignored | :record | :success @type out_signature :: :ack_failure | :begin | :commit | :discard_all | :goodbye | :hello | :init | :pull_all | :reset | :rollback | :run @type raw :: {out_signature, list()} @type decoded :: {in_signature(), any()} @type encoded :: <<_::16, _::_*8>> @doc """ Encode a message depending on its type """ @spec encode({Bolt.Sips.Internals.PackStream.Message.out_signature(), list()}, integer()) :: Bolt.Sips.Internals.PackStream.Message.encoded() def encode(message, bolt_version) do Encoder.encode(message, bolt_version) end @doc """ Decode a message """ @spec decode(Bolt.Sips.Internals.PackStream.Message.encoded(), integer()) :: Bolt.Sips.Internals.PackStream.Message.decoded() def decode(message, bolt_version) do Decoder.decode(message, bolt_version) end end ================================================ FILE: lib/bolt_sips/internals/pack_stream/utils.ex ================================================ defmodule Bolt.Sips.Internals.PackStream.Utils do alias Bolt.Sips.Internals.PackStream.Encoder alias Bolt.Sips.Types.Duration alias Bolt.Sips.Internals.PackStreamError defmacro __using__(_options) do quote do import unquote(__MODULE__) # catch all clause for encoding implementation defp do_call_encode(data_type, data, original_version) do raise PackStreamError, data_type: data_type, data: data, bolt_version: original_version, message: "Encoding function not implemented for" end @spec encode_list_data(list(), integer()) :: [any()] defp encode_list_data(data, bolt_version) do Enum.map( data, &Encoder.encode(&1, bolt_version) ) end @spec encode_kv(map(), integer()) :: binary() defp encode_kv(map, bolt_version) do Enum.reduce(map, <<>>, fn data, acc -> [acc, do_reduce_kv(data, bolt_version)] end) end @spec do_reduce_kv({atom(), any()}, integer()) :: [binary()] defp do_reduce_kv({key, value}, bolt_version) do [ Encoder.encode( key, bolt_version ), Encoder.encode(value, bolt_version) ] end @spec day_time(Time.t()) :: integer() defp day_time(time) do Time.diff(time, ~T[00:00:00.000], :nanosecond) end @spec decompose_datetime(Calendar.naive_datetime()) :: [integer()] defp decompose_datetime(%NaiveDateTime{} = datetime) do datetime_micros = NaiveDateTime.diff(datetime, ~N[1970-01-01 00:00:00.000], :microsecond) seconds = div(datetime_micros, 1_000_000) nanoseconds = rem(datetime_micros, 1_000_000) * 1_000 [seconds, nanoseconds] end @spec compact_duration(Duration.t()) :: [integer()] defp compact_duration(%Duration{} = duration) do months = 12 * duration.years + duration.months days = 7 * duration.weeks + duration.days seconds = 3600 * duration.hours + 60 * duration.minutes + duration.seconds [months, days, seconds, duration.nanoseconds] end end end end ================================================ FILE: lib/bolt_sips/internals/pack_stream/v1.ex ================================================ defmodule Bolt.Sips.Internals.PackStream.V1 do defmacro __using__(_options) do quote do import unquote(__MODULE__) @last_version Bolt.Sips.Internals.BoltVersionHelper.last() @int8 -128..-17 @int16_low -32_768..-129 @int16_high 128..32_767 @int32_low -2_147_483_648..-32_769 @int32_high 32_768..2_147_483_647 @int64_low -9_223_372_036_854_775_808..-2_147_483_649 @int64_high 2_147_483_648..9_223_372_036_854_775_807 # Null @null_marker 0xC0 # Boolean @true_marker 0xC3 @false_marker 0xC2 # String @tiny_bitstring_marker 0x8 @bitstring8_marker 0xD0 @bitstring16_marker 0xD1 @bitstring32_marker 0xD2 # Integer @int8_marker 0xC8 @int16_marker 0xC9 @int32_marker 0xCA @int64_marker 0xCB # Float @float_marker 0xC1 # List @tiny_list_marker 0x9 @list8_marker 0xD4 @list16_marker 0xD5 @list32_marker 0xD6 # Map @tiny_map_marker 0xA @map8_marker 0xD8 @map16_marker 0xD9 @map32_marker 0xDA # Structure @tiny_struct_marker 0xB @struct8_marker 0xDC @struct16_marker 0xDD @spec do_call_encode(atom(), any(), integer()) :: binary() | PackStreamError.t() # Atoms defp do_call_encode(:atom, nil, bolt_version) when bolt_version <= @last_version do <<@null_marker>> end defp do_call_encode(:atom, true, bolt_version) when bolt_version <= @last_version do <<@true_marker>> end defp do_call_encode(:atom, false, bolt_version) when bolt_version <= @last_version do <<@false_marker>> end defp do_call_encode(:atom, other, bolt_version) when bolt_version <= @last_version do call_encode(:string, other |> Atom.to_string(), bolt_version) end # Strings defp do_call_encode(:string, string, bolt_version) when bolt_version <= @last_version and byte_size(string) <= 15 do [<<@tiny_bitstring_marker::4, byte_size(string)::4>>, string] end defp do_call_encode(:string, string, bolt_version) when bolt_version <= @last_version and byte_size(string) <= 255 do [<<@bitstring8_marker, byte_size(string)::8>>, string] end defp do_call_encode(:string, string, bolt_version) when bolt_version <= @last_version and byte_size(string) <= 65_535 do [<<@bitstring16_marker, byte_size(string)::16>>, string] end defp do_call_encode(:string, string, bolt_version) when bolt_version <= @last_version and byte_size(string) <= 4_294_967_295 do [<<@bitstring32_marker, byte_size(string)::32>>, string] end # Integer defp do_call_encode(:integer, integer, bolt_version) when bolt_version <= @last_version and integer in -16..127 do <> end defp do_call_encode(:integer, integer, bolt_version) when bolt_version <= @last_version and integer in @int8 do <<@int8_marker, integer>> end defp do_call_encode(:integer, integer, bolt_version) when bolt_version <= @last_version and integer in @int16_low when bolt_version <= @last_version and integer in @int16_high do <<@int16_marker, integer::16>> end defp do_call_encode(:integer, integer, bolt_version) when bolt_version <= @last_version and integer in @int32_low when bolt_version <= @last_version and integer in @int32_high do <<@int32_marker, integer::32>> end defp do_call_encode(:integer, integer, bolt_version) when bolt_version <= @last_version and integer in @int64_low when bolt_version <= @last_version and integer in @int64_high do <<@int64_marker, integer::64>> end # Float defp do_call_encode(:float, number, bolt_version) when bolt_version <= 3 do <<@float_marker, number::float>> end # lists defp do_call_encode(:list, list, bolt_version) when bolt_version <= @last_version and length(list) <= 15 do [<<@tiny_list_marker::4, length(list)::4>>, encode_list_data(list, bolt_version)] end defp do_call_encode(:list, list, bolt_version) when bolt_version <= @last_version and length(list) <= 255 do [<<@list8_marker, length(list)::8>>, encode_list_data(list, bolt_version)] end defp do_call_encode(:list, list, bolt_version) when bolt_version <= @last_version and length(list) <= 65_535 do [<<@list16_marker, length(list)::16>>, encode_list_data(list, bolt_version)] end defp do_call_encode(:list, list, bolt_version) when bolt_version <= @last_version and length(list) <= 4_294_967_295 do [<<@list32_marker, length(list)::32>>, encode_list_data(list, bolt_version)] end # maps defp do_call_encode(:map, map, bolt_version) when bolt_version <= @last_version and map_size(map) <= 15 do [<<@tiny_map_marker::4, map_size(map)::4>>, encode_kv(map, bolt_version)] end defp do_call_encode(:map, map, bolt_version) when bolt_version <= @last_version and map_size(map) <= 255 do [<<@map8_marker, map_size(map)::8>>, encode_kv(map, bolt_version)] end defp do_call_encode(:map, map, bolt_version) when bolt_version <= @last_version and map_size(map) <= 65_535 do [<<@map16_marker, map_size(map)::16>>, encode_kv(map, bolt_version)] end defp do_call_encode(:map, map, bolt_version) when bolt_version <= @last_version and map_size(map) <= 4_294_967_295 do [<<@map32_marker, map_size(map)::32>>, encode_kv(map, bolt_version)] end # Structs defp do_call_encode(:struct, {signature, list}, bolt_version) when bolt_version <= @last_version and length(list) <= 15 do [ <<@tiny_struct_marker::4, length(list)::4, signature>>, encode_list_data(list, bolt_version) ] end defp do_call_encode(:struct, {signature, list}, bolt_version) when bolt_version <= @last_version and length(list) <= 255 do [<<@struct8_marker::8, length(list)::8, signature>>, encode_list_data(list, bolt_version)] end defp do_call_encode(:struct, {signature, list}, bolt_version) when bolt_version <= @last_version and length(list) <= 65_535 do [ <<@struct16_marker::8, length(list)::16, signature>>, encode_list_data(list, bolt_version) ] end end end end ================================================ FILE: lib/bolt_sips/internals/pack_stream/v2.ex ================================================ defmodule Bolt.Sips.Internals.PackStream.V2 do alias Bolt.Sips.Types.{TimeWithTZOffset, DateTimeWithTZOffset, Duration, Point} alias Bolt.Sips.Internals.PackStream.Encoder defmacro __using__(_options) do quote do import unquote(__MODULE__) @last_version Bolt.Sips.Internals.BoltVersionHelper.last() # Local Time @local_time_signature 0x74 # Time With TZ Offset @time_with_tz_signature 0x54 # Date @date_signature 0x44 # Local DateTime @local_datetime_signature 0x64 # Datetime with TZ offset @datetime_with_zone_offset_signature 0x46 # Datetime with TZ id @datetime_with_zone_id_signature 0x66 # Duration @duration_signature 0x45 # Point 2D @point2d_signature 0x58 # Point 3D @point3d_signature 0x59 defp do_call_encode(:local_time, local_time, bolt_version) when bolt_version >= 2 and bolt_version <= @last_version do Encoder.encode({@local_time_signature, [day_time(local_time)]}, bolt_version) end defp do_call_encode( :time_with_tz, %TimeWithTZOffset{time: time, timezone_offset: offset}, bolt_version ) when bolt_version >= 2 and bolt_version <= @last_version do Encoder.encode({@time_with_tz_signature, [day_time(time), offset]}, bolt_version) end defp do_call_encode(:date, date, bolt_version) when bolt_version >= 2 and bolt_version <= @last_version do epoch = Date.diff(date, ~D[1970-01-01]) Encoder.encode({@date_signature, [epoch]}, bolt_version) end defp do_call_encode(:local_datetime, local_datetime, bolt_version) when bolt_version >= 2 and bolt_version <= @last_version do Encoder.encode( {@local_datetime_signature, decompose_datetime(local_datetime)}, bolt_version ) end defp do_call_encode(:datetime_with_tz_id, datetime, bolt_version) when bolt_version >= 2 and bolt_version <= @last_version do data = decompose_datetime(DateTime.to_naive(datetime)) ++ [datetime.time_zone] Encoder.encode({@datetime_with_zone_id_signature, data}, bolt_version) end defp do_call_encode( :datetime_with_tz_offset, %DateTimeWithTZOffset{naive_datetime: ndt, timezone_offset: tz_offset}, bolt_version ) when bolt_version >= 2 and bolt_version <= @last_version do data = decompose_datetime(ndt) ++ [tz_offset] Encoder.encode({@datetime_with_zone_offset_signature, data}, bolt_version) end defp do_call_encode(:duration, %Duration{} = duration, bolt_version) when bolt_version >= 2 and bolt_version <= @last_version do Encoder.encode({@duration_signature, compact_duration(duration)}, bolt_version) end defp do_call_encode(:point, %Point{z: nil} = point, bolt_version) when bolt_version >= 2 and bolt_version <= @last_version do Encoder.encode({@point2d_signature, [point.srid, point.x, point.y]}, bolt_version) end defp do_call_encode(:point, %Point{} = point, bolt_version) when bolt_version >= 2 and bolt_version <= @last_version do Encoder.encode( {@point3d_signature, [point.srid, point.x, point.y, point.z]}, bolt_version ) end end end end ================================================ FILE: lib/bolt_sips/internals/pack_stream.ex ================================================ defmodule Bolt.Sips.Internals.PackStream do @moduledoc false # The PackStream implementation for Bolt. # # This module defines a decode function, that will take a binary stream of data # and recursively turn it into a list of Elixir data types. # # It further defines a function for encoding Elixir data types into a binary # stream, using the Bolt.Sips.Internals.PackStream.Encoder protocol. @type value :: <<_::8, _::_*8>> @doc """ Encodes a list of items into their binary representation. As developers tend to be lazy, single objects may be passed. ## Examples iex> Bolt.Sips.Internals.PackStream.encode "hello world" <<0x8B, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64>> """ @spec encode(any(), integer()) :: Bolt.Sips.Internals.PackStream.value() | <<_::16, _::_*8>> def encode(item, bolt_version) do Bolt.Sips.Internals.PackStream.Encoder.encode(item, bolt_version) end @doc """ Decode data from Bolt binary format to Elixir type ## Example iex> Bolt.Sips.Internals.PackStream.decode(<<0xC3>>) [true] """ @spec decode(binary(), integer()) :: list() def decode(data, bolt_version) do Bolt.Sips.Internals.PackStream.Decoder.decode(data, bolt_version) end end ================================================ FILE: lib/bolt_sips/metadata.ex ================================================ defmodule Bolt.Sips.Metadata do @moduledoc false defstruct [:bookmarks, :tx_timeout, :metadata] @type t :: %__MODULE__{ bookmarks: [String.t()], tx_timeout: non_neg_integer(), metadata: map() } alias Bolt.Sips.Metadata @doc """ Create a new metadata structure. Data must be valid. """ @spec new(map()) :: {:ok, Bolt.Sips.Metadata.t()} | {:error, String.t()} def new(data) do with {:ok, data} <- check_keys(data), {:ok, bookmarks} <- validate_bookmarks(Map.get(data, :bookmarks, [])), {:ok, tx_timeout} <- validate_timeout(Map.get(data, :tx_timeout)), {:ok, metadata} <- validate_metadata(Map.get(data, :metadata, %{})) do {:ok, %__MODULE__{ bookmarks: bookmarks, tx_timeout: tx_timeout, metadata: metadata }} else error -> error end end @doc """ Convert the Metadata struct to a map. All `nil` will be stripped """ @spec to_map(Bolt.Sips.Metadata.t()) :: map() def to_map(metadata) do with {:ok, metadata} <- check_keys(Map.from_struct(metadata)) do metadata |> Map.from_struct() |> Enum.filter(fn {_, value} -> value != nil end) |> Enum.into(%{}) else error -> error end end defp check_keys(data) do try do {:ok, struct!(Metadata, data)} rescue _ in KeyError -> {:error, "[Metadata] Invalid keys"} end end @spec validate_bookmarks(any()) :: {:ok, list()} | {:ok, nil} | {:error, String.t()} defp validate_bookmarks(bookmarks) when (is_list(bookmarks) and length(bookmarks) > 0) or is_nil(bookmarks) do {:ok, bookmarks} end defp validate_bookmarks([]) do {:ok, nil} end defp validate_bookmarks(_) do {:error, "[Metadata] Invalid bookmkarks. Should be a list."} end @spec validate_timeout(any()) :: {:ok, integer()} | {:error, String.t()} defp validate_timeout(timeout) when (is_integer(timeout) and timeout > 0) or is_nil(timeout) do {:ok, timeout} end defp validate_timeout(nil) do {:ok, nil} end defp validate_timeout(_) do {:error, "[Metadata] Invalid timeout. Should be a positive integer."} end @spec validate_metadata(any()) :: {:ok, map()} | {:ok, nil} | {:error, String.t()} defp validate_metadata(metadata) when (is_map(metadata) and map_size(metadata) > 0) or is_nil(metadata) do {:ok, metadata} end defp validate_metadata(%{}) do {:ok, nil} end defp validate_metadata(_) do {:error, "[Metadata] Invalid timeout. Should be a valid map or nil."} end end ================================================ FILE: lib/bolt_sips/protocol.ex ================================================ defmodule Bolt.Sips.Protocol do @moduledoc false # Implements callbacks required by DBConnection. # Each callback receives an open connection as a state. defmodule ConnData do @moduledoc false # Defines the state used by DbConnection implementation defstruct [:sock, :bolt_version, :configuration] @type t :: %__MODULE__{ sock: port(), bolt_version: integer(), configuration: Keyword.t() } end use DBConnection require Logger alias Bolt.Sips.QueryStatement alias Bolt.Sips.Internals.Error, as: BoltError alias Bolt.Sips.Internals.BoltProtocol @doc "Callback for DBConnection.connect/1" def connect(opts \\ []) def connect([]), do: connect(Bolt.Sips.Utils.default_config()) def connect(opts) do conf = opts |> Bolt.Sips.Utils.default_config() host = _to_hostname(conf[:hostname]) port = conf[:port] auth = extract_auth(conf[:basic_auth]) timeout = conf[:timeout] socket = conf[:socket] default_socket_options = [packet: :raw, mode: :binary, active: false] socket_opts = case conf[:ssl] do list when is_list(list) -> Keyword.merge(default_socket_options, conf[:ssl]) _ -> default_socket_options end with {:ok, sock} <- socket.connect(host, port, socket_opts, timeout), {:ok, bolt_version} <- BoltProtocol.handshake(socket, sock), {:ok, server_version} <- do_init(socket, sock, bolt_version, auth), :ok <- socket.setopts(sock, active: :once) do {:ok, %ConnData{ sock: sock, bolt_version: bolt_version, configuration: Keyword.merge(conf, server_version: server_version) }} else {:error, %BoltError{}} = error -> error {:error, reason} -> {:error, BoltError.exception(reason, nil, :connect)} end end defp do_init(transport, port, 3, auth) do BoltProtocol.hello(transport, port, 3, auth) end defp do_init(transport, port, bolt_version, auth) do BoltProtocol.init(transport, port, bolt_version, auth) end @doc "Callback for DBConnection.checkout/1" def checkout(%ConnData{sock: sock, configuration: conf} = conn_data) do case conf[:socket].setopts(sock, active: false) do :ok -> {:ok, conn_data} other -> other end end @doc "Callback for DBConnection.checkin/1" def checkin(%ConnData{sock: sock, configuration: conf} = conn_data) do case conf[:socket].setopts(sock, active: :once) do :ok -> {:ok, conn_data} other -> other end end def disconnect(_err, %ConnData{sock: sock, bolt_version: 3, configuration: conf} = conn_data) do socket = conf[:socket] :ok = BoltProtocol.goodbye(socket, sock, conn_data.bolt_version) socket.close(sock) :ok end @doc "Callback for DBConnection.disconnect/1" def disconnect(_err, %ConnData{sock: sock, configuration: conf}) do conf[:socket].close(sock) :ok end @doc "Callback for DBConnection.handle_begin/1" def handle_begin(_, %ConnData{sock: sock, bolt_version: 3, configuration: conf} = conn_data) do {:ok, _} = BoltProtocol.begin(conf[:socket], sock, conn_data.bolt_version) {:ok, :began, conn_data} end def handle_begin(_opts, conn_data) do %QueryStatement{statement: "BEGIN"} |> handle_execute(%{}, [], conn_data) {:ok, :began, conn_data} end @doc "Callback for DBConnection.handle_rollback/1" def handle_rollback(_, %ConnData{sock: sock, bolt_version: 3, configuration: conf} = conn_data) do :ok = BoltProtocol.rollback(conf[:socket], sock, conn_data.bolt_version) {:ok, :rolledback, conn_data} end def handle_rollback(_opts, conn_data) do %QueryStatement{statement: "ROLLBACK"} |> handle_execute(%{}, [], conn_data) {:ok, :rolledback, conn_data} end @doc "Callback for DBConnection.handle_commit/1" def handle_commit(_, %ConnData{sock: sock, bolt_version: 3, configuration: conf} = conn_data) do {:ok, _} = BoltProtocol.commit(conf[:socket], sock, conn_data.bolt_version) {:ok, :committed, conn_data} end def handle_commit(_opts, conn_data) do %QueryStatement{statement: "COMMIT"} |> handle_execute(%{}, [], conn_data) {:ok, :committed, conn_data} end @doc "Callback for DBConnection.handle_execute/1" def handle_execute(query, params, opts, conn_data) do execute(query, params, opts, conn_data) end def handle_info(msg, state) do Logger.error(fn -> [inspect(__MODULE__), ?\s, inspect(self()), " received unexpected message: " | inspect(msg)] end) {:ok, state} end ### Calming the warnings # Callbacks for ... def ping(state), do: {:ok, state} def handle_prepare(query, _opts, state), do: {:ok, query, state} def handle_close(query, _opts, state), do: {:ok, query, state} def handle_deallocate(query, _cursor, _opts, state), do: {:ok, query, state} def handle_declare(query, _params, _opts, state), do: {:ok, query, state, nil} def handle_fetch(query, _cursor, _opts, state), do: {:cont, query, state} def handle_status(_opts, state), do: {:idle, state} defp extract_auth(nil), do: {} defp extract_auth(basic_auth), do: {basic_auth[:username], basic_auth[:password]} defp execute(q, params, _, conn_data) do %QueryStatement{statement: statement} = q %ConnData{sock: sock, bolt_version: bolt_version, configuration: conf} = conn_data socket = conf |> Keyword.get(:socket) case BoltProtocol.run_statement(socket, sock, bolt_version, statement, params) do [{:success, _} | _] = data -> {:ok, q, data, conn_data} %BoltError{type: :cypher_error} = error -> BoltProtocol.reset(socket, sock, bolt_version) {:error, error, conn_data} %BoltError{type: :connection_error} = error -> {:disconnect, error, conn_data} %BoltError{} = error -> {:error, error, conn_data} end rescue e -> msg = case e do %Bolt.Sips.Internals.PackStreamError{} -> "unable to encode value: #{inspect(e.data)}" %BoltError{} -> "#{e.message}, type: #{e.type}" _ -> e.message end {:error, %{code: :failure, message: msg}, conn_data} end defp _to_hostname(hostname) when is_binary(hostname), do: String.to_charlist(hostname) defp _to_hostname(hostname) when is_list(hostname), do: hostname defp _to_hostname(hostname), do: hostname end ================================================ FILE: lib/bolt_sips/query.ex ================================================ defmodule Bolt.Sips.Query do @moduledoc """ Provides a simple Query DSL. You can run simple Cypher queries with or w/o parameters, for example: {:ok, row} = Bolt.Sips.query(conn, "match (n:Person {bolt_sips: true}) return n.name as Name limit 5") assert List.first(row)["Name"] == "Patrick Rothfuss" Or more complex ones: cypher = \""" MATCH (p:Person {bolt_sips: true}) RETURN p, p.name AS name, upper(p.name) as NAME, coalesce(p.nickname,"n/a") AS nickname, { name: p.name, label:head(labels(p))} AS person \""" {:ok, r} = Bolt.Sips.query(conn, cypher) As you can see, you can organize your longer queries using the Elixir multiple line conventions, for readability. And there is one more trick, you can use for more complex Cypher commands: use `;` as a transactional separator. For example, say you want to clean up the test database **before** creating some tests entities. You can do that like this: cypher = \""" MATCH (n {bolt_sips: true}) OPTIONAL MATCH (n)-[r]-() DELETE n,r; CREATE (BoltSips:BoltSips {title:'Elixir sipping from Neo4j, using Bolt', released:2016, license:'MIT', bolt_sips: true}) CREATE (TNOTW:Book {title:'The Name of the Wind', released:2007, genre:'fantasy', bolt_sips: true}) CREATE (Patrick:Person {name:'Patrick Rothfuss', bolt_sips: true}) CREATE (Kvothe:Person {name:'Kote', bolt_sips: true}) CREATE (Denna:Person {name:'Denna', bolt_sips: true}) CREATE (Chandrian:Deamon {name:'Chandrian', bolt_sips: true}) CREATE (Kvothe)-[:ACTED_IN {roles:['sword fighter', 'magician', 'musician']}]->(TNOTW), (Denna)-[:ACTED_IN {roles:['many talents']}]->(TNOTW), (Chandrian)-[:ACTED_IN {roles:['killer']}]->(TNOTW), (Patrick)-[:WROTE]->(TNOTW) \""" assert {:ok, _r} = Bolt.Sips.query(conn, cypher) In the example above, this command: `MATCH (n {bolt_sips: true}) OPTIONAL MATCH (n)-[r]-() DELETE n,r;` will be executed in a distinct transaction, before all the other queries See the various tests, or more examples and implementation details. """ alias Bolt.Sips.{QueryStatement, Response, Types, Error, Exception} @cypher_seps ~r/;(.){0,1}\n/ @spec query!(Bolt.Sips.conn(), String.t()) :: Response.t() | Exception.t() def query!(conn, statement), do: query!(conn, statement, %{}) @spec query!(Bolt.Sips.conn(), String.t(), map, Keyword.t()) :: Response.t() | Exception.t() def query!(conn, statement, params, opts \\ []) when is_map(params) do with {:ok, r} <- query_commit(conn, statement, params, opts) do r else {:error, msg} -> raise Exception, message: msg e -> raise Exception, message: "unexpected error: #{inspect(e)}" end end @spec query(Bolt.Sips.conn(), String.t()) :: {:error, Error.t()} | {:ok, Response.t()} def query(conn, statement), do: query(conn, statement, %{}) @spec query(Bolt.Sips.conn(), String.t(), map, Keyword.t()) :: {:error, Error.t()} | {:ok, Response.t()} def query(conn, statement, params, opts \\ []) when is_map(params) do case query_commit(conn, statement, params, opts) do {:error, message} -> {:error, %Error{message: message}} r -> r end rescue e in Bolt.Sips.Exception -> {:error, %Bolt.Sips.Error{code: e.code, message: e.message}} end ########### # Private # ########### @spec query_commit(Bolt.Sips.conn(), String.t(), map, Keyword.t()) :: {:error, Error.t()} | {:ok, Response.t()} defp query_commit(conn, statement, params, opts) do statements = statement |> String.split(@cypher_seps, trim: true) |> Enum.map(&String.trim/1) |> Enum.filter(&(String.length(&1) > 0)) formatted_params = params |> Enum.map(&format_param/1) |> Enum.map(fn {k, {:ok, value}} -> {k, value} end) |> Map.new() errors = formatted_params |> Enum.filter(fn {_, formated} -> case formated do {:error, _} -> true _ -> false end end) |> Enum.map(fn {k, {:error, error}} -> {k, error} end) {:ok, commit!(errors, conn, statements, formatted_params, opts)} rescue e in [RuntimeError, DBConnection.ConnectionError] -> {:error, Bolt.Sips.Error.new(e.message)} e in Exception -> {:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace) # n/a in newer Elixir version: # reraise e, __STACKTRACE__ # using a safe call, for backward compatibility reraise e, stacktrace e -> {:error, e} end defp commit!([], conn, statements, formatted_params, opts), do: tx!(conn, statements, formatted_params, opts) defp commit!(errors, _conn, _statements, _formatted_params, _opts), do: raise(Exception, message: "Unable to format params: #{inspect(errors)}") defp tx!(conn, [statement], params, opts), do: hd(send!(conn, statement, params, opts)) # todo It returns [Response.t] !!! defp tx!(conn, statements, params, opts) when is_list(statements), do: Enum.reduce(statements, [], &send!(conn, &1, params, opts, &2)) defp send!(conn, statement, params, opts, acc \\ []) defp send!(conn, statement, params, opts, acc) do # Retrieve timeout defined in config prefix = Keyword.get(opts, :prefix, :default) conf_timeout = Bolt.Sips.info() |> Map.get(prefix) |> Map.get(:user_options) |> Keyword.get(:timeout) opts = Keyword.put_new(opts, :timeout, Keyword.get(opts, :timeout, conf_timeout)) case DBConnection.execute(conn, %QueryStatement{statement: statement}, params, opts) do {:ok, _query, resp} -> case Response.transform(resp) do {:ok, %Response{} = r} -> acc ++ [r] {:error, e} -> raise DBConnection.ConnectionError, message: e end {:error, %Bolt.Sips.Internals.Error{code: code, message: msg}} -> raise Exception, code: code, message: msg {:error, %{message: message}} -> raise DBConnection.ConnectionError, message: message end end # Format the param to be used in query # must return a tuple of the form: {:ok, param} or {:error, param} # In order to let query_commit handle the error @spec format_param({String.t(), any()}) :: {String.t(), {:ok | :error, any()}} defp format_param({name, %Types.Duration{} = duration}), do: {name, Types.Duration.format_param(duration)} defp format_param({name, %Types.Point{} = point}), do: {name, Types.Point.format_param(point)} defp format_param({name, value}), do: {name, {:ok, value}} end ================================================ FILE: lib/bolt_sips/query_statement.ex ================================================ defmodule Bolt.Sips.QueryStatement do @moduledoc false defstruct statement: "" end defimpl DBConnection.Query, for: Bolt.Sips.QueryStatement do def describe(query, _), do: query def parse(query, _), do: query def encode(_query, data, _), do: data def decode(_, result, _), do: result end ================================================ FILE: lib/bolt_sips/response.ex ================================================ defmodule Bolt.Sips.Response do @moduledoc """ Support for transforming a Bolt response to a list of Bolt.Sips.Types or arbitrary values. A Bolt.Sips.Response is used for mapping any response received from a Neo4j server into a an Elixir struct. You'll interact with this module every time you're needing to get and manipulate the data resulting from your queries. For example, a simple Cypher query like this: Bolt.Sips.query(Bolt.Sips.conn(), "RETURN [10,11,21] AS arr") will return: {:ok, %Bolt.Sips.Response{ bookmark: "neo4j:bookmark:v1:tx21868", fields: ["arr"], notifications: [], plan: nil, profile: nil, records: [[[10, 11, 21]]], results: [%{"arr" => [10, 11, 21]}], stats: [], type: "r" }} and while you have now access to the full data returned by Neo4j, most of the times you'll just want the results: iex> %Bolt.Sips.Response{results: results} = Bolt.Sips.query!(Bolt.Sips.conn(), "RETURN [10,11,21] AS arr") iex> results [%{"arr" => [10, 11, 21]}] More complex queries, i.e.: MATCH p=({name:'Alice'})-[r:KNOWS]->({name:'Bob'}) RETURN r will return `results` like this: [%{"r" => %Bolt.Sips.Types.Relationship{end: 647, id: 495, properties: %{}, start: 646, type: "KNOWS"}}] Note: the `Path` has also functionality for "drawing" a graph, from a given node-relationship path Our Bolt.Sips.Response is implementing Elixir's [Enumerable Protocol](https://hexdocs.pm/elixir/Enumerable.html), to help you accessing the results. Hence something like this, is possible: iex> Bolt.Sips.query!(Bolt.Sips.conn(), "RETURN [10,11,21] AS arr") |> ...> Enum.reduce(0, &(Enum.sum(&1["arr"]) + &2)) 42 an overly complicated example, but you get the point?! :) You can also quickly get the `first` of the results, returned by Neo4j. Example: iex> Bolt.Sips.query!(Bolt.Sips.conn(), "UNWIND range(1, 10) AS n RETURN n") |> ...> Bolt.Sips.Response.first() %{"n" => 1} """ @type t :: %__MODULE__{ results: list, fields: list, records: list, plan: map, notifications: list, stats: list | map, profile: any, type: String.t(), bookmark: String.t() } @type key :: any @type value :: any @type acc :: any @type element :: any defstruct results: [], fields: nil, records: [], plan: nil, notifications: [], stats: [], profile: nil, type: nil, bookmark: nil alias Bolt.Sips.Error require Logger require Integer def first(%__MODULE__{results: []}), do: nil def first(%__MODULE__{results: [head | _tail]}), do: head @doc false # transform a raw Bolt response to a list of Responses def transform!(records, stats \\ :no) do with {:ok, %__MODULE__{} = records} <- transform(records, stats) do records else e -> e end end def transform(records, _stats \\ :no) do # records |> IO.inspect(label: "raw>> lib/bolt_sips/response.ex:44") with %__MODULE__{fields: fields, records: records} = response <- parse(records) do {:ok, %__MODULE__{response | results: create_results(fields, records)}} else e -> {:error, e} end rescue e in Bolt.Sips.Exception -> {:error, e.message} e -> {:error, e} end def fetch(%Bolt.Sips.Response{fields: fields, results: results}, key) do with true <- Enum.member?(fields, key) do findings = Enum.map(results, fn map -> Map.get(map, key) end) |> Enum.filter(&(&1 != nil)) {:ok, findings} else _ -> nil end end def fetch!(%Bolt.Sips.Response{} = r, key) do with {:ok, findings} <- fetch(r, key) do findings end end defp parse(records) do with {err_type, error} when err_type in ~w(halt error failure)a <- Error.new(records) do {:error, error} else records -> response = records |> Enum.reduce(%__MODULE__{}, fn {type, record}, acc -> with {:error, e} <- parse_record(type, record, acc) do raise Bolt.Sips.Exception, e else acc -> acc end end) %__MODULE__{response | records: response.records |> :lists.reverse()} end end # defp parse_record(:success, %{"fields" => fields, "t_first" => 0}, response) do defp parse_record(:success, %{"fields" => fields}, response) do %{response | fields: fields} end defp parse_record(:success, %{"profile" => profile, "type" => type} = record, response) do %{response | profile: profile, stats: Map.get(record, "stats", []), type: type} end defp parse_record(:success, %{"notifications" => n, "plan" => plan, "type" => type}, response) do %{response | plan: plan, notifications: n, type: type} end defp parse_record(:success, %{"plan" => plan, "type" => type}, response) do %{response | plan: plan, type: type} end defp parse_record(:success, %{"stats" => stats, "type" => type}, response) do %{response | stats: stats, type: type} end defp parse_record(:success, %{"bookmark" => bookmark, "type" => type}, response) do %{response | bookmark: bookmark, type: type} end defp parse_record(:success, %{"type" => type}, response) do %{response | type: type} end defp parse_record(:success, %{}, response) do %{response | type: "boltkit?"} end defp parse_record(:success, record, _response) do line = "; around: #{String.replace_leading("#{__ENV__.file}", "#{File.cwd!()}", "") |> Path.relative()}:#{__ENV__.line()}" err_msg = "UNKNOWN success type: " <> inspect(record) <> line Logger.error(err_msg) {:error, Bolt.Sips.Error.new(err_msg)} end # defp parse_record(:record, %{"bookmark" => "neo4j:bookmark:v1:tx14519", "t_last" => 1, "type" => "r"}, response) do defp parse_record(:record, record, response) do %{response | records: [record | response.records]} end defp parse_record(_type, record, _response) do line = "; around: #{String.replace_leading("#{__ENV__.file}", "#{File.cwd!()}", "") |> Path.relative()}:#{__ENV__.line()}" err_msg = "UNKNOWN `:record`: " <> inspect(record) <> line Logger.error(err_msg) {:error, Bolt.Sips.Error.new(err_msg)} end defp create_results(fields, records) do records |> Enum.map(fn recs -> Enum.zip(fields, recs) end) |> Enum.map(fn data -> Enum.into(data, %{}) end) end end ================================================ FILE: lib/bolt_sips/response_encoder/json/jason.ex ================================================ if Code.ensure_loaded?(Jason) do defmodule Bolt.Sips.ResponseEncoder.Json.Jason do @moduledoc """ A default implementation for Jason encoding library. More info about Jason: [https://hex.pm/packages/jason](https://hex.pm/packages/jason) Allow this usage: ``` conn = Bolt.Sips.conn() {:ok, res} = Bolt.Sips.query(conn, "MATCH (t:TestNode) RETURN t") Jason.encode!(res) ``` Default implementation can be overriden by providing your own implementation. More info about implementation: [https://hexdocs.pm/jason/readme.html#differences-to-poison](https://hexdocs.pm/jason/readme.html#differences-to-poison) #### Note: In order to benefit from Bolt.Sips.ResponseEncoder implementation, use `Bolt.Sips.ResponseEncoder.Json.encode` and pass the result to the Jason encoding functions. """ alias Bolt.Sips.Types alias Bolt.Sips.ResponseEncoder.Json defimpl Jason.Encoder, for: [Types.Node, Types.Relationship, Types.Path, Types.Point] do @spec encode(struct(), Jason.Encode.opts()) :: iodata() def encode(data, opts) do data |> Json.encode() |> Jason.Encode.map(opts) end end defimpl Jason.Encoder, for: [Types.DateTimeWithTZOffset, Types.TimeWithTZOffset, Types.Duration] do @spec encode(struct(), Jason.Encode.opts()) :: iodata() def encode(data, opts) do data |> Json.encode() |> Jason.Encode.string(opts) end end end end ================================================ FILE: lib/bolt_sips/response_encoder/json/poison.ex ================================================ if Code.ensure_loaded?(Poison) do defmodule Bolt.Sips.ResponseEncoder.Json.Poison do @moduledoc """ A default implementation for Poison encoding library. More info about poison here: [https://hex.pm/packages/poison](https://hex.pm/packages/poison) Allow this usage: ``` conn = Bolt.Sips.conn() {:ok, res} = Bolt.Sips.query(conn, "MATCH (t:TestNode) RETURN t") Poison.encode!(res) ``` Default implementation can be overriden by providing your own implementation. More info about implementation: [https://hexdocs.pm/poison/Poison.html#module-encoder](https://hexdocs.pm/poison/Poison.html#module-encoder) #### Note: In order to benefit from Bolt.Sips.ResponseEncoder implementation, use `Bolt.Sips.ResponseEncoder.Json.encode` and pass the result to the Poison encoding functions. """ alias Bolt.Sips.Types alias Bolt.Sips.ResponseEncoder.Json defimpl Poison.Encoder, for: [Types.Node, Types.Relationship, Types.Path, Types.Point] do @spec encode(struct(), Poison.Encoder.options()) :: iodata def encode(data, opts) do data |> Json.encode() |> Poison.Encoder.Map.encode(opts) end end defimpl Poison.Encoder, for: [Types.DateTimeWithTZOffset, Types.TimeWithTZOffset, Types.Duration] do @spec encode(struct(), Poison.Encoder.options()) :: iodata def encode(data, opts) do data |> Json.encode() |> Poison.Encoder.BitString.encode(opts) end end end end ================================================ FILE: lib/bolt_sips/response_encoder/json.ex ================================================ defprotocol Bolt.Sips.ResponseEncoder.Json do @moduledoc """ Protocol controlling how a value is made jsonable. Its only purpose is to convert Bolt Sips specific structures into elixir buit-in types which can be encoed in json by Jason. ## Deriving If the provided default implementation don't fit your need, you can override with your own implementation. ### Example Let's assume that you don't want Node's id available as they are Neo4j's ones and are not reliable because of id reuse and you want to have you own `uuid` in place. Instead of: ``` { id: 0, labels: ["TestNode"], properties: { uuid: "837806a7-6c37-4630-9f6c-9aa7ad0129ed" value: "my node" } } ``` you want: ``` { uuid: "837806a7-6c37-4630-9f6c-9aa7ad0129ed", labels: ["TestNode"], properties: { value: "my node" } } ``` You can achieve that with the following implementation: ``` defimpl Bolt.Sips.ResponseEncoder.Json, for: Bolt.Sips.Types.Node do def encode(node) do new_props = Map.drop(node.properties, :uuid) node |> Map.from_struct() |> Map.put(:uuid, node.properties.uuid) |> Map.put(:properties, new_props) end end ``` It is also possible to provide implementation that returns structs or updated Bolt.Sips.Types, the use of a final `Bolt.Sips.ResponseEncoder.Json.encode()` will ensure that these values will be converted to jsonable ones. """ @fallback_to_any true @doc """ Convert a value in a jsonable format """ @spec encode(any()) :: any() def encode(value) end alias Bolt.Sips.{Types, ResponseEncoder} defimpl ResponseEncoder.Json, for: Types.DateTimeWithTZOffset do @spec encode(Types.DateTimeWithTZOffset.t()) :: String.t() def encode(value) do {:ok, dt} = Types.DateTimeWithTZOffset.format_param(value) ResponseEncoder.Json.encode(dt) end end defimpl ResponseEncoder.Json, for: Types.TimeWithTZOffset do @spec encode(Types.TimeWithTZOffset.t()) :: String.t() def encode(struct) do {:ok, t} = Types.TimeWithTZOffset.format_param(struct) ResponseEncoder.Json.encode(t) end end defimpl ResponseEncoder.Json, for: Types.Duration do @spec encode(Types.Duration.t()) :: String.t() def encode(struct) do {:ok, d} = Types.Duration.format_param(struct) ResponseEncoder.Json.encode(d) end end defimpl ResponseEncoder.Json, for: Types.Point do @spec encode(Types.Point.t()) :: map() def encode(struct) do {:ok, pt} = Types.Point.format_param(struct) ResponseEncoder.Json.encode(pt) end end defimpl ResponseEncoder.Json, for: [Types.Node, Types.Relationship, Types.UnboundRelationship, Types.Path] do @spec encode(struct()) :: map() def encode(value) do value |> Map.from_struct() |> ResponseEncoder.Json.encode() end end defimpl ResponseEncoder.Json, for: Any do @spec encode(any()) :: any() def encode(value) when is_list(value) do value |> Enum.map(&ResponseEncoder.Json.encode/1) end def encode(%{__struct__: _} = value) do value |> Map.from_struct() |> ResponseEncoder.Json.encode() end def encode(value) when is_map(value) do value |> Enum.into(%{}, fn {k, val} -> {k, ResponseEncoder.Json.encode(val)} end) end def encode(value) do value end end ================================================ FILE: lib/bolt_sips/response_encoder.ex ================================================ defmodule Bolt.Sips.ResponseEncoder do @moduledoc """ This module provides functions to encode a query result or data containing Bolt.Sips.Types into various format. For now, only JSON is supported. Encoding is handled by protocols to allow override if a specific implemention is required. See targeted protocol documentation for more information """ @doc """ Encode the data in json format. This is done is 2 steps: - first, the data is converted into a jsonable format - the result is encoded in json via Jason Both of these steps are overridable, see: - for step 1: `Bolt.Sips.ResponseEncoder.Json` - for step 2 (depending of your preferred library): - `Bolt.Sips.ResponseEncoder.Json.Jason` - `Bolt.Sips.ResponseEncoder.Json.Poison` ## Example iex> data = %{"t1" => %Bolt.Sips.Types.Node{ ...> id: 69, ...> labels: ["Test"], ...> properties: %{ ...> "created" => %Bolt.Sips.Types.DateTimeWithTZOffset{ ...> naive_datetime: ~N[2016-05-24 13:26:08.543], ...> timezone_offset: 7200 ...> }, ...> "uuid" => 12345 ...> } ...> } ...> } iex> Bolt.Sips.ResponseEncoder.encode(data, :json) {:ok, ~S|{"t1":{"id":69,"labels":["Test"],"properties":{"created":"2016-05-24T13:26:08.543+02:00","uuid":12345}}}|} iex> Bolt.Sips.ResponseEncoder.encode("\\xFF", :json) {:error, %Jason.EncodeError{message: "invalid byte 0xFF in <<255>>"}} """ @spec encode(any(), :json) :: {:ok, String.t()} | {:error, Jason.EncodeError.t() | Exception.t()} def encode(response, :json) do response |> jsonable_response() |> Jason.encode() end @doc """ Encode the data in json format. Similar to `encode/1` except it will unwrap the error tuple and raise in case of errors. ## Example iex> data = %{"t1" => %Bolt.Sips.Types.Node{ ...> id: 69, ...> labels: ["Test"], ...> properties: %{ ...> "created" => %Bolt.Sips.Types.DateTimeWithTZOffset{ ...> naive_datetime: ~N[2016-05-24 13:26:08.543], ...> timezone_offset: 7200 ...> }, ...> "uuid" => 12345 ...> } ...> } ...> } iex> Bolt.Sips.ResponseEncoder.encode!(data, :json) ~S|{"t1":{"id":69,"labels":["Test"],"properties":{"created":"2016-05-24T13:26:08.543+02:00","uuid":12345}}}| iex> Bolt.Sips.ResponseEncoder.encode!("\\xFF", :json) ** (Jason.EncodeError) invalid byte 0xFF in <<255>> """ @spec encode!(any(), :json) :: String.t() | no_return() def encode!(response, :json) do response |> jsonable_response() |> Jason.encode!() end defp jsonable_response(response) do response |> Bolt.Sips.ResponseEncoder.Json.encode() end end ================================================ FILE: lib/bolt_sips/router.ex ================================================ defmodule Bolt.Sips.Router do @moduledoc """ This "driver" works in tandem with Neo4j's [Causal Clustering](https://neo4j.com/docs/operations-manual/current/clustering/>) feature by directing read and write behaviour to appropriate cluster members """ use GenServer require Logger alias Bolt.Sips.Routing.RoutingTable alias Bolt.Sips.{Protocol, ConnectionSupervisor, LoadBalancer, Response, Error} defmodule State do @moduledoc """ todo: this is work in progress and will be used for defining the state of the Router (Gen)Server """ @type role :: atom @type t :: %__MODULE__{ connections: %{ role => %{String.t() => non_neg_integer}, updated_at: non_neg_integer, ttl: non_neg_integer } } @enforce_keys [:connections] defstruct @enforce_keys end @no_routing nil @routing_table_keys [:read, :write, :route, :updated_at, :ttl, :error] def configure(opts), do: GenServer.call(__MODULE__, {:configure, opts}) def get_connection(role, prefix \\ :direct) def get_connection(role, prefix), do: GenServer.call(__MODULE__, {:get_connection, role, prefix}) def terminate_connections(role, prefix \\ :default) def terminate_connections(role, prefix), do: GenServer.call(__MODULE__, {:terminate_connections, role, prefix}) def info(), do: GenServer.call(__MODULE__, :info) def routing_table(prefix), do: GenServer.call(__MODULE__, {:routing_table_info, prefix}) @spec start_link(Keyword.t()) :: :ignore | {:error, Keyword.t()} | {:ok, pid()} def start_link(init_args) do GenServer.start_link(__MODULE__, init_args, name: __MODULE__) end @impl true @spec init(Keyword.t()) :: {:ok, State.t(), {:continue, :post_init}} def init(options) do {:ok, options, {:continue, :post_init}} end @impl true def handle_call({:configure, opts}, _from, state) do prefix = Keyword.get(opts, :prefix, :default) %{connections: current_connections} = Map.get(state, prefix, %{connections: %{}}) %{user_options: user_options, connections: connections} = try do opts |> _configure() |> Map.get(prefix) rescue e in Bolt.Sips.Exception -> %{user_options: opts, connections: %{error: e.message}} end updated_connections = Map.merge(current_connections, connections) new_state = Map.put(state, prefix, %{user_options: user_options, connections: updated_connections}) {:reply, new_state, new_state} end # getting connections for role in [:route, :read, :write] @impl true def handle_call({:get_connection, role, prefix}, _from, state) when role in [:route, :read, :write] do with %{connections: connections} <- Map.get(state, prefix), {:ok, conn, updated_connections} <- _get_connection(role, connections, prefix) do {:reply, {:ok, conn}, put_in(state, [prefix, :connections], updated_connections)} else e -> err_msg = error_no_connection_available_for_role(role, e, prefix) {:reply, {:error, err_msg}, state} end end # getting connections for any user defined roles, or: `:direct` @impl true def handle_call({:get_connection, role, prefix}, _from, state) do with %{connections: connections} <- Map.get(state, prefix), true <- Map.has_key?(connections, role), [url | _none] <- connections |> Map.get(role) |> Map.keys(), {:ok, pid} <- ConnectionSupervisor.find_connection(role, url, prefix) do {:reply, {:ok, pid}, state} else e -> err_msg = error_no_connection_available_for_role(role, e, prefix) {:reply, {:error, err_msg}, state} end end @impl true def handle_call({:terminate_connections, role, prefix}, _from, state) do %{connections: connections} = Map.get(state, prefix, %{}) with true <- Map.has_key?(connections, role), :ok <- connections |> Map.get(role) |> Map.keys() |> Enum.each(&ConnectionSupervisor.terminate_connection(role, &1, prefix)) do new_connections = Map.delete(connections, role) new_state = put_in(state, [prefix, :connections], new_connections) {:reply, :ok, new_state} else _e -> {:reply, {:error, :not_found}, state} end end @impl true def handle_call(:info, _from, state), do: {:reply, state, state} def handle_call({:routing_table_info, prefix}, _from, state) do routing_table = with connections when not is_nil(connections) <- get_in(state, [prefix, :connections]) do Map.take(connections, @routing_table_keys) end {:reply, routing_table, state} end @impl true @spec handle_continue(:post_init, Keyword.t()) :: {:noreply, map} def handle_continue(:post_init, opts), do: {:noreply, _configure(opts)} defp _configure(opts) do options = Bolt.Sips.Utils.default_config(opts) prefix = Keyword.get(options, :prefix, :default) ssl_or_sock = if(Keyword.get(options, :ssl), do: :ssl, else: Keyword.get(options, :socket)) user_options = Keyword.put(options, :socket, ssl_or_sock) with_routing? = Keyword.get(user_options, :schema, "bolt") =~ ~r/(^bolt\+routing$)/i with {:ok, routing_table} <- get_routing_table(user_options, with_routing?), {:ok, connections} <- start_connections(user_options, routing_table) do connections = Map.put(connections, :routing_query, routing_table[:routing_query]) %{prefix => %{user_options: user_options, connections: connections}} else {:error, msg} -> Logger.error("cannot load the routing table. Error: #{msg}") %{prefix => %{user_options: user_options, connections: %{error: "Not a router"}}} end end defp get_routing_table( %{routing_query: %{params: props, query: query}} = connections, _, prefix ) do with {:ok, conn, updated_connections} <- _get_connection(:route, connections, prefix), {:ok, %Response{} = results} <- Bolt.Sips.query(conn, query, props) do {:ok, Response.first(results), updated_connections} else {:error, %Error{code: code, message: message}} -> err_msg = "#{code}; #{message}" Logger.error(err_msg) {:error, err_msg} {:error, msg, _updated_connections} -> Logger.error(msg) {:error, :routing_table_not_available} e -> Logger.error("get_routing_table error: #{inspect(e)}") {:error, :routing_table_not_available_at_all} end end defp get_routing_table(_opts, false), do: {:ok, @no_routing} defp get_routing_table(opts, _) do prefix = Keyword.get(opts, :prefix, :default) with {:ok, %Protocol.ConnData{configuration: configuration}} <- Protocol.connect(opts), # DON'T> :ok <- Protocol.disconnect(:stop, conn), {_long, short} <- parse_server_version(configuration[:server_version]) do {query, params} = if Version.match?(short, ">= 3.2.3") do props = Keyword.get(opts, :routing_context, %{}) {"CALL dbms.cluster.routing.getRoutingTable($context)", %{context: props}} else {"CALL dbms.cluster.routing.getServers()", %{}} end with {:ok, pid} <- DBConnection.start_link(Protocol, Keyword.delete(opts, :name)), {:ok, %Response{} = results} <- Bolt.Sips.query(pid, query, params), true <- Process.exit(pid, :normal) do table = results |> Response.first() |> Map.put(:routing_query, %{query: query, params: params}) ttl = Map.get(table, :ttl, 300) * 1000 # may overwrite the ttl, when desired in exceptional situations: tests, for example. ttl = Keyword.get(opts, :ttl, ttl) Process.send_after(self(), {:refresh, prefix}, ttl) {:ok, table} else {:error, %Error{message: message}} -> Logger.error(message) {:error, message} _e -> "Are you sure you're connected to a Neo4j cluster? The routing table, is not available." |> Logger.error() {:error, :routing_table_not_available} end end end @doc """ start a new (DB)Connection process, supervised registered under a name following this convention: - "role@hostname:port", the `role`, `hostname` and the `port` are collected from the user's configuration: `opts`. The `role` parameter is ignored when the `routing_table` parameter represents a neo4j map containing the definition for a neo4j cluster! It defaults to: `:direct`, when not specified! """ def start_connections(opts, routing_table) def start_connections(opts, routing_table) when is_nil(routing_table) do url = "#{opts[:hostname]}:#{opts[:port]}" role = Keyword.get(opts, :role, :direct) with {:ok, _pid} <- ConnectionSupervisor.start_child(role, url, opts) do {:ok, %{role => %{url => 0}}} end end def start_connections(opts, routing_table) do connections = with %Bolt.Sips.Routing.RoutingTable{roles: roles} = rt <- RoutingTable.parse(routing_table) do roles |> Enum.reduce(%{}, fn {role, addresses}, acc -> addresses |> Enum.reduce(acc, fn {address, count}, acc -> # interim hack; force the schema to be `bolt`, otherwise the parse is not happening url = "bolt://" <> address %URI{host: host, port: port} = URI.parse(url) # Important! # We remove the url from the routing-specific configs, because the port and the address where the # socket will be opened, is using the host and the port returned by the routing table, and not by the # initial url param. The Utils will overwrite them if the `url` is defined! config = opts |> Keyword.put(:host, String.to_charlist(host)) |> Keyword.put(:port, port) |> Keyword.put(:name, role) |> Keyword.put(:hits, count) |> Keyword.delete(:url) with {:ok, _pid} <- ConnectionSupervisor.start_child(role, address, config) do Map.update(acc, role, %{address => 0}, fn urls -> Map.put(urls, address, 0) end) else _ -> acc end end) |> Map.merge(acc) end) |> Map.put(:ttl, rt.ttl) |> Map.put(:updated_at, rt.updated_at) end {:ok, connections} end @with_routing true @impl true def handle_info({:refresh, prefix}, state) do %{connections: connections, user_options: user_options} = Map.get(state, prefix) %{ttl: ttl} = connections # may overwrite the ttl, when desired in exceptional situations: tests, for example. ttl = Keyword.get(user_options, :ttl, ttl) state = with {:ok, routing_table, _updated_connections} <- get_routing_table(connections, @with_routing, prefix), {:ok, new_connections} <- start_connections(user_options, routing_table) do connections = connections |> Map.put(:updated_at, Bolt.Sips.Utils.now()) |> merge_connections_maps(new_connections, prefix) ttl = Keyword.get(user_options, :ttl, ttl * 1000) Process.send_after(self(), {:refresh, prefix}, ttl) new_state = %{user_options: user_options, connections: connections} Map.put(state, prefix, new_state) else e -> Logger.error("Cannot create any connections. Error: #{inspect(e)}") Map.put(state, prefix, %{user_options: user_options, connections: %{}}) end {:noreply, state} end def handle_info(req, state) do Logger.warning("An unusual request: #{inspect(req)}") {:noreply, state} end @server_version_stringex ~r/Neo4j\/(?\d+)\.(?\d+)\.(?

\d+)/ @spec parse_server_version(map) :: {binary, <<_::16, _::_*8>>} @doc """ parse the version string received from the server, while considering the lack of the patch number in some situations ## Examples iex> Bolt.Sips.Router.parse_server_version(%{"server" => "Neo4j/3.5.0"}) {"Neo4j/3.5.0", "3.5.0"} iex> Bolt.Sips.Router.parse_server_version(%{"server" => "Neo4j/3.5"}) {"Neo4j/3.5", "3.5.0"} iex> Bolt.Sips.Router.parse_server_version(%{"server" => "Neo4j/3.5.10"}) {"Neo4j/3.5.10", "3.5.10"} iex> Bolt.Sips.Router.parse_server_version(%{"server" => "Neo4j/3.5.11.1"}) {"Neo4j/3.5.11.1", "3.5.11"} """ def parse_server_version(%{"server" => server_version_string}) do %{"M" => major, "m" => minor, "p" => patch} = @server_version_stringex |> Regex.named_captures(server_version_string <> ".0") {server_version_string, "#{major}.#{minor}.#{patch}"} end def parse_server_version(some_version), do: raise(ArgumentError, "not a Neo4J version info: " <> inspect(some_version)) defp error_no_connection_available_for_role(role, _e, prefix \\ :default) defp error_no_connection_available_for_role(role, _e, prefix) do "no connection exists with this role: #{role} (prefix: #{prefix})" end @routing_roles ~w{read write route}a @spec merge_connections_maps(any(), any(), any()) :: any() def merge_connections_maps(current_connections, new_connections, prefix \\ :default) def merge_connections_maps(current_connections, new_connections, prefix) do @routing_roles |> Enum.flat_map(fn role -> new_urls = Map.keys(new_connections[role]) current_connections[role] |> Map.keys() |> Enum.flat_map(fn url -> remove_old_urls(role, url, new_urls) end) end) |> close_connections(prefix) @routing_roles |> Enum.reduce(current_connections, fn role, acc -> Map.put(acc, role, new_connections[role]) end) end defp remove_old_urls(role, url, urls), do: if(url in urls, do: [], else: [{role, url}]) # [ # read: "localhost:7689", # write: "localhost:7687", # write: "localhost:7690", # route: "localhost:7688", # route: "localhost:7689" # ] defp close_connections(connections, prefix) do connections |> Enum.each(fn {role, url} -> with {:ok, _pid} = r <- ConnectionSupervisor.terminate_connection(role, url, prefix) do r else {:error, :not_found} -> Logger.debug("#{role}: #{url}; not a valid connection/process. It can't be terminated") end end) end @spec _get_connection(role :: String.t() | atom, state :: map, prefix :: atom) :: {:ok, pid, map} | {:error, any, map} defp _get_connection(role, connections, prefix) do with true <- Map.has_key?(connections, role), {:ok, url} <- LoadBalancer.least_reused_url(Map.get(connections, role)), {:ok, pid} <- ConnectionSupervisor.find_connection(role, url, prefix) do {_, updated_connections} = connections |> get_and_update_in([role, url], fn hits -> {hits, hits + 1} end) {:ok, pid, updated_connections} else e -> err_msg = error_no_connection_available_for_role(role, e) {:error, err_msg, connections} end end end ================================================ FILE: lib/bolt_sips/routing/connection_supervisor.ex ================================================ defmodule Bolt.Sips.ConnectionSupervisor do @moduledoc false use DynamicSupervisor alias Bolt.Sips.Protocol alias Bolt.Sips require Logger @name __MODULE__ def start_link(init_args) do DynamicSupervisor.start_link(__MODULE__, init_args, name: @name) end @impl true def init(_args) do DynamicSupervisor.init(strategy: :one_for_one) end @doc """ the resulting connection name i.e. "write@localhost:7687" will be used to spawn new DBConnection processes, and for finding available connections """ def start_child(role, url, config) do prefix = Keyword.get(config, :prefix, :default) connection_name = "#{prefix}_#{role}@#{url}" role_config = config |> Keyword.put(:role, role) |> Keyword.put(:name, via_tuple(connection_name)) spec = %{ id: connection_name, start: {DBConnection, :start_link, [Protocol, role_config]}, type: :worker, restart: :transient, shutdown: 500 } # [Protocol, role_config]; with {:error, :not_found} <- find_connection(connection_name), {:ok, _pid} = r <- DynamicSupervisor.start_child(@name, spec) do [spec, r] r else {:ok, pid, _info} -> {:ok, pid} {:error, {:already_started, pid}} -> {:ok, pid} pid -> {:ok, pid} end end @spec find_connection(atom, String.t(), atom) :: {:error, :not_found} | {:ok, pid()} def find_connection(role, url, prefix), do: find_connection("#{prefix}_#{role}@#{url}") @spec find_connection(any()) :: {:error, :not_found} | {:ok, pid()} def find_connection(name) do case Registry.lookup(Sips.registry_name(), name) do [{pid, _}] -> {:ok, pid} _ -> {:error, :not_found} end end @spec terminate_connection(atom, String.t(), atom) :: {:error, :not_found} | {:ok, pid()} def terminate_connection(role, url, prefix \\ :default) do with {:ok, pid} = r <- find_connection(role, url, prefix), true <- Process.exit(pid, :normal) do connections() r end end def connections() do _connections() |> Enum.map(fn pid -> Sips.registry_name() |> Registry.keys(pid) |> List.first() end) end defp _connections() do __MODULE__ |> DynamicSupervisor.which_children() |> Enum.filter(fn {_, pid, _type, modules} -> case {pid, modules} do {:restarting, _} -> false {_pid, _} -> true end end) |> Enum.map(fn {_, pid, _, _} -> pid end) end def via_tuple(name) do {:via, Registry, {Bolt.Sips.registry_name(), name}} end end ================================================ FILE: lib/bolt_sips/routing/load_balancer.ex ================================================ defmodule Bolt.Sips.LoadBalancer do @moduledoc """ a simple load balancer used for selecting a server address from a map. The address is selected based on how many hits has; least reused url. """ @doc """ sort by total number of hits and return the least reused url ## Examples iex> least_reused_url(%{"url1" => 10, "url2" => 5}) {:ok, "url2"} iex> least_reused_url(%{}) {:error, :not_found} """ @spec least_reused_url(map) :: {:ok, String.t()} | {:error, :not_found} def least_reused_url(urls) do {url, _hits} = urls |> Enum.sort(fn {_, hits1}, {_, hits2} -> hits1 <= hits2 end) |> List.first() {:ok, url} end end ================================================ FILE: lib/bolt_sips/routing/routing_table.ex ================================================ defmodule Bolt.Sips.Routing.RoutingTable do @moduledoc ~S""" representing the routing table elements There are a couple of ways to get the routing table from the server, for recent Neo4j servers, and with the latest version of Bolt.Sips, you could use this query: Bolt.Sips.query!(Bolt.Sips.conn, "call dbms.cluster.routing.getRoutingTable($props)", %{props: %{}}) [ %{ "servers" => [ %{"addresses" => ["localhost:7687"], "role" => "WRITE"}, %{"addresses" => ["localhost:7689", "localhost:7688"], "role" => "READ"}, %{ "addresses" => ["localhost:7688", "localhost:7689", "localhost:7687"], "role" => "ROUTE" } ], "ttl" => 300 } ] """ @type t :: %__MODULE__{ roles: %{(:read | :write | :route | :direct) => %{String.t() => non_neg_integer}}, updated_at: non_neg_integer, ttl: non_neg_integer } defstruct roles: %{}, ttl: 300, updated_at: 0 alias Bolt.Sips.Utils @write "WRITE" @read "READ" @route "ROUTE" @spec parse(map) :: __MODULE__.t() | {:error, String.t()} def parse(%{"servers" => servers, "ttl" => ttl}) do with {:ok, roles} <- parse_servers(servers), {:ok, ttl} <- parse_ttl(ttl) do %__MODULE__{roles: roles, ttl: ttl, updated_at: Utils.now()} end end def parse(map), do: {:error, "not a valid routing table: " <> inspect(map)} @spec parse_servers(list()) :: {:ok, map()} defp parse_servers(servers) do parsed_servers = servers |> Enum.reduce(%{}, fn %{"addresses" => addresses, "role" => role}, acc -> with {:ok, atomized_role} <- to_atomic_role(role) do roles = addresses |> Enum.reduce(acc, fn address, acc -> Map.update(acc, atomized_role, %{address => 0}, &Map.put(&1, address, 0)) end) roles else _ -> acc end end) {:ok, parsed_servers} end defp to_atomic_role(role) when role in [@read, @write, @route] do atomic_role = case role do @read -> :read @write -> :write @route -> :route _ -> :direct end {:ok, atomic_role} end defp to_atomic_role(_), do: {:error, :alien_role} def parse_ttl(ttl), do: {:ok, ensure_integer(ttl)} @doc false def ttl_expired?(updated_at, ttl) do updated_at + ttl <= Utils.now() end defp ensure_integer(ttl) when is_nil(ttl), do: 0 defp ensure_integer(ttl) when is_binary(ttl), do: String.to_integer(ttl) defp ensure_integer(ttl) when is_integer(ttl), do: ttl defp ensure_integer(ttl), do: raise(ArgumentError, "invalid ttl: " <> inspect(ttl)) end ================================================ FILE: lib/bolt_sips/socket.ex ================================================ defmodule Bolt.Sips.Socket do @moduledoc """ A default socket interface used to communicate to a Neo4j instance. Any other socket implementing the same interface can be used in place of this one. Actually, this module doesn't implement the interface on its own, it delegates calls to the gen_tcp (http://erlang.org/doc/man/gen_tcp.html) and inet (http://erlang.org/doc/man/inet.html) modules. Any of these modules doesn't fully implement the required interface, hence, both of them must be used. """ @doc "Delegates to :gen_tcp.connect/4" defdelegate connect(host, port, opts, timeout), to: :gen_tcp @doc "Delegates to :inet.setopts/2" defdelegate setopts(sock, opts), to: :inet @doc "Delegates to :gen_tcp.send/2" defdelegate send(sock, package), to: :gen_tcp @doc "Delegates to :gen_tcp.recv/3" defdelegate recv(sock, length, timeout), to: :gen_tcp @doc "Delegates to :gen_tcp.recv/2" defdelegate recv(sock, length), to: :gen_tcp @doc "Delegates to :inet.close/1" defdelegate close(sock), to: :inet end ================================================ FILE: lib/bolt_sips/types.ex ================================================ defmodule Bolt.Sips.Types do @moduledoc """ Basic support for representing nodes, relationships and paths belonging to a Neo4j graph database. Four supported types of entities: - Node - Relationship - UnboundRelationship - Path More details, about the Bolt protocol, here: https://github.com/boltprotocol/boltprotocol/blob/master/README.md Additionally, since bolt V2, new types appears: spatial and temporal Those are not documented in bolt protocol, but neo4j documentation can be found here: https://neo4j.com/docs/cypher-manual/current/syntax/temporal/ https://neo4j.com/docs/cypher-manual/current/syntax/spatial/ To work with temporal types, the following Elixir structs are available: - Time, DateTime, NaiveDateTime - Calendar.DateTime to work with timezone (as string) - TimeWithTZOffset, DateTimeWithTZOffset to work with (date)time and timezone offset(as integer) - Duration For spatial types, you only need Point struct as it covers: - 2D point (cartesian or geographic) - 3D point (cartesian or geographic) """ alias Bolt.Sips.TypesHelper defmodule Entity do @moduledoc """ base structure for Node and Relationship """ @base_fields [id: nil, properties: nil] defmacro __using__(fields) do fields = @base_fields ++ fields quote do defstruct unquote(fields) end end end defmodule Node do @moduledoc """ Self-contained graph node. A Node represents a node from a Neo4j graph and consists of a unique identifier (within the scope of its origin graph), a list of labels and a map of properties. https://github.com/boltprotocol/boltprotocol/blob/master/v1/_serialization.asciidoc#node """ use Entity, labels: nil @type t :: %__MODULE__{ id: integer, labels: [String.t()], properties: map } end defmodule Relationship do @moduledoc """ Self-contained graph relationship. A Relationship represents a relationship from a Neo4j graph and consists of a unique identifier (within the scope of its origin graph), identifiers for the start and end nodes of that relationship, a type and a map of properties. https://github.com/boltprotocol/boltprotocol/blob/master/v1/_serialization.asciidoc#relationship """ use Entity, start: nil, end: nil, type: nil @type t :: %__MODULE__{ id: integer, properties: map } end defmodule UnboundRelationship do @moduledoc """ Self-contained graph relationship without endpoints. An UnboundRelationship represents a relationship relative to a separately known start point and end point. https://github.com/boltprotocol/boltprotocol/blob/master/v1/_serialization.asciidoc#unboundrelationship """ use Entity, start: nil, end: nil, type: nil @type t :: %__MODULE__{ id: integer, properties: map } end defmodule Path do @moduledoc """ Self-contained graph path. A Path is a sequence of alternating nodes and relationships corresponding to a walk in the graph. The path always begins and ends with a node. Its representation consists of a list of distinct nodes, a list of distinct relationships and a sequence of integers describing the path traversal https://github.com/boltprotocol/boltprotocol/blob/master/v1/_serialization.asciidoc#path """ @type t :: %__MODULE__{ nodes: list() | nil, relationships: list() | nil, sequence: list() | nil } defstruct nodes: nil, relationships: nil, sequence: nil @doc """ represents a traversal or walk through a graph and maintains a direction separate from that of any relationships traversed """ @spec graph(Path.t()) :: list() | nil def graph(path) do entities = [List.first(path.nodes)] draw_path( path.nodes, path.relationships, path.sequence, 0, Enum.take_every(path.sequence, 2), entities, # last node List.first(path.nodes), # next node nil ) end # @lint false defp draw_path(_n, _r, _s, _i, [], acc, _ln, _nn), do: acc defp draw_path(n, r, s, i, [h | t] = _rel_index, acc, ln, _nn) do next_node = Enum.at(n, Enum.at(s, 2 * i + 1)) urel = if h > 0 && h < 255 do # rel: rels[rel_index - 1], start/end: (ln.id, next_node.id) rel = Enum.at(r, h - 1) unbound_relationship = [:id, :type, :properties, :start, :end] |> Enum.zip([rel.id, rel.type, rel.properties, ln.id, next_node.id]) struct(UnboundRelationship, unbound_relationship) else # rel: rels[-rel_index - 1], start/end: (next_node.id, ln.id) # Neo4j sends: -1, and Bolt.Sips.Internals. returns 255 instead? Investigating, # meanwhile ugly path: # oh dear ... haha = if h == 255, do: -1, else: h rel = Enum.at(r, -haha - 1) unbound_relationship = [:id, :type, :properties, :start, :end] |> Enum.zip([rel.id, rel.type, rel.properties, next_node.id, ln.id]) struct(UnboundRelationship, unbound_relationship) end draw_path(n, r, s, i + 1, t, (acc ++ [urel]) ++ [next_node], next_node, ln) end end defmodule TimeWithTZOffset do @moduledoc """ Manage a Time and its time zone offset. This temporal types hs been added in bolt v2 """ defstruct [:time, :timezone_offset] @type t :: %__MODULE__{ time: Calendar.time(), timezone_offset: integer() } @doc """ Create a valid TimeWithTZOffset from a Time and offset in seconds """ @spec create(Calendar.time(), integer()) :: TimeWithTZOffset.t() def create(%Time{} = time, offset) when is_integer(offset) do %TimeWithTZOffset{ time: time, timezone_offset: offset } end @doc """ Convert TimeWithTZOffset struct in a cypher-compliant string """ @spec format_param(TimeWithTZOffset.t()) :: {:ok, String.t()} | {:error, any()} def format_param(%TimeWithTZOffset{time: time, timezone_offset: offset}) when is_integer(offset) do param = Time.to_iso8601(time) <> TypesHelper.formated_time_offset(offset) {:ok, param} end def format_param(param) do {:error, param} end end defmodule DateTimeWithTZOffset do @moduledoc """ Manage a Time and its time zone offset. This temporal types hs been added in bolt v2 """ defstruct [:naive_datetime, :timezone_offset] @type t :: %__MODULE__{ naive_datetime: Calendar.naive_datetime(), timezone_offset: integer() } @doc """ Create a valid DateTimeWithTZOffset from a NaiveDateTime and offset in seconds """ @spec create(Calendar.naive_datetime(), integer()) :: DateTimeWithTZOffset.t() def create(%NaiveDateTime{} = naive_datetime, offset) when is_integer(offset) do %DateTimeWithTZOffset{ naive_datetime: naive_datetime, timezone_offset: offset } end @doc """ Convert DateTimeWithTZOffset struct in a cypher-compliant string """ @spec format_param(DateTimeWithTZOffset.t()) :: {:ok, String.t()} | {:error, any()} def format_param(%DateTimeWithTZOffset{naive_datetime: ndt, timezone_offset: offset}) when is_integer(offset) do formated = NaiveDateTime.to_iso8601(ndt) <> TypesHelper.formated_time_offset(offset) {:ok, formated} end def format_param(param) do {:error, param} end end defmodule Duration do @moduledoc """ a Duration type, as introduced in bolt V2. Composed of months, days, seconds and nanoseconds, it can be used in date operations """ defstruct years: 0, months: 0, weeks: 0, days: 0, hours: 0, minutes: 0, seconds: 0, nanoseconds: 0 @type t :: %__MODULE__{ years: non_neg_integer(), months: non_neg_integer(), weeks: non_neg_integer(), days: non_neg_integer(), hours: non_neg_integer(), minutes: non_neg_integer(), seconds: non_neg_integer(), nanoseconds: non_neg_integer() } @period_prefix "P" @time_prefix "T" @year_suffix "Y" @month_suffix "M" @week_suffix "W" @day_suffix "D" @hour_suffix "H" @minute_suffix "M" @second_suffix "S" @doc """ Create a Duration struct from data returned by Neo4j. Neo4j returns a list of 4 integers: - months - days - seconds - nanoseconds Struct elements are computed in a logical way, then for exmple 65 seconds is 1min and 5 seconds. Beware that you may not retrieve the same data you send! Note: days are not touched as they are not a fixed number of days for each month. ## Example: iex> Duration.create(15, 53, 125, 54) %Bolt.Sips.Types.Duration{ days: 53, hours: 0, minutes: 2, months: 3, nanoseconds: 54, seconds: 5, weeks: 0, years: 1 } """ @spec create(integer(), integer(), integer(), integer()) :: Duration.t() def create(months, days, seconds, nanoseconds) when is_integer(months) and is_integer(days) and is_integer(seconds) and is_integer(nanoseconds) do years = div(months, 12) months_ = rem(months, 12) {hours, minutes, seconds_inter} = TypesHelper.decompose_in_hms(seconds) {seconds_, nanoseconds_} = manage_nanoseconds(seconds_inter, nanoseconds) %Duration{ years: years, months: months_, days: days, hours: hours, minutes: minutes, seconds: seconds_, nanoseconds: nanoseconds_ } end @spec manage_nanoseconds(integer(), integer()) :: {integer(), integer()} defp manage_nanoseconds(seconds, nanoseconds) when nanoseconds >= 1_000_000_000 do seconds_ = seconds + div(nanoseconds, 1_000_000_000) nanoseconds_ = rem(nanoseconds, 1_000_000_000) {seconds_, nanoseconds_} end defp manage_nanoseconds(seconds, nanoseconds) do {seconds, nanoseconds} end @doc """ Convert a %Duration in a cypher-compliant string. To know everything about duration format, please see: https://neo4j.com/docs/cypher-manual/current/syntax/temporal/#cypher-temporal-durations """ @spec format_param(Duration.t()) :: {:ok, String.t()} | {:error, any()} def format_param( %Duration{ years: y, months: m, days: d, hours: h, minutes: mm, seconds: s, nanoseconds: ss } = duration ) when is_integer(y) and is_integer(m) and is_integer(d) and is_integer(h) and is_integer(mm) and is_integer(s) and is_integer(ss) do formated = format_date(duration) <> format_time(duration) param = case formated do "" -> "" formated_duration -> @period_prefix <> formated_duration end {:ok, param} end def format_param(param) do {:error, param} end @spec format_date(Duration.t()) :: String.t() defp format_date(%Duration{years: years, months: months, weeks: weeks, days: days}) do format_duration_part(years, @year_suffix) <> format_duration_part(months, @month_suffix) <> format_duration_part(weeks, @week_suffix) <> format_duration_part(days, @day_suffix) end @spec format_time(Duration.t()) :: String.t() defp format_time(%Duration{ hours: hours, minutes: minutes, seconds: s, nanoseconds: ns }) when hours > 0 or minutes > 0 or s > 0 or ns > 0 do {seconds, nanoseconds} = manage_nanoseconds(s, ns) nanoseconds_f = nanoseconds |> Integer.to_string() |> String.pad_leading(9, "0") seconds_f = "#{Integer.to_string(seconds)}.#{nanoseconds_f}" |> String.to_float() @time_prefix <> format_duration_part(hours, @hour_suffix) <> format_duration_part(minutes, @minute_suffix) <> format_duration_part(seconds_f, @second_suffix) end defp format_time(_) do "" end @spec format_duration_part(number(), String.t()) :: String.t() defp format_duration_part(duration_part, suffix) when duration_part > 0 and is_bitstring(suffix) do "#{stringify_number(duration_part)}#{suffix}" end defp format_duration_part(_, _) do "" end @spec stringify_number(number()) :: String.t() defp stringify_number(number) when is_integer(number) do Integer.to_string(number) end defp stringify_number(number) do Float.to_string(number) end end defmodule Point do @moduledoc """ Manage spatial data introduced in Bolt V2 Point can be: - Cartesian 2D - Geographic 2D - Cartesian 3D - Geographic 3D """ @srid_cartesian 7203 @srid_cartesian_3d 9157 @srid_wgs_84 4326 @srid_wgs_84_3d 4979 defstruct [:crs, :srid, :x, :y, :z, :longitude, :latitude, :height] @type t :: %__MODULE__{ crs: String.t(), srid: integer(), x: number() | nil, y: number() | nil, z: number() | nil, longitude: number() | nil, latitude: number() | nil, height: number() | nil } defguardp is_crs(crs) when crs in ["cartesian", "cartesian-3d", "wgs-84", "wgs-84-3d"] defguardp is_srid(srid) when srid in [@srid_cartesian, @srid_cartesian_3d, @srid_wgs_84, @srid_wgs_84_3d] defguardp are_coords(lt, lg, h, x, y, z) when (is_number(lt) or is_nil(lt)) and (is_number(lg) or is_nil(lg)) and (is_number(h) or is_nil(h)) and (is_number(x) or is_nil(x)) and (is_number(y) or is_nil(y)) and (is_number(z) or is_nil(z)) defguardp is_valid_coords(x, y) when is_number(x) and is_number(y) defguardp is_valid_coords(x, y, z) when is_number(x) and is_number(y) and is_number(z) @doc """ A 2D point either needs: - 2 coordinates and a atom (:cartesian or :wgs_84) to define its type - 2 coordinates and a srid (4326 or 7203) to define its type ## Examples: iex> Point.create(:cartesian, 10, 20.0) %Bolt.Sips.Types.Point{ crs: "cartesian", height: nil, latitude: nil, longitude: nil, srid: 7203, x: 10.0, y: 20.0, z: nil } iex> Point.create(4326, 10, 20.0) %Bolt.Sips.Types.Point{ crs: "wgs-84", height: nil, latitude: 20.0, longitude: 10.0, srid: 4326, x: 10.0, y: 20.0, z: 30.0 } """ @spec create(:cartesian | :wgs_84 | 4326 | 7203, number(), number()) :: Point.t() def create(:cartesian, x, y) do create(@srid_cartesian, x, y) end def create(:wgs_84, longitude, latitude) do create(@srid_wgs_84, longitude, latitude) end def create(@srid_cartesian, x, y) when is_valid_coords(x, y) do %Point{ crs: crs(@srid_cartesian), srid: @srid_cartesian, x: format_coord(x), y: format_coord(y) } end def create(@srid_wgs_84, longitude, latitude) when is_valid_coords(longitude, latitude) do %Point{ crs: crs(@srid_wgs_84), srid: @srid_wgs_84, x: format_coord(longitude), y: format_coord(latitude), longitude: format_coord(longitude), latitude: format_coord(latitude) } end @doc """ Create a 3D point A 3D point either needs: - 3 coordinates and a atom (:cartesian or :wgs_84) to define its type - 3 coordinates and a srid (4979 or 9147) to define its type ## Examples: iex> Point.create(:cartesian, 10, 20.0, 30) %Bolt.Sips.Types.Point{ crs: "cartesian-3d", height: nil, latitude: nil, longitude: nil, srid: 9157, x: 10.0, y: 20.0, z: 30.0 } iex> Point.create(4979, 10, 20.0, 30) %Bolt.Sips.Types.Point{ crs: "wgs-84-3d", height: 30.0, latitude: 20.0, longitude: 10.0, srid: 4979, x: 10.0, y: 20.0, z: 30.0 } """ @spec create(:cartesian | :wgs_84 | 4979 | 9157, number(), number(), number()) :: Point.t() def create(:cartesian, x, y, z) do create(@srid_cartesian_3d, x, y, z) end def create(:wgs_84, longitude, latitude, height) do create(@srid_wgs_84_3d, longitude, latitude, height) end def create(@srid_cartesian_3d, x, y, z) when is_valid_coords(x, y, z) do %Point{ crs: crs(@srid_cartesian_3d), srid: @srid_cartesian_3d, x: format_coord(x), y: format_coord(y), z: format_coord(z) } end def create(@srid_wgs_84_3d, longitude, latitude, height) when is_valid_coords(longitude, latitude, height) do %Point{ crs: crs(@srid_wgs_84_3d), srid: @srid_wgs_84_3d, x: format_coord(longitude), y: format_coord(latitude), z: format_coord(height), longitude: format_coord(longitude), latitude: format_coord(latitude), height: format_coord(height) } end @spec crs(4326 | 4979 | 7203 | 9157) :: String.t() defp crs(@srid_cartesian), do: "cartesian" defp crs(@srid_cartesian_3d), do: "cartesian-3d" defp crs(@srid_wgs_84), do: "wgs-84" defp crs(@srid_wgs_84_3d), do: "wgs-84-3d" defp format_coord(coord) when is_integer(coord), do: coord / 1 defp format_coord(coord), do: coord @doc """ Convert a Point struct into a cypher-compliant map ## Example iex(8)> Point.create(4326, 10, 20.0) |> Point.format_to_param %{crs: "wgs-84", latitude: 20.0, longitude: 10.0, x: 10.0, y: 20.0} """ @spec format_param(Point.t()) :: {:ok, map()} | {:error, any()} def format_param( %Point{crs: crs, srid: srid, latitude: lt, longitude: lg, height: h, x: x, y: y, z: z} = point ) when is_crs(crs) and is_srid(srid) and are_coords(lt, lg, h, x, y, z) do param = point |> Map.from_struct() |> Enum.filter(fn {_, val} -> not is_nil(val) end) |> Map.new() |> Map.drop([:srid]) {:ok, param} end def format_param(param) do {:error, param} end end end ================================================ FILE: lib/bolt_sips/types_helper.ex ================================================ defmodule Bolt.Sips.TypesHelper do @doc """ Decompose an amount seconds into the tuple {hours, minutes, seconds} """ @spec decompose_in_hms(integer()) :: {integer(), integer(), integer()} def decompose_in_hms(seconds) do [{minutes, seconds}, {hours, _}, _] = [3600, 60] |> Enum.reduce([{0, seconds}], fn divisor, acc -> {_, num} = hd(acc) [{div(num, divisor), rem(num, divisor)} | acc] end) {hours, minutes, seconds} end @doc """ Convert NaiveDateTime and timezone into a Calendar.DateTime Without losing micorsecond data! """ @spec datetime_with_micro(Calendar.naive_datetime(), String.t()) :: Calendar.datetime() def datetime_with_micro(%NaiveDateTime{} = naive_dt, timezone) do DateTime.from_naive!(naive_dt, timezone) end @doc """ Convert an amount of seconds in a +hours:minutes offset """ @spec formated_time_offset(integer()) :: String.t() def formated_time_offset(offset_seconds) do {hours, minutes, _} = offset_seconds |> abs() |> decompose_in_hms() get_sign_string(offset_seconds) <> format_time_part(hours) <> ":" <> format_time_part(minutes) end defp get_sign_string(number) when number >= 0 do "+" end defp get_sign_string(_) do "-" end defp format_time_part(time_part) when time_part < 10 do "0" <> Integer.to_string(time_part) end defp format_time_part(time_part) do Integer.to_string(time_part) end end ================================================ FILE: lib/bolt_sips/utils.ex ================================================ defmodule Bolt.Sips.Utils do @moduledoc false # Common utilities @default_hostname "localhost" @default_bolt_port 7687 @default_driver_options [ hostname: @default_hostname, port: @default_bolt_port, pool_size: 15, max_overflow: 0, timeout: 15_000, ssl: false, socket: Bolt.Sips.Socket, with_etls: false, schema: "bolt", prefix: :default ] @doc """ Generate a random string. """ def random_id, do: :rand.uniform() |> Float.to_string() |> String.slice(2..10) @doc """ Fills in the given `opts` with default options. """ @spec default_config(Keyword.t()) :: Keyword.t() def default_config(), do: Application.get_env(:bolt_sips, Bolt, []) |> default_config def default_config(opts) do config = @default_driver_options |> Keyword.merge(opts) ssl_or_sock = if(Keyword.get(config, :ssl), do: :ssl, else: Keyword.get(config, :socket)) config |> Keyword.put_new(:hostname, System.get_env("NEO4J_HOST") || @default_hostname) |> Keyword.put_new(:port, System.get_env("NEO4J_PORT") || @default_bolt_port) |> Keyword.put_new(:pool_size, 5) |> Keyword.put_new(:max_overflow, 2) |> Keyword.put_new(:timeout, 15_000) |> Keyword.put_new(:ssl, false) |> Keyword.put_new(:socket, Bolt.Sips.Socket) |> Keyword.put_new(:with_etls, false) |> Keyword.put_new(:schema, "bolt") |> Keyword.put_new(:path, "") |> Keyword.put_new(:prefix, :default) |> or_use_url_if_present |> Enum.reject(fn {_k, v} -> is_nil(v) end) |> Keyword.put(:socket, ssl_or_sock) end @doc """ source: https://github.com/eksperimental/experimental_elixir/blob/master/lib/kernel_modulo.ex Modulo operation. Returns the remainder after division of `number` by `modulus`. The sign of the result will always be the same sign as the `modulus`. More information: [Modulo operation](https://en.wikipedia.org/wiki/Modulo_operation) on Wikipedia. ## Examples iex> mod(17, 17) 0 iex> mod(17, 1) 0 iex> mod(17, 13) 4 iex> mod(-17, 13) 9 iex> mod(17, -13) -9 iex> mod(-17, -13) -4 iex> mod(17, 26) 17 iex> mod(-17, 26) 9 iex> mod(17, -26) -9 iex> mod(-17, -26) -17 iex> mod(17, 0) ** (ArithmeticError) bad argument in arithmetic expression iex> mod(1.5, 2) ** (FunctionClauseError) no function clause matching in Experimental.KernelModulo.mod/2 """ @spec mod(integer, integer) :: non_neg_integer def mod(number, modulus) when is_integer(number) and is_integer(modulus) do remainder = rem(number, modulus) if (remainder > 0 and modulus < 0) or (remainder < 0 and modulus > 0) do remainder + modulus else remainder end end @doc false defp or_use_url_if_present(config) do if Keyword.has_key?(config, :url) do f = config[:url] |> to_string() |> URI.parse() schema = if f.scheme, do: f.scheme, else: "bolt" config |> Keyword.put(:hostname, f.host) |> Keyword.put(:schema, schema) |> Keyword.put(:query, f.query) |> Keyword.put(:path, f.path) |> Keyword.put(:fragment, f.fragment) |> Keyword.put(:routing_context, routing_context(f.query)) |> Keyword.put(:port, port_from_url(f.port)) |> username_and_password(f.userinfo) |> Enum.reject(fn {_k, v} -> is_nil(v) end) else config end end @username_password ~r""" ^ (?: (? \* | [a-zA-Z0-9%_.!~*'();&=+$,-]+) (?: : (? \* | [a-zA-Z0-9%_.!~*'();&=+$,-]*))? )? $ """x @spec username_and_password(Keyword.t(), String.t()) :: Keyword.t() defp username_and_password(config, uri_user_info) when is_binary(uri_user_info) do case Regex.named_captures(@username_password, uri_user_info) do %{"username" => username, "password" => password} -> config |> Keyword.put(:basic_auth, username: username, password: password) _ -> config end end defp username_and_password(config, _), do: config @spec port_from_url(integer) :: integer defp port_from_url(port) when is_nil(port) when not is_integer(port), do: @default_bolt_port defp port_from_url(port) when is_integer(port), do: port defp port_from_url(_port), do: @default_bolt_port @accepted_units_of_time [:seconds, :millisecond, :microsecond, :nanosecond] @accepted_units_of_time_str Enum.join(@accepted_units_of_time, ", ") @doc """ return now for UTC, in :seconds, :millisecond, :microsecond and :nanosecond """ @spec now(unit :: :seconds | :millisecond | :microsecond | :nanosecond) :: integer def now(unit \\ :seconds) def now(unit) when unit in @accepted_units_of_time, do: :os.system_time(unit) def now(unit), do: raise( ArgumentError, "expected one of these: #{@accepted_units_of_time_str}, but received: #{inspect(unit)}, instead." ) defp routing_context(nil), do: decode("") defp routing_context(query), do: decode(query) def decode(query) do do_decode(:binary.split(query, [";", ",", "&"], [:global]), %{}) end defp do_decode([], acc), do: acc defp do_decode([h | t], acc) do case decode_kv(h) do {k, v} -> do_decode(t, Map.put(acc, k, v)) false -> do_decode(t, acc) end end # borrowed some code from Plug defp decode_kv(""), do: false defp decode_kv(<>), do: false defp decode_kv(<>) when h in [?\s, ?\t], do: decode_kv(t) defp decode_kv(kv), do: decode_key(kv, "") defp decode_key("", _key), do: false defp decode_key(<>, ""), do: false defp decode_key(<>, key), do: decode_value(t, "", key, "") defp decode_key(<>, _key) when h in [?\s, ?\t, ?\r, ?\n, ?\v, ?\f], do: false defp decode_key(<>, key), do: decode_key(t, <>) defp decode_value("", _spaces, key, value), do: {key, value} defp decode_value(<>, spaces, key, value), do: decode_value(t, <>, key, value) defp decode_value(<>, _spaces, _key, _value) when h in [?\t, ?\r, ?\n, ?\v, ?\f], do: false defp decode_value(<>, spaces, key, value), do: decode_value(t, "", key, <>) end ================================================ FILE: lib/bolt_sips.ex ================================================ defmodule Bolt.Sips do @moduledoc """ A Neo4j driver for Elixir providing many useful features: - using the Bolt protocol, the Elixir implementation - the Neo4j's newest network protocol, designed for high-performance; latest Bolt versions, are supported. - Can connect to a standalone Neo4j server (`:direct` mode) or to a Neo4j causal cluster, using the `bolt+routing` or the newer `neo4j` schemes; connecting in `:routing` mode. - Provides the user with the ability to create and manage distinct ad-hoc `role-based` connections to one or more Neo4j servers/databases - Supports transactions, simple and complex Cypher queries with or w/o parameters - Multi-tenancy - Supports Neo4j versions: 3.0.x/3.1.x/3.2.x/3.4.x/3.5.x To start, add the `:bolt_sips` dependency to you project, run `mix do deps.get, compile` on it and then you can quickly start experimenting with Neo4j from the convenience of your IEx shell. Example: iex> {:ok, _neo} = Bolt.Sips.start_link(url: "bolt://neo4j:test@localhost") {:ok, #PID<0.250.0>} iex> conn = Bolt.Sips.conn() #PID<0.256.0> iex> Bolt.Sips.query!(conn, "RETURN 1 as n") %Bolt.Sips.Response{ records: [[1]], results: [%{"n" => 1}] } the example above presumes that you have a Neo4j server available locally, using the Bolt protocol and requiring authentication. """ use Supervisor @registry_name :bolt_sips_registry # @timeout 15_000 # @max_rows 500 alias Bolt.Sips.{Query, ConnectionSupervisor, Router, Error, Response, Exception} @type conn :: DBConnection.conn() @type transaction :: DBConnection.t() @doc """ Start or add a new Neo4j connection ## Options: - `:url`- a full url to pointing to a running Neo4j server. Please remember you must specify the scheme used to connect to the server. Valid schemes:`bolt`,`bolt+routing`and`neo4j` - the last two being used for connecting to a Neo4j causal cluster. - `:pool_size` - the size of the connection pool. Default: 15 - `:timeout` - a connection timeout value defined in milliseconds. Default: 15_000 - `:ssl`-`true`, if the connection must be encrypted. Default:`false` - `:prefix`- used for differentiating between multiple connections available in the same app. Default:`:default` ## Example of valid configurations (i.e. defined in config/dev.exs) and usage: This is the most basic configuration: config :bolt_sips, Bolt, url: "bolt://localhost:7687" and if you need to connect to remote servers: config :bolt_sips, Bolt, url: "bolt://Bilbo:Baggins@hobby-hobbits.dbs.graphenedb.com:24786", ssl: true, timeout: 15_000, Example with a configuration defined in the `config/dev.exs`: opts = Application.get_env(:bolt_sips, Bolt) {:ok, pid} = Bolt.Sips.start_link(opts) Bolt.Sips.query!(pid, "CREATE (a:Person {name:'Bob'})") Bolt.Sips.query!(pid, "MATCH (a:Person) RETURN a.name AS name") |> Enum.map(&(&1["name"])) Or defining an ad-hoc configuration: Example with a configuration defined in the `config/dev.exs`: {:ok, _neo} = Bolt.Sips.start_link(url: "bolt://neo4j:test@localhost") conn = Bolt.Sips.conn() Bolt.Sips.query!(conn, "return 1 as n") """ @spec start_link(Keyword.t()) :: Supervisor.on_start() def start_link(opts) do with nil <- Process.whereis(__MODULE__) do Supervisor.start_link(__MODULE__, opts, name: __MODULE__) else pid -> Router.configure(opts) {:ok, pid} {pid, node} -> Router.configure(opts) {:ok, {pid, node}} end end @doc false def init(opts) do [ Registry.child_spec(keys: :unique, name: registry_name()), ConnectionSupervisor, {Router, opts} ] |> Supervisor.init(strategy: :one_for_one) end @doc """ Returns a pool name which can be used to acquire a connection from a pool of servers responsible with a specific type of operations: read, write and route, or all of the above: "direct" """ @spec conn(atom, keyword) :: conn def conn(role \\ :direct, opts \\ [prefix: :default]) def conn(role, opts) do prefix = Keyword.get(opts, :prefix, :default) with {:ok, conn} <- Router.get_connection(role, prefix) do conn else {:error, e} -> raise Exception, e e -> {:error, e} end end ## Query ######################## @doc """ sends the query (and its parameters) to the server and returns `{:ok, Response.t()}` or `{:error, Error}` otherwise """ @spec query(conn, String.t()) :: {:ok, Response.t() | [Response.t()]} | {:error, Error.t()} defdelegate query(conn, statement), to: Query @doc """ The same as query/2 but raises a Exception if it fails. Returns the server response otherwise. """ @spec query!(conn, String.t()) :: Response.t() | [Response.t()] | Exception.t() defdelegate query!(conn, statement), to: Query @doc """ send a query and an associated map of parameters. Returns the server response or an error """ @spec query(conn, String.t(), map()) :: {:ok, Response.t() | [Response.t()]} | {:error, Error.t()} defdelegate query(conn, statement, params), to: Query @doc """ The same as query/3 but raises a Exception if it fails. """ @spec query!(conn, String.t(), map()) :: Response.t() | [Response.t()] | Exception.t() defdelegate query!(conn, statement, params), to: Query @doc """ send a query and an associated map of parameters with options. Returns the server response or an error The `opts` keyword list will be passed to `DbConnection.execute/4`. By default, the query will timeout after `15_000` milliseconds, this may be overriden by setting the `timeout` option in `opts`. """ @spec query(conn, String.t(), map(), Keyword.t()) :: {:ok, Response.t() | [Response.t()]} | {:error, Error.t()} defdelegate query(conn, statement, params, opts), to: Query @doc """ The same as query/4 but raises a Exception if it fails. """ @spec query!(conn, String.t(), map(), Keyword.t()) :: Response.t() | [Response.t()] | Exception.t() defdelegate query!(conn, statement, params, opts), to: Query ## Transaction ######################## @doc """ Example: ```elixir setup do {:ok, [main_conn: Bolt.Sips.conn()]} end test "execute statements in transaction", %{main_conn: main_conn} do Bolt.Sips.transaction(main_conn, fn conn -> book = Bolt.Sips.query!(conn, "CREATE (b:Book {title: \"The Game Of Trolls\"}) return b") |> Response.first() assert %{"b" => g_o_t} = book assert g_o_t.properties["title"] == "The Game Of Trolls" Bolt.Sips.rollback(conn, :changed_my_mind) end) books = Bolt.Sips.query!(main_conn, "MATCH (b:Book {title: \"The Game Of Trolls\"}) return b") assert Enum.count(books) == 0 end ``` """ defdelegate transaction(conn, fun, opts \\ []), to: DBConnection @doc """ Rollback a database transaction and release lock on connection. When inside of a `transaction/3` call does a non-local return, using a `throw/1` to cause the transaction to enter a failed state and the `transaction/3` call returns `{:error, reason}`. If `transaction/3` calls are nested the connection is marked as failed until the outermost transaction call does the database rollback. ### Example {:error, :oops} = Bolt.Sips.transaction(pool, fn(conn) -> Bolt.Sips.rollback(conn, :oops) end) """ @spec rollback(DBConnection.t(), reason :: any) :: no_return defdelegate rollback(conn, opts), to: DBConnection @doc """ terminate a pool of connections with the role specified """ defdelegate terminate_connections(role), to: Router @doc """ peek into the main Router state, and return the internal state controlling the connections to the server/server. Mostly for internal use or for helping driver developers. The authentication credentials will be sanitized, if any ### Examples: iex> Bolt.Sips.info() %{default: %{connections: %{direct: %{"localhost:7687" => 0}, routing_query: nil, zorba: %{"localhost:7687" => 0}}, user_options: [basic_auth: [username: "******", password: "******"], socket: Bolt.Sips.Socket, port: 7687, routing_context: %{}, schema: "bolt", hostname: "localhost", timeout: 15000, ssl: false, with_etls: false, prefix: :default, url: "bolt://localhost", pool_size: 10, max_overflow: 2, role: :zorba]}} """ @spec info() :: map def info(), do: sanitized_info(Bolt.Sips.Router.info()) @doc """ extract the routing table from the router """ @spec routing_table(any) :: map def routing_table(prefix \\ :default) def routing_table(prefix) do Bolt.Sips.Router.routing_table(prefix) end @doc """ the registry name used across the various driver components """ @spec registry_name() :: :bolt_sips_registry def registry_name(), do: @registry_name @hide_auth [username: "******", password: "******"] defp sanitized_info(info) when is_map(info) do for {k, v} <- info, into: %{} do {k, Map.update(v, :user_options, @hide_auth, &Keyword.put(&1, :basic_auth, @hide_auth))} end end defp sanitized_info(info), do: info end ================================================ FILE: lib/mix/tasks/cypher.ex ================================================ defmodule Mix.Tasks.Bolt.Cypher do use Mix.Task @shortdoc "Execute a Cypher command" @recursive true @moduledoc """ Quickly run Cypher commands from a mix task ## Command line options - `--url`, `-u` - Neo4j server URL - `--ssl`, `-s` - use ssl The command line options have lower precedence than the options specified in your `mix.exs` file, if defined. Examples: MIX_ENV=test mix bolt.cypher "MATCH (people:Person) RETURN people.name LIMIT 5" Output sample: "MATCH (people:Person) RETURN people.name as name LIMIT 5" [%{"name" => "Keanu Reeves"}, %{"name" => "Carrie-Anne Moss"}, %{"name" => "Andy Wachowski"}, %{"name" => "Lana Wachowski"}, %{"name" => "Joel Silver"}] """ alias Bolt.Sips, as: Neo4j @doc false def run(args) do Application.ensure_all_started(:bolt_sips) {cli_opts, args, _} = OptionParser.parse(args, aliases: [u: :url, s: :ssl], switches: []) options = run_options(cli_opts, Application.get_env(:bolt_sips, Bolt)) if args == [], do: Mix.raise("Try entering a Cypher command") cypher = args |> List.first() {:ok, _pid} = Neo4j.start_link(options) # display the cypher command log_cypher(cypher) with {:ok, response} <- Neo4j.query(Bolt.Sips.conn(), cypher) do response |> log_response else {:error, [code: code, message: message]} -> "#{inspect(code)} - cannot execute the command, see error above. Details: #{message}" |> log_error() e -> log_error("Unknown error: #{inspect(e)}") end end defp run_options(_, nil) do Mix.raise( "can't find a valid configuration file, use: MIX_ENV=test mix bolt.cypher \"MATCH...\", for example" ) end defp run_options(args, config) do Keyword.merge(config, args) end defp log_cypher(msg), do: Mix.shell().info([:green, "#{inspect(msg)}"]) defp log_response(msg), do: Mix.shell().info([:yellow, "#{inspect(msg)}"]) defp log_error(msg), do: Mix.shell().info([:white, "#{msg}"]) end ================================================ FILE: mix.exs ================================================ defmodule BoltSips.Mixfile do use Mix.Project @version "2.1.0" @url_docs "https://hexdocs.pm/bolt_sips" @url_github "https://github.com/florinpatrascu/bolt_sips" def project do [ app: :bolt_sips, version: @version, elixir: "~> 1.7", elixirc_paths: elixirc_paths(Mix.env()), deps: deps(), package: package(), description: "Neo4j driver for Elixir, using the fast Bolt protocol", name: "Bolt.Sips", build_embedded: Mix.env() == :prod, start_permanent: Mix.env() == :prod, docs: docs(), dialyzer: [plt_add_apps: [:jason, :poison, :mix], ignore_warnings: ".dialyzer_ignore.exs"], test_coverage: [ tool: ExCoveralls ], preferred_cli_env: [ bench: :bench, credo: :dev, bolt_sips: :test, coveralls: :test, "coveralls.html": :test, "coveralls.travis": :test ], aliases: aliases() ] end def application do [ extra_applications: [ :logger ] ] end defp aliases do [ test: [ "test --exclude bolt_v1 --exclude routing --exclude boltkit --exclude enterprise" ] ] end defp elixirc_paths(:test), do: ["lib", "test/support"] defp elixirc_paths(_), do: ["lib"] defp package do %{ files: [ "lib", "mix.exs", "LICENSE" ], licenses: ["Apache 2.0"], maintainers: [ "Florin T.PATRASCU", "Dmitriy Nesteryuk", "Dominique VASSARD", "Kristof Semjen" ], links: %{ "Docs" => @url_docs, "Github" => @url_github } } end defp docs do [ name: "Bolt.Sips", logo: "assets/bolt_sips_white_transparent.png", assets: "assets", source_ref: "v#{@version}", source_url: @url_github, main: "Bolt.Sips", extra_section: "guides", extras: [ "README.md", "CHANGELOG.md", "docs/getting-started.md", "docs/features/configuration.md", "docs/features/using-cypher.md", "docs/features/using-temporal-and-spatial-types.md", "docs/features/about-transactions.md", "docs/features/about-encoding.md", "docs/features/routing.md", "docs/features/multi-tenancy.md", "docs/features/using-with-phoenix.md" ] ] end # Type "mix help deps" for more examples and options defp deps do [ {:db_connection, "~> 2.4.2"}, {:jason, "~> 1.4", optional: true}, {:poison, "~> 5.0", optional: true}, # Testing dependencies {:excoveralls, "~> 0.15.0", optional: true, only: [:test, :dev]}, {:mix_test_watch, "~> 1.1.0", only: [:dev, :test]}, {:porcelain, "~> 2.0.3", only: [:test, :dev], runtime: false}, {:uuid, "~> 1.1.8", only: [:test, :dev], runtime: false}, {:tzdata, "~> 1.1", only: [:test, :dev]}, # Benchmarking dependencies {:benchee, "~> 1.1.0", optional: true, only: [:dev, :test]}, {:benchee_html, "~> 1.0.0", optional: true, only: [:dev]}, # Linting dependencies {:credo, "~> 1.6.7", only: [:dev]}, {:dialyxir, "~> 1.2.0", only: [:dev], runtime: false}, # mix eye_drops {:eye_drops, github: "florinpatrascu/eye_drops", only: [:dev, :test], runtime: false}, # Documentation dependencies # Run me like this: `mix docs` {:ex_doc, "~> 0.29", only: :dev, runtime: false} ] end end ================================================ FILE: requirements.txt ================================================ boto==2.48.0 certifi click<8,>=7 docker urllib3 ================================================ FILE: test/bolt_sips/internals/bolt_protocol_all_bolt_version_test.exs ================================================ defmodule Bolt.Sips.Internals.BoltProtocolAllBoltVersionTest do use Bolt.Sips.InternalCase alias Bolt.Sips.Internals.BoltProtocol test "works for small queries", %{port: port, bolt_version: bolt_version} do string = Enum.to_list(0..100) |> Enum.join() query = """ RETURN $string as string """ params = %{string: string} [{:success, _} | records] = BoltProtocol.run_statement(:gen_tcp, port, bolt_version, query, params) assert [record: [^string], success: _] = records end test "works for big queries", %{port: port, bolt_version: bolt_version} do string = Enum.to_list(0..25_000) |> Enum.join() query = """ RETURN $string as string """ params = %{string: string} [{:success, _} | records] = BoltProtocol.run_statement(:gen_tcp, port, bolt_version, query, params) assert [record: [^string], success: _] = records end test "returns errors for wrong cypher queris", %{port: port, bolt_version: bolt_version} do assert %Bolt.Sips.Internals.Error{type: :cypher_error} = BoltProtocol.run_statement(:gen_tcp, port, bolt_version, "What?") end test "allows to recover from error with reset", %{port: port, bolt_version: bolt_version} do assert %Bolt.Sips.Internals.Error{type: :cypher_error} = BoltProtocol.run_statement(:gen_tcp, port, bolt_version, "What?") assert :ok = BoltProtocol.reset(:gen_tcp, port, bolt_version) assert [{:success, _} | _] = BoltProtocol.run_statement(:gen_tcp, port, bolt_version, "RETURN 1 as num") end test "returns proper error when using a bad session", %{port: port, bolt_version: bolt_version} do assert %Bolt.Sips.Internals.Error{type: :cypher_error} = BoltProtocol.run_statement(:gen_tcp, port, bolt_version, "What?") error = BoltProtocol.run_statement(:gen_tcp, port, bolt_version, "RETURN 1 as num") assert %Bolt.Sips.Internals.Error{} = error assert error.message =~ ~r/The session is in a failed state/ end test "returns proper error when misusing reset", %{ port: port, bolt_version: bolt_version } do :gen_tcp.close(port) assert %Bolt.Sips.Internals.Error{} = BoltProtocol.reset(:gen_tcp, port, bolt_version) end test "returns proper error when using a closed port", %{port: port, bolt_version: bolt_version} do :gen_tcp.close(port) assert %Bolt.Sips.Internals.Error{type: :connection_error} = BoltProtocol.run_statement(:gen_tcp, port, bolt_version, "RETURN 1 as num") end test "an invalid parameter value yields an error", %{port: port, bolt_version: bolt_version} do cypher = "MATCH (n:Person {invalid: {a_tuple}}) RETURN TRUE" assert_raise Bolt.Sips.Internals.PackStreamError, ~r/^unable to encode/i, fn -> BoltProtocol.run_statement(:gen_tcp, port, bolt_version, cypher, %{ a_tuple: {:error, "don't work"} }) end end test "encode value -128", %{port: port, bolt_version: bolt_version} do query = "CREATE (t:Test) SET t.value = $value RETURN t" [{:success, _} | records] = BoltProtocol.run_statement(:gen_tcp, port, bolt_version, query, %{value: -128}) assert [ record: [ %Bolt.Sips.Types.Node{ labels: ["Test"], properties: %{"value" => -128} } ], success: _ ] = records end end ================================================ FILE: test/bolt_sips/internals/bolt_protocol_bolt_v1_test.exs ================================================ defmodule Bolt.Sips.Internals.BoltProtocolV1Test do use Bolt.Sips.InternalCase @moduletag :bolt_v1 alias Bolt.Sips.Internals.BoltProtocol test "allows to recover from error with ack_failure", %{port: port, bolt_version: bolt_version} do assert %Bolt.Sips.Internals.Error{type: :cypher_error} = BoltProtocol.run_statement(:gen_tcp, port, bolt_version, "What?") assert :ok = BoltProtocol.ack_failure(:gen_tcp, port, bolt_version) assert [{:success, _} | _] = BoltProtocol.run_statement(:gen_tcp, port, bolt_version, "RETURN 1 as num") end test "returns proper error when misusing ack_failure and reset", %{ port: port, bolt_version: bolt_version } do assert %Bolt.Sips.Internals.Error{} = BoltProtocol.ack_failure(:gen_tcp, port, bolt_version) end test "works within a transaction", %{port: port, bolt_version: bolt_version} do assert [{:success, _}, {:success, _}] = BoltProtocol.run_statement(:gen_tcp, port, bolt_version, "BEGIN") assert [{:success, _} | _] = BoltProtocol.run_statement(:gen_tcp, port, bolt_version, "RETURN 1 as num") assert [{:success, _}, {:success, _}] = BoltProtocol.run_statement(:gen_tcp, port, bolt_version, "COMMIT") end test "works with rolled-back transactions", %{port: port, bolt_version: bolt_version} do assert [{:success, _}, {:success, _}] = BoltProtocol.run_statement(:gen_tcp, port, bolt_version, "BEGIN") assert [{:success, _} | _] = BoltProtocol.run_statement(:gen_tcp, port, bolt_version, "RETURN 1 as num") assert [{:success, _}, {:success, _}] = BoltProtocol.run_statement(:gen_tcp, port, bolt_version, "ROLLBACK") end end ================================================ FILE: test/bolt_sips/internals/bolt_protocol_bolt_v2_test.exs ================================================ defmodule Bolt.Sips.Internals.BoltProtoolBoltV2Test do use Bolt.Sips.InternalCase @moduletag :bolt_v2 alias Bolt.Sips.Internals.BoltProtocol describe "Temporal types" do test "Local date", %{port: port, bolt_version: bolt_version} do assert [ success: %{"fields" => ["d"]}, record: [~D[2017-01-01]], success: %{"type" => "r"} ] = BoltProtocol.run_statement( :gen_tcp, port, bolt_version, "RETURN date('2017-01-01') as d" ) end test "Time with Timezone Offset", %{port: port, bolt_version: bolt_version} do assert [ success: %{"fields" => ["t"]}, record: [ %Bolt.Sips.Types.TimeWithTZOffset{ time: ~T[12:45:30.250000], timezone_offset: 3600 } ], success: %{"type" => "r"} ] = BoltProtocol.run_statement( :gen_tcp, port, bolt_version, "RETURN time('12:45:30.25+01:00') AS t" ) end test "Local time", %{port: port, bolt_version: bolt_version} do assert [ success: %{"fields" => ["t"]}, record: [~T[12:45:30.250000]], success: %{"type" => "r"} ] = BoltProtocol.run_statement( :gen_tcp, port, bolt_version, "RETURN localtime('12:45:30.25') AS t" ) end test "Duration", %{port: port, bolt_version: bolt_version} do assert [ success: %{"fields" => ["d"]}, record: [ %Bolt.Sips.Types.Duration{ days: 34, hours: 0, minutes: 0, months: 3, nanoseconds: 5550, seconds: 54, weeks: 0, years: 1 } ], success: %{"type" => "r"} ] = BoltProtocol.run_statement( :gen_tcp, port, bolt_version, "RETURN duration('P1Y3M34DT54.00000555S') AS d" ) end test "Local datetime", %{port: port, bolt_version: bolt_version} do assert [ success: %{"fields" => ["d"]}, record: [~N[2018-04-05 12:34:00.654321]], success: %{"type" => "r"} ] = BoltProtocol.run_statement( :gen_tcp, port, bolt_version, "RETURN localdatetime('2018-04-05T12:34:00.654321') AS d" ) end test "datetime with timezone offset", %{port: port, bolt_version: bolt_version} do assert [ success: %{"fields" => ["d"]}, record: [ %Bolt.Sips.Types.DateTimeWithTZOffset{ naive_datetime: ~N[2018-04-05 12:34:23.654321], timezone_offset: 3600 } ], success: %{"type" => "r"} ] = BoltProtocol.run_statement( :gen_tcp, port, bolt_version, "RETURN datetime('2018-04-05T12:34:23.654321+01:00') AS d" ) end test "datetime with timezone id", %{port: port, bolt_version: bolt_version} do dt = Bolt.Sips.TypesHelper.datetime_with_micro(~N[2018-04-05T12:34:23.654321], "Europe/Berlin") assert [ success: %{"fields" => ["d"]}, record: [^dt], success: %{"type" => "r"} ] = BoltProtocol.run_statement( :gen_tcp, port, bolt_version, "RETURN datetime('2018-04-05T12:34:23.654321[Europe/Berlin]') AS d" ) end end describe "Spatial types" do test "Point 2D cartesian", %{port: port, bolt_version: bolt_version} do assert [ success: %{"fields" => ["p"]}, record: [ %Bolt.Sips.Types.Point{ crs: "cartesian", height: nil, latitude: nil, longitude: nil, srid: 7203, x: 40.0, y: 45.0, z: nil } ], success: %{"type" => "r"} ] = BoltProtocol.run_statement( :gen_tcp, port, bolt_version, "RETURN point({x: 40, y: 45}) AS p" ) end test "Point2D geographic", %{port: port, bolt_version: bolt_version} do assert [ success: %{"fields" => ["p"]}, record: [ %Bolt.Sips.Types.Point{ crs: "wgs-84", height: nil, latitude: 45.0, longitude: 40.0, srid: 4326, x: 40.0, y: 45.0, z: nil } ], success: %{"type" => "r"} ] = BoltProtocol.run_statement( :gen_tcp, port, bolt_version, "RETURN point({longitude: 40, latitude: 45}) AS p" ) end test "Point 3D cartesian", %{port: port, bolt_version: bolt_version} do assert [ success: %{"fields" => ["p"]}, record: [ %Bolt.Sips.Types.Point{ crs: "cartesian-3d", height: nil, latitude: nil, longitude: nil, srid: 9157, x: 40.0, y: 45.0, z: 150.0 } ], success: %{"type" => "r"} ] = BoltProtocol.run_statement( :gen_tcp, port, bolt_version, "RETURN point({x: 40, y: 45, z: 150}) AS p" ) end test "Point 3D geographic", %{port: port, bolt_version: bolt_version} do assert [ success: %{"fields" => ["p"]}, record: [ %Bolt.Sips.Types.Point{ crs: "wgs-84-3d", height: 150.0, latitude: 45.0, longitude: 40.0, srid: 4979, x: 40.0, y: 45.0, z: 150.0 } ], success: %{"type" => "r"} ] = BoltProtocol.run_statement( :gen_tcp, port, bolt_version, "RETURN point({longitude: 40, latitude: 45, height: 150}) AS p" ) end end end ================================================ FILE: test/bolt_sips/internals/bolt_protocol_bolt_v3_test.exs ================================================ defmodule Bolt.Sips.Internals.BoltProtocolBoltV3Test do use ExUnit.Case, async: true @moduletag :bolt_v3 alias Bolt.Sips.Internals.BoltProtocol alias Bolt.Sips.Metadata alias Bolt.Sips.Utils setup do app_config = Application.get_env(:bolt_sips, Bolt) port = Keyword.get(app_config, :port, 7687) auth = {app_config[:basic_auth][:username], app_config[:basic_auth][:password]} config = app_config |> Keyword.put(:port, port) |> Keyword.put(:auth, auth) |> Utils.default_config() {:ok, port} = config[:hostname] |> String.to_charlist() |> :gen_tcp.connect(config[:port], active: false, mode: :binary, packet: :raw ) {:ok, bolt_version} = BoltProtocol.handshake(:gen_tcp, port, []) {:ok, _} = BoltProtocol.hello(:gen_tcp, port, bolt_version, auth) on_exit(fn -> :gen_tcp.close(port) end) {:ok, config: config, port: port, bolt_version: bolt_version} end describe "run/7" do test "(no params, no metadata, no options)", %{port: port, bolt_version: bolt_version} do assert {:ok, {:success, _}} = BoltProtocol.run(:gen_tcp, port, bolt_version, "RETURN 1 AS num") end test "(params, no metadata, no options)", %{port: port, bolt_version: bolt_version} do assert {:ok, {:success, _}} = BoltProtocol.run(:gen_tcp, port, bolt_version, "RETURN $num AS num", %{num: 14}) end test "(no params, metadata, no options)", %{port: port, bolt_version: bolt_version} do {:ok, metadata} = Metadata.new(%{tx_timeout: 1_000}) assert {:ok, {:success, _}} = BoltProtocol.run(:gen_tcp, port, bolt_version, "RETURN 1 AS num", %{}, metadata) end test "(params, metadata, no options)", %{port: port, bolt_version: bolt_version} do {:ok, metadata} = Metadata.new(%{tx_timeout: 1_000}) assert {:ok, {:success, _}} = BoltProtocol.run( :gen_tcp, port, bolt_version, "RETURN $num AS num", %{num: 14}, metadata ) end test "(no params, no metadata, options)", %{port: port, bolt_version: bolt_version} do assert {:ok, {:success, _}} = BoltProtocol.run(:gen_tcp, port, bolt_version, "RETURN 1 AS num", %{}, %{}, recv_timeout: 5000 ) end test "(params, no metadata, options)", %{port: port, bolt_version: bolt_version} do assert {:ok, {:success, _}} = BoltProtocol.run( :gen_tcp, port, bolt_version, "RETURN $num AS num", %{num: 14}, %{}, recv_timeout: 5000 ) end test "(no params, metadata, options)", %{port: port, bolt_version: bolt_version} do {:ok, metadata} = Metadata.new(%{tx_timeout: 1_000}) assert {:ok, {:success, _}} = BoltProtocol.run(:gen_tcp, port, bolt_version, "RETURN 1 AS num", %{}, metadata, recv_timeout: 5000 ) end test "(params, metadata, options)", %{port: port, bolt_version: bolt_version} do {:ok, metadata} = Metadata.new(%{tx_timeout: 1_000}) assert {:ok, {:success, _}} = BoltProtocol.run( :gen_tcp, port, bolt_version, "RETURN $num AS num", %{num: 14}, metadata, recv_timeout: 5000 ) end test "Bolt >=2 run syntax should upscale nicely", %{port: port, bolt_version: bolt_version} do assert {:ok, {:success, %{"fields" => ["num"]}}} = BoltProtocol.run(:gen_tcp, port, bolt_version, "RETURN 5 AS num", %{}, recv_timeout: 5000 ) end end describe "run_statement/7" do test "(no params, no metadata, no options)", %{port: port, bolt_version: bolt_version} do assert [success: _, record: _, success: _] = BoltProtocol.run_statement(:gen_tcp, port, bolt_version, "RETURN 1 AS num") end test "(params, no metadata, no options)", %{port: port, bolt_version: bolt_version} do assert [success: _, record: _, success: _] = BoltProtocol.run_statement(:gen_tcp, port, bolt_version, "RETURN $num AS num", %{ num: 14 }) end test "(no params, metadata, no options)", %{port: port, bolt_version: bolt_version} do {:ok, metadata} = Metadata.new(%{tx_timeout: 1_000}) assert [success: _, record: _, success: _] = BoltProtocol.run_statement( :gen_tcp, port, bolt_version, "RETURN 1 AS num", %{}, metadata ) end test "(params, metadata, no options)", %{port: port, bolt_version: bolt_version} do {:ok, metadata} = Metadata.new(%{tx_timeout: 1_000}) assert [success: _, record: _, success: _] = BoltProtocol.run_statement( :gen_tcp, port, bolt_version, "RETURN $num AS num", %{num: 14}, metadata ) end test "(no params, no metadata, options)", %{port: port, bolt_version: bolt_version} do assert [success: _, record: _, success: _] = BoltProtocol.run_statement( :gen_tcp, port, bolt_version, "RETURN 1 AS num", %{}, %{}, recv_timeout: 5000 ) end test "(params, no metadata, options)", %{port: port, bolt_version: bolt_version} do assert [success: _, record: _, success: _] = BoltProtocol.run_statement( :gen_tcp, port, bolt_version, "RETURN $num AS num", %{num: 14}, %{}, recv_timeout: 5000 ) end test "(no params, metadata, options)", %{port: port, bolt_version: bolt_version} do {:ok, metadata} = Metadata.new(%{tx_timeout: 1_000}) assert [success: _, record: _, success: _] = BoltProtocol.run_statement( :gen_tcp, port, bolt_version, "RETURN 1 AS num", %{}, metadata, recv_timeout: 5000 ) end test "(params, metadata, options)", %{port: port, bolt_version: bolt_version} do {:ok, metadata} = Metadata.new(%{tx_timeout: 1_000}) assert [success: _, record: _, success: _] = BoltProtocol.run_statement( :gen_tcp, port, bolt_version, "RETURN $num AS num", %{num: 14}, metadata, recv_timeout: 5000 ) end end describe "transactions" do test "Successful committed transaction (begin + run_statement + commit)", %{ port: port, bolt_version: bolt_version } do {:ok, _} = BoltProtocol.begin(:gen_tcp, port, bolt_version) [success: _, record: _, success: _] = BoltProtocol.run_statement(:gen_tcp, port, bolt_version, "RETURN 1 AS num") assert {:ok, _} = BoltProtocol.commit(:gen_tcp, port, bolt_version) end test "Successful rollbacked transaction (begin + run_statement + rollback)", %{ port: port, bolt_version: bolt_version } do {:ok, _} = BoltProtocol.begin(:gen_tcp, port, bolt_version) [success: _, record: _, success: _] = BoltProtocol.run_statement(:gen_tcp, port, bolt_version, "RETURN 1 AS num") assert :ok = BoltProtocol.rollback(:gen_tcp, port, bolt_version) end test "If an error occurs during transaction, ROLLBACK is performed at server-level", %{ port: port, bolt_version: bolt_version } do {:ok, _} = BoltProtocol.begin(:gen_tcp, port, bolt_version) [success: _, record: _, success: _] = BoltProtocol.run_statement( :gen_tcp, port, bolt_version, "CREATE (t:Test {value: 555}) RETURN t" ) %Bolt.Sips.Internals.Error{} = BoltProtocol.run_statement(:gen_tcp, port, bolt_version, "RETRN 1 AS num") assert :ok = BoltProtocol.reset(:gen_tcp, port, bolt_version) [success: _, record: [0], success: _] = BoltProtocol.run_statement( :gen_tcp, port, bolt_version, "MATCH (t:Test {value: 555}) RETURN COUNT(t) AS num_node" ) end end end ================================================ FILE: test/bolt_sips/internals/bolt_protocol_v1_test.exs ================================================ defmodule BoltProtocolV1.Sips.Internals.BoltProtocolV1Test do use ExUnit.Case, async: true @moduletag :bolt_v1 alias Bolt.Sips.Internals.BoltProtocolV1 alias Bolt.Sips.Internals.BoltVersionHelper setup do app_config = Application.get_env(:bolt_sips, Bolt) port = Keyword.get(app_config, :port, 7687) auth = {app_config[:basic_auth][:username], app_config[:basic_auth][:password]} config = app_config |> Keyword.put(:port, port) |> Keyword.put(:auth, auth) {:ok, port} = :gen_tcp.connect(config[:url], config[:port], active: false, mode: :binary, packet: :raw) on_exit(fn -> :gen_tcp.close(port) end) {:ok, config: config, port: port} end test "handshake/3", %{port: port} do assert {:ok, version} = BoltProtocolV1.handshake(:gen_tcp, port, []) assert is_integer(version) assert version in BoltVersionHelper.available_versions() end describe "init/5:" do test "ok", %{config: config, port: port} do assert {:ok, _bolt_version} = BoltProtocolV1.handshake(:gen_tcp, port, []) assert {:ok, _} = BoltProtocolV1.init( :gen_tcp, port, 1, config[:auth], [] ) end test "invalid auth", %{config: config, port: port} do assert {:ok, _bolt_version} = BoltProtocolV1.handshake(:gen_tcp, port, []) assert {:error, _} = BoltProtocolV1.init( :gen_tcp, port, 1, {config[:basic_auth][:username], "wrong!"}, [] ) end end describe "run/6:" do test "ok without parameters", %{config: config, port: port} do assert {:ok, _bolt_version} = BoltProtocolV1.handshake(:gen_tcp, port, []) assert {:ok, _} = BoltProtocolV1.init(:gen_tcp, port, 1, config[:auth], []) assert {:ok, {:success, %{"fields" => ["num"]}}} = BoltProtocolV1.run(:gen_tcp, port, 1, "RETURN 1 AS num", %{}, []) end test "ok with parameters", %{config: config, port: port} do assert {:ok, _bolt_version} = BoltProtocolV1.handshake(:gen_tcp, port, []) assert {:ok, _} = BoltProtocolV1.init(:gen_tcp, port, 1, config[:auth], []) assert {:ok, {:success, %{"fields" => ["num"]}}} = BoltProtocolV1.run(:gen_tcp, port, 1, "RETURN $num AS num", %{num: 5}, []) end test "returns IGNORED when sending RUN on a FAILURE state", %{config: config, port: port} do assert {:ok, _bolt_version} = BoltProtocolV1.handshake(:gen_tcp, port, []) assert {:ok, _} = BoltProtocolV1.init(:gen_tcp, port, 1, config[:auth], []) assert {:error, _} = BoltProtocolV1.run(:gen_tcp, port, 1, "Invalid cypher", %{}, []) assert {:error, _} = BoltProtocolV1.pull_all(:gen_tcp, port, 1, []) end test "ok after IGNORED AND ACK_FAILURE", %{config: config, port: port} do assert {:ok, _bolt_version} = BoltProtocolV1.handshake(:gen_tcp, port, []) assert {:ok, _} = BoltProtocolV1.init(:gen_tcp, port, 1, config[:auth], []) assert {:error, _} = BoltProtocolV1.run(:gen_tcp, port, 1, "Invalid cypher", %{}, []) assert {:error, _} = BoltProtocolV1.pull_all(:gen_tcp, port, 1, []) :ok = BoltProtocolV1.ack_failure(:gen_tcp, port, 1, []) assert {:ok, {:success, %{"fields" => ["num"]}}} = BoltProtocolV1.run(:gen_tcp, port, 1, "RETURN 1 AS num", %{}, []) assert {:ok, [ record: [1], success: %{"type" => "r"} ]} = BoltProtocolV1.pull_all(:gen_tcp, port, 1, []) end end test "pull_all/4 (successful)", %{config: config, port: port} do assert {:ok, _bolt_version} = BoltProtocolV1.handshake(:gen_tcp, port, []) assert {:ok, _} = BoltProtocolV1.init(:gen_tcp, port, 1, config[:auth], []) assert {:ok, {:success, %{"fields" => ["num"]}}} = BoltProtocolV1.run(:gen_tcp, port, 1, "RETURN 1 AS num", %{}, []) assert {:ok, [ record: [1], success: %{"type" => "r"} ]} = BoltProtocolV1.pull_all(:gen_tcp, port, 1, []) end test "run_statement/6 (successful)", %{config: config, port: port} do assert {:ok, _bolt_version} = BoltProtocolV1.handshake(:gen_tcp, port, []) assert {:ok, _} = BoltProtocolV1.init(:gen_tcp, port, 1, config[:auth], []) assert [_ | _] = BoltProtocolV1.run_statement(:gen_tcp, port, 1, "RETURN 1 AS num", %{}, []) end test "discard_all/4 (successful)", %{config: config, port: port} do assert {:ok, _bolt_version} = BoltProtocolV1.handshake(:gen_tcp, port, []) assert {:ok, _} = BoltProtocolV1.init(:gen_tcp, port, 1, config[:auth], []) assert {:ok, {:success, %{"fields" => ["num"]}}} = BoltProtocolV1.run(:gen_tcp, port, 1, "RETURN 1 AS num", %{}, []) assert :ok = BoltProtocolV1.discard_all(:gen_tcp, port, 1, []) end test "ack_failure/4 (successful)", %{config: config, port: port} do assert {:ok, _bolt_version} = BoltProtocolV1.handshake(:gen_tcp, port, []) assert {:ok, _} = BoltProtocolV1.init(:gen_tcp, port, 1, config[:auth], []) assert {:error, _} = BoltProtocolV1.run(:gen_tcp, port, 1, "Invalid cypher", %{}, []) assert :ok = BoltProtocolV1.ack_failure(:gen_tcp, port, 1, []) end describe "reset/4" do test "ok", %{config: config, port: port} do assert {:ok, _bolt_version} = BoltProtocolV1.handshake(:gen_tcp, port, []) assert {:ok, _} = BoltProtocolV1.init(:gen_tcp, port, 1, config[:auth], []) assert {:ok, {:success, %{"fields" => ["num"]}}} = BoltProtocolV1.run(:gen_tcp, port, 1, "RETURN 1 AS num", %{}, []) assert :ok = BoltProtocolV1.reset(:gen_tcp, port, 1, []) end test "ok during process", %{config: config, port: port} do assert {:ok, _bolt_version} = BoltProtocolV1.handshake(:gen_tcp, port, []) assert {:ok, _} = BoltProtocolV1.init(:gen_tcp, port, 1, config[:auth], []) assert {:error, _} = BoltProtocolV1.run(:gen_tcp, port, 1, "Invalid cypher", %{}, []) {:error, _} = BoltProtocolV1.pull_all(:gen_tcp, port, 1, []) assert :ok = BoltProtocolV1.reset(:gen_tcp, port, 1, []) assert {:ok, {:success, %{"fields" => ["num"]}}} = BoltProtocolV1.run(:gen_tcp, port, 1, "RETURN 1 AS num", %{}, []) assert {:ok, [{:record, _}, {:success, _}]} = BoltProtocolV1.pull_all(:gen_tcp, port, 1, []) end end end ================================================ FILE: test/bolt_sips/internals/bolt_protocol_v3_test.exs ================================================ defmodule Bolt.Sips.Internals.BoltProtocolV3Test do use ExUnit.Case, async: true @moduletag :bolt_v3 alias Bolt.Sips.Internals.BoltProtocol alias Bolt.Sips.Internals.BoltProtocolV3 alias Bolt.Sips.Metadata setup do app_config = Application.get_env(:bolt_sips, Bolt) port = Keyword.get(app_config, :port, 7687) auth = {app_config[:basic_auth][:username], app_config[:basic_auth][:password]} config = app_config |> Keyword.put(:port, port) |> Keyword.put(:auth, auth) |> Bolt.Sips.Utils.default_config() {:ok, port} = config[:hostname] |> String.to_charlist() |> :gen_tcp.connect(config[:port], active: false, mode: :binary, packet: :raw ) {:ok, _} = BoltProtocol.handshake(:gen_tcp, port, []) on_exit(fn -> :gen_tcp.close(port) end) {:ok, config: config, port: port} end describe "hello/5:" do test "ok", %{config: config, port: port} do assert {:ok, _} = BoltProtocolV3.hello( :gen_tcp, port, 3, config[:auth], [] ) end test "invalid auth", %{config: config, port: port} do assert {:error, _} = BoltProtocolV3.hello( :gen_tcp, port, 3, {config[:basic_auth][:username], "wrong!"}, [] ) end end test "goodbye/5", %{config: config, port: port} do assert {:ok, _} = BoltProtocolV3.hello(:gen_tcp, port, 3, config[:auth], []) assert :ok = BoltProtocolV3.goodbye(:gen_tcp, port, 3) end describe "run/7:" do test "ok without parameters nor metadata", %{config: config, port: port} do assert {:ok, _} = BoltProtocolV3.hello(:gen_tcp, port, 3, config[:auth], []) assert {:ok, {:success, %{"fields" => ["num"]}}} = BoltProtocolV3.run(:gen_tcp, port, 3, "RETURN 1 AS num", %{}, %{}, []) end test "ok without parameters with metadata", %{config: config, port: port} do assert {:ok, _} = BoltProtocolV3.hello(:gen_tcp, port, 3, config[:auth], []) {:ok, metadata} = Metadata.new(%{tx_timeout: 10_000}) assert {:ok, {:success, %{"fields" => ["num"]}}} = BoltProtocolV3.run(:gen_tcp, port, 3, "RETURN 1 AS num", %{}, metadata, []) end test "ok with parameters without metadata", %{config: config, port: port} do assert {:ok, _} = BoltProtocolV3.hello(:gen_tcp, port, 3, config[:auth], []) assert {:ok, {:success, %{"fields" => ["num"]}}} = BoltProtocolV3.run(:gen_tcp, port, 3, "RETURN $num AS num", %{num: 5}, %{}, []) end test "ok with parameters with metadata", %{config: config, port: port} do assert {:ok, _} = BoltProtocolV3.hello(:gen_tcp, port, 3, config[:auth], []) {:ok, metadata} = Metadata.new(%{tx_timeout: 10_000}) assert {:ok, {:success, %{"fields" => ["num"]}}} = BoltProtocolV3.run( :gen_tcp, port, 3, "RETURN $num AS num", %{num: 5}, metadata, [] ) end test "returns IGNORED when sending RUN on a FAILURE state", %{config: config, port: port} do assert {:ok, _} = BoltProtocolV3.hello(:gen_tcp, port, 3, config[:auth], []) assert {:error, _} = BoltProtocolV3.run(:gen_tcp, port, 3, "Invalid cypher", %{}, %{}, []) assert {:error, _} = BoltProtocol.pull_all(:gen_tcp, port, 3, []) end test "ok after IGNORED and RESET", %{config: config, port: port} do assert {:ok, _} = BoltProtocolV3.hello(:gen_tcp, port, 3, config[:auth], []) assert {:error, _} = BoltProtocolV3.run(:gen_tcp, port, 3, "Invalid cypher", %{}, %{}, []) assert {:error, _} = BoltProtocol.pull_all(:gen_tcp, port, 3, []) :ok = BoltProtocol.reset(:gen_tcp, port, 3, []) assert {:ok, {:success, %{"fields" => ["num"]}}} = BoltProtocolV3.run(:gen_tcp, port, 3, "RETURN 1 AS num", %{}, %{}, []) assert {:ok, [ record: [1], success: %{"type" => "r"} ]} = BoltProtocol.pull_all(:gen_tcp, port, 3, []) end end test "run_statement/7 (successful)", %{config: config, port: port} do assert {:ok, _} = BoltProtocolV3.hello(:gen_tcp, port, 3, config[:auth], []) assert [_ | _] = BoltProtocolV3.run_statement(:gen_tcp, port, 3, "RETURN 1 AS num", %{}, %{}, []) end test "pull_all/4 (successful)", %{config: config, port: port} do assert {:ok, _} = BoltProtocolV3.hello(:gen_tcp, port, 3, config[:auth], []) assert {:ok, {:success, %{"fields" => ["num"]}}} = BoltProtocolV3.run(:gen_tcp, port, 3, "RETURN 1 AS num", %{}, %{}, []) assert {:ok, [ record: [1], success: %{"type" => "r"} ]} = BoltProtocol.pull_all(:gen_tcp, port, 3, []) end test "discard_all/4 (successful)", %{config: config, port: port} do assert {:ok, _} = BoltProtocolV3.hello(:gen_tcp, port, 3, config[:auth], []) assert {:ok, {:success, %{"fields" => ["num"]}}} = BoltProtocolV3.run(:gen_tcp, port, 1, "RETURN 1 AS num", %{}, %{}, []) assert :ok = BoltProtocol.discard_all(:gen_tcp, port, 3, []) end test "reset/4 (successful)", %{config: config, port: port} do assert {:ok, _} = BoltProtocolV3.hello(:gen_tcp, port, 3, config[:auth], []) assert {:ok, {:success, %{"fields" => ["num"]}}} = BoltProtocolV3.run(:gen_tcp, port, 3, "RETURN 1 AS num", %{}, %{}, []) assert :ok = BoltProtocol.reset(:gen_tcp, port, 3, []) end describe "Transaction management" do test "Open a transaction without metadata", %{config: config, port: port} do assert {:ok, _} = BoltProtocolV3.hello(:gen_tcp, port, 3, config[:auth], []) {:ok, _} = BoltProtocolV3.begin(:gen_tcp, port, 3, %{}, []) end # Work only with Neo4j Enterprise @tag :enterprise test "Open a transaction with metadata", %{config: config, port: port} do assert {:ok, _} = BoltProtocolV3.hello(:gen_tcp, port, 3, config[:auth], []) {:ok, metadata} = Metadata.new(%{bookmarks: ["neo4j:bookmark:v1:tx234"], tx_timeout: 1_000}) {:ok, _} = BoltProtocolV3.begin(:gen_tcp, port, 3, metadata, []) end test "Commit a transaction", %{config: config, port: port} do assert {:ok, _} = BoltProtocolV3.hello(:gen_tcp, port, 3, config[:auth], []) {:ok, _} = BoltProtocolV3.begin(:gen_tcp, port, 3, %{}, []) assert {:ok, {:success, %{"fields" => ["num"]}}} = BoltProtocolV3.run(:gen_tcp, port, 3, "RETURN 1 AS num", %{}, %{}, []) assert {:ok, _} = BoltProtocol.pull_all(:gen_tcp, port, 3, []) {:ok, %{"bookmark" => _}} = BoltProtocolV3.commit(:gen_tcp, port, 3, []) end test "Rollback a transaction", %{config: config, port: port} do assert {:ok, _} = BoltProtocolV3.hello(:gen_tcp, port, 3, config[:auth], []) {:ok, _} = BoltProtocolV3.begin(:gen_tcp, port, 3, %{}, []) assert {:ok, {:success, %{"fields" => ["num"]}}} = BoltProtocolV3.run(:gen_tcp, port, 3, "RETURN 1 AS num", %{}, %{}, []) BoltProtocol.discard_all(:gen_tcp, port, 3, []) assert :ok = BoltProtocolV3.rollback(:gen_tcp, port, 3, []) end # It works with Neo4j Enterprise only @tag :enterprise test "With socket instead of gent_tcp", %{config: config, port: port} do assert {:ok, _} = BoltProtocolV3.hello(Bolt.Sips.Socket, port, 3, config[:auth], []) {:ok, metadata} = Metadata.new(%{bookmarks: ["neo4j:bookmark:v1:tx234"], tx_timeout: 1_000}) {:ok, _} = BoltProtocolV3.begin(Bolt.Sips.Socket, port, 3, metadata, []) end end end ================================================ FILE: test/bolt_sips/internals/bolt_version_helper_test.exs ================================================ defmodule Bolt.Sips.Internals.BoltVersionHelperTest do use ExUnit.Case, async: true doctest Bolt.Sips.Internals.BoltVersionHelper alias Bolt.Sips.Internals.BoltVersionHelper test "available_bolt_versions/0 returns a list" do assert [_ | _] = BoltVersionHelper.available_versions() end describe "previous/1" do test "successfully return the previous version" do assert 1 == BoltVersionHelper.previous(2) assert 2 == BoltVersionHelper.previous(3) assert 3 == BoltVersionHelper.previous(4) end test "return nil if there is no previous version" do assert nil == BoltVersionHelper.previous(1) end end end ================================================ FILE: test/bolt_sips/internals/logger_test.exs ================================================ defmodule Bolt.Sips.Internals.LoggerTest do use ExUnit.Case import ExUnit.CaptureLog alias Bolt.Sips.Internals.Logger test "Log from formed message" do assert capture_log(fn -> Logger.log_message(:client, {:success, %{data: "ok"}}) end) =~ "C: SUCCESS ~ %{data: \"ok\"}" end test "Log from non-formed message" do assert capture_log(fn -> Logger.log_message(:client, :success, %{data: "ok"}) end) =~ "C: SUCCESS ~ %{data: \"ok\"}" end # Excluded as another test has a long result and therefore a long hex and slow down tests # test "Log hex data" do # assert capture_log(fn -> Logger.log_message(:client, :success, <<0x01, 0xAF>>, :hex) end) =~ # "C: SUCCESS ~ <<0x1, 0xAF>>" # end end ================================================ FILE: test/bolt_sips/internals/pack_stream/decoder_test.exs ================================================ defmodule Bolt.Sips.Internals.PackStream.DecoderTest do use ExUnit.Case, async: true alias Bolt.Sips.Internals.PackStream.Decoder alias Bolt.Sips.Internals.PackStreamError alias Bolt.Sips.Internals.BoltVersionHelper alias Bolt.Sips.Types describe "Decode common types" do Enum.each(BoltVersionHelper.available_versions(), fn bolt_version -> test "Null (bolt_version: #{bolt_version})" do assert [nil] == Decoder.decode(<<0xC0>>, unquote(bolt_version)) end test "Boolean (bolt_version: #{bolt_version})" do assert [false] == Decoder.decode(<<0xC2>>, unquote(bolt_version)) assert [true] == Decoder.decode(<<0xC3>>, unquote(bolt_version)) end test "Float (bolt_version: #{bolt_version})" do assert [7.7] == Decoder.decode( <<0xC1, 0x40, 0x1E, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCD>>, unquote(bolt_version) ) end test "String (bolt_version: #{bolt_version})" do assert ["hello"] == Decoder.decode(<<0x85, 0x68, 0x65, 0x6C, 0x6C, 0x6F>>, unquote(bolt_version)) end test "List (bolt_version: #{bolt_version})" do assert [[]] == Decoder.decode(<<0x90>>, unquote(bolt_version)) assert [[2, 4]] == Decoder.decode(<<0x92, 0x2, 0x4>>, unquote(bolt_version)) end test "Integer (bolt_version: #{bolt_version})" do assert [42] == Decoder.decode(<<0x2A>>, unquote(bolt_version)) end test "Node (bolt_version: #{bolt_version})" do node = <<0x91, 0xB3, 0x4E, 0x11, 0x91, 0x86, 0x50, 0x65, 0x72, 0x73, 0x6F, 0x6E, 0xA2, 0x84, 0x6E, 0x61, 0x6D, 0x65, 0xD0, 0x10, 0x50, 0x61, 0x74, 0x72, 0x69, 0x63, 0x6B, 0x20, 0x52, 0x6F, 0x74, 0x68, 0x66, 0x75, 0x73, 0x73, 0x89, 0x62, 0x6F, 0x6C, 0x74, 0x5F, 0x73, 0x69, 0x70, 0x73, 0xC3>> assert [ [ %Bolt.Sips.Types.Node{ id: 17, labels: ["Person"], properties: %{"bolt_sips" => true, "name" => "Patrick Rothfuss"} } ] ] == Decoder.decode(node, unquote(bolt_version)) end test "Relationship (bolt_version: #{bolt_version})" do rel = <<0x91, 0xB5, 0x52, 0x50, 0x46, 0x43, 0x85, 0x57, 0x52, 0x4F, 0x54, 0x45, 0xA0>> assert [ [ %Bolt.Sips.Types.Relationship{ end: 67, id: 80, properties: %{}, start: 70, type: "WROTE" } ] ] = Decoder.decode(rel, unquote(bolt_version)) end # test "UnboundRelationship (bolt_version: #{bolt_version})" do # end test "Path (bolt_version: #{bolt_version})" do path = <<0x91, 0xB3, 0x50, 0x92, 0xB3, 0x4E, 0x30, 0x90, 0xA2, 0x84, 0x6E, 0x61, 0x6D, 0x65, 0x85, 0x41, 0x6C, 0x69, 0x63, 0x65, 0x89, 0x62, 0x6F, 0x6C, 0x74, 0x5F, 0x73, 0x69, 0x70, 0x73, 0xC3, 0xB3, 0x4E, 0x38, 0x90, 0xA2, 0x84, 0x6E, 0x61, 0x6D, 0x65, 0x83, 0x42, 0x6F, 0x62, 0x89, 0x62, 0x6F, 0x6C, 0x74, 0x5F, 0x73, 0x69, 0x70, 0x73, 0xC3, 0x91, 0xB3, 0x72, 0x13, 0x85, 0x4B, 0x4E, 0x4F, 0x57, 0x53, 0xA0, 0x92, 0x1, 0x1>> [ [ %Bolt.Sips.Types.Path{ nodes: [ %Bolt.Sips.Types.Node{ id: 48, labels: [], properties: %{"bolt_sips" => true, "name" => "Alice"} }, %Bolt.Sips.Types.Node{ id: 56, labels: [], properties: %{"bolt_sips" => true, "name" => "Bob"} } ], relationships: [ %Bolt.Sips.Types.UnboundRelationship{ end: nil, id: 19, properties: %{}, start: nil, type: "KNOWS" } ], sequence: [1, 1] } ] ] = Decoder.decode(path, unquote(bolt_version)) end test "Fails to decode something unknown (bolt_version: #{bolt_version})" do assert_raise PackStreamError, fn -> Decoder.decode(0xFF, unquote(bolt_version)) end end end) end describe "Decodes Bolt >= 2 specific types" do BoltVersionHelper.available_versions() |> Enum.filter(&(&1 >= 2)) |> Enum.each(fn bolt_version -> test "Local Date (bolt_version: #{bolt_version})" do assert [~D[2013-12-15]] == Decoder.decode(<<0xB1, 0x44, 0xC9, 0x3E, 0xB6>>, unquote(bolt_version)) end test "Local Time (bolt_version: #{bolt_version})" do assert [~T[09:34:23.654321]] == Decoder.decode( <<0xB1, 0x74, 0xCB, 0x0, 0x0, 0x1F, 0x58, 0x31, 0xDF, 0x9B, 0x68>>, unquote(bolt_version) ) end test "Local DateTime (bolt_version: #{bolt_version})" do assert [~N[2018-04-05 12:34:00.654321]] == Decoder.decode( <<0xB2, 0x64, 0xCA, 0x5A, 0xC6, 0x17, 0xB8, 0xCA, 0x27, 0x0, 0x25, 0x68>>, unquote(bolt_version) ) end test "Time with timezone offset (bolt_version: #{bolt_version})" do ttz = Types.TimeWithTZOffset.create(~T[12:45:30.654321], 3600) assert [ttz] == Decoder.decode( <<0xB2, 0x54, 0xCB, 0x0, 0x0, 0x29, 0xC6, 0x10, 0x55, 0xC9, 0x68, 0xC9, 0xE, 0x10>>, unquote(bolt_version) ) end test "Datetime with timezone id (bolt_version: #{bolt_version})" do dt = Bolt.Sips.TypesHelper.datetime_with_micro( ~N[2016-05-24 13:26:08.654321], "Europe/Berlin" ) assert [dt] == Decoder.decode( <<0xB3, 0x66, 0xCA, 0x57, 0x44, 0x56, 0x70, 0xCA, 0x27, 0x0, 0x25, 0x68, 0x8D, 0x45, 0x75, 0x72, 0x6F, 0x70, 0x65, 0x2F, 0x42, 0x65, 0x72, 0x6C, 0x69, 0x6E>>, unquote(bolt_version) ) end test "Datetime with timezone offset (bolt_version: #{bolt_version})" do assert [ %Types.DateTimeWithTZOffset{ naive_datetime: ~N[2016-05-24 13:26:08.654321], timezone_offset: 7200 } ] = Decoder.decode( <<0xB3, 0x46, 0xCA, 0x57, 0x44, 0x56, 0x70, 0xCA, 0x27, 0x0, 0x25, 0x68, 0xC9, 0x1C, 0x20>>, unquote(bolt_version) ) end test "Duration (bolt_version: #{bolt_version})" do assert [ %Types.Duration{ years: 1, months: 3, days: 34, hours: 2, minutes: 32, seconds: 54, nanoseconds: 5550 } ] == Decoder.decode( <<0xB4, 0x45, 0xF, 0x22, 0xC9, 0x23, 0xD6, 0xC9, 0x15, 0xAE>>, unquote(bolt_version) ) end test "Point 2D cartesian (bolt_version: #{bolt_version})" do assert [ %Types.Point{ crs: "cartesian", height: nil, latitude: nil, longitude: nil, srid: 7203, x: 40.0, y: 45.0, z: nil } ] = Decoder.decode( <<0xB3, 0x58, 0xC9, 0x1C, 0x23, 0xC1, 0x40, 0x44, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC1, 0x40, 0x46, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0>>, unquote(bolt_version) ) end test "Point 2D geographic (bolt_version: #{bolt_version})" do assert [ %Types.Point{ crs: "wgs-84", height: nil, latitude: 45.0, longitude: 40.0, srid: 4326, x: 40.0, y: 45.0, z: nil } ] = Decoder.decode( <<0xB3, 0x58, 0xC9, 0x10, 0xE6, 0xC1, 0x40, 0x44, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC1, 0x40, 0x46, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0>>, unquote(bolt_version) ) end test "Point 3D cartesian (bolt_version: #{bolt_version})" do assert [ %Types.Point{ crs: "cartesian-3d", height: nil, latitude: nil, longitude: nil, srid: 9157, x: 40.0, y: 45.0, z: 150.0 } ] = Decoder.decode( <<0xB4, 0x59, 0xC9, 0x23, 0xC5, 0xC1, 0x40, 0x44, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC1, 0x40, 0x46, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC1, 0x40, 0x62, 0xC0, 0x0, 0x0, 0x0, 0x0, 0x0>>, unquote(bolt_version) ) end test "Point 3D geographic (bolt_version: #{bolt_version})" do assert [ %Types.Point{ crs: "wgs-84-3d", height: 150.0, latitude: 45.0, longitude: 40.0, srid: 4979, x: 40.0, y: 45.0, z: 150.0 } ] = Decoder.decode( <<0xB4, 0x59, 0xC9, 0x13, 0x73, 0xC1, 0x40, 0x44, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC1, 0x40, 0x46, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC1, 0x40, 0x62, 0xC0, 0x0, 0x0, 0x0, 0x0, 0x0>>, unquote(bolt_version) ) end end) end end ================================================ FILE: test/bolt_sips/internals/pack_stream/decoder_v1_test.exs ================================================ defmodule Bolt.Sips.Internals.PackStream.DecoderV1Test do use ExUnit.Case, async: true alias Bolt.Sips.Internals.PackStream alias Bolt.Sips.Internals.PackStream.DecoderV1 test "decodes null" do assert DecoderV1.decode(<<0xC0>>, 1) == [nil] end test "decodes boolean" do assert DecoderV1.decode(<<0xC3>>, 1) == [true] assert DecoderV1.decode(<<0xC2>>, 1) == [false] end test "decodes floats" do positive = <<0xC1, 0x3F, 0xF1, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9A>> negative = <<0xC1, 0xBF, 0xF1, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9A>> assert DecoderV1.decode(positive, 1) == [1.1] assert DecoderV1.decode(negative, 1) == [-1.1] end test "decodes strings" do longstr = <<0xD0, 0x1A, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A>> specialcharstr = <<0xD0, 0x18, 0x45, 0x6E, 0x20, 0xC3, 0xA5, 0x20, 0x66, 0x6C, 0xC3, 0xB6, 0x74, 0x20, 0xC3, 0xB6, 0x76, 0x65, 0x72, 0x20, 0xC3, 0xA4, 0x6E, 0x67, 0x65, 0x6E>> assert DecoderV1.decode(<<0x80>>, 1) == [""] assert DecoderV1.decode(<<0x81, 0x61>>, 1) == ["a"] assert DecoderV1.decode(longstr, 1) == ["abcdefghijklmnopqrstuvwxyz"] assert DecoderV1.decode(specialcharstr, 1) == ["En å flöt över ängen"] end test "decodes lists" do assert DecoderV1.decode(<<0x90>>, 1) == [[]] assert DecoderV1.decode(<<0x93, 0x01, 0x02, 0x03>>, 1) == [[1, 2, 3]] list_8 = <<0xD4, 16::8>> <> (1..16 |> Enum.map(&PackStream.encode(&1, 1)) |> Enum.join()) assert DecoderV1.decode(list_8, 1) == [1..16 |> Enum.to_list()] list_16 = <<0xD5, 256::16>> <> (1..256 |> Enum.map(&PackStream.encode(&1, 1)) |> Enum.join()) assert DecoderV1.decode(list_16, 1) == [1..256 |> Enum.to_list()] list_32 = <<0xD6, 66_000::32>> <> (1..66_000 |> Enum.map(&PackStream.encode(&1, 1)) |> Enum.join()) assert DecoderV1.decode(list_32, 1) == [1..66_000 |> Enum.to_list()] ending_0_list = <<0x93, 0x91, 0x1, 0x92, 0x2, 0x0, 0x0>> assert DecoderV1.decode(ending_0_list, 1) == [[[1], [2, 0], 0]] end test "decodes maps" do assert DecoderV1.decode(<<0xA0>>, 1) == [%{}] assert DecoderV1.decode(<<0xA1, 0x81, 0x61, 0x01>>, 1) == [%{"a" => 1}] assert DecoderV1.decode(<<0xAB, 0x81, 0x61, 0x01>>, 1) == [%{"a" => 1}] map_8 = <<0xD8, 16::8>> <> (1..16 |> Enum.map(fn i -> :erlang.iolist_to_binary(PackStream.encode("#{i}", 1)) <> <<1>> end) |> Enum.join()) assert DecoderV1.decode(map_8, 1) |> List.first() |> map_size == 16 map_16 = <<0xD9, 256::16>> <> (1..256 |> Enum.map(fn i -> :erlang.iolist_to_binary(PackStream.encode("#{i}", 1)) <> <<1>> end) |> Enum.join()) assert DecoderV1.decode(map_16, 1) |> List.first() |> map_size == 256 map_32 = <<0xDA, 66_000::32>> <> (1..66_000 |> Enum.map(fn i -> :erlang.iolist_to_binary(PackStream.encode("#{i}", 1)) <> <<1>> end) |> Enum.join()) assert DecoderV1.decode(map_32, 1) |> List.first() |> map_size == 66_000 end test "decodes integers" do assert DecoderV1.decode(<<0x2A>>, 1) == [42] assert DecoderV1.decode(<<0xC8, 0x2A>>, 1) == [42] assert DecoderV1.decode(<<0xC9, 0, 0x2A>>, 1) == [42] assert DecoderV1.decode(<<0xCA, 0, 0, 0, 0x2A>>, 1) == [42] assert DecoderV1.decode(<<0xCB, 0, 0, 0, 0, 0, 0, 0, 0x2A>>, 1) == [42] end test "decodes negative integers" do assert DecoderV1.decode(<<0xC8, 0xD6>>, 1) == [-42] end test "decodes Node" do node = <<0x91, 0xB3, 0x4E, 0x11, 0x91, 0x86, 0x50, 0x65, 0x72, 0x73, 0x6F, 0x6E, 0xA2, 0x84, 0x6E, 0x61, 0x6D, 0x65, 0xD0, 0x10, 0x50, 0x61, 0x74, 0x72, 0x69, 0x63, 0x6B, 0x20, 0x52, 0x6F, 0x74, 0x68, 0x66, 0x75, 0x73, 0x73, 0x89, 0x62, 0x6F, 0x6C, 0x74, 0x5F, 0x73, 0x69, 0x70, 0x73, 0xC3>> assert [ [ %Bolt.Sips.Types.Node{ id: 17, labels: ["Person"], properties: %{"bolt_sips" => true, "name" => "Patrick Rothfuss"} } ] ] == DecoderV1.decode(node, 1) end test "decodes Relationship" do rel = <<0x91, 0xB5, 0x52, 0x50, 0x46, 0x43, 0x85, 0x57, 0x52, 0x4F, 0x54, 0x45, 0xA0>> assert [ [ %Bolt.Sips.Types.Relationship{ end: 67, id: 80, properties: %{}, start: 70, type: "WROTE" } ] ] = DecoderV1.decode(rel, 1) end test "decodes path" do path = <<0x91, 0xB3, 0x50, 0x92, 0xB3, 0x4E, 0x30, 0x90, 0xA2, 0x84, 0x6E, 0x61, 0x6D, 0x65, 0x85, 0x41, 0x6C, 0x69, 0x63, 0x65, 0x89, 0x62, 0x6F, 0x6C, 0x74, 0x5F, 0x73, 0x69, 0x70, 0x73, 0xC3, 0xB3, 0x4E, 0x38, 0x90, 0xA2, 0x84, 0x6E, 0x61, 0x6D, 0x65, 0x83, 0x42, 0x6F, 0x62, 0x89, 0x62, 0x6F, 0x6C, 0x74, 0x5F, 0x73, 0x69, 0x70, 0x73, 0xC3, 0x91, 0xB3, 0x72, 0x13, 0x85, 0x4B, 0x4E, 0x4F, 0x57, 0x53, 0xA0, 0x92, 0x1, 0x1>> [ [ %Bolt.Sips.Types.Path{ nodes: [ %Bolt.Sips.Types.Node{ id: 48, labels: [], properties: %{"bolt_sips" => true, "name" => "Alice"} }, %Bolt.Sips.Types.Node{ id: 56, labels: [], properties: %{"bolt_sips" => true, "name" => "Bob"} } ], relationships: [ %Bolt.Sips.Types.UnboundRelationship{ end: nil, id: 19, properties: %{}, start: nil, type: "KNOWS" } ], sequence: [1, 1] } ] ] = DecoderV1.decode(path, 1) end end ================================================ FILE: test/bolt_sips/internals/pack_stream/decoder_v2_test.exs ================================================ defmodule Bolt.Sips.Internals.PackStream.DecoderV2Test do use ExUnit.Case, async: true alias Bolt.Sips.Internals.PackStream.DecoderV2 alias Bolt.Sips.Types.{TimeWithTZOffset, DateTimeWithTZOffset, Duration, Point} describe "Decode temporal data:" do test "date post 1970-01-01" do assert [~D[2018-07-29]] == DecoderV2.decode({0x44, <<0xC9, 0x45, 0x4D>>, 1}, 2) end test "date pre 1970-01-01" do assert [~D[1918-07-29]] == DecoderV2.decode({0x44, <<0xC9, 0xB6, 0xA0>>, 1}, 2) end test "local time" do assert [~T[13:25:01.952456]] == DecoderV2.decode( {0x74, <<0xCB, 0x0, 0x0, 0x2B, 0xEE, 0x2C, 0xB7, 0xD5, 0x40>>, 1}, 2 ) end test "local datetime" do assert [~N[2014-11-30 16:15:01.435432]] == DecoderV2.decode( {0x64, <<0xCA, 0x54, 0x7B, 0x42, 0x85, 0xCA, 0x19, 0xF4, 0x2A, 0x40>>, 2}, 2 ) end test "Time with timezone offzet" do assert [%TimeWithTZOffset{time: ~T[04:45:32.123456], timezone_offset: 7200}] == DecoderV2.decode( {0x54, <<0xCB, 0x0, 0x0, 0xF, 0x94, 0xE2, 0x22, 0x2, 0x0, 0xC9, 0x1C, 0x20>>, 2}, 2 ) end test "Datetime with zone id" do dt = Bolt.Sips.TypesHelper.datetime_with_micro(~N[1998-03-18 06:25:12.123456], "Europe/Paris") assert [dt] == DecoderV2.decode( {0x66, <<0xCA, 0x35, 0xF, 0x68, 0xC8, 0xCA, 0x7, 0x5B, 0xCA, 0x0, 0x8C, 0x45, 0x75, 0x72, 0x6F, 0x70, 0x65, 0x2F, 0x50, 0x61, 0x72, 0x69, 0x73>>, 3}, 2 ) end test "Datetime with zone offset" do assert [ %DateTimeWithTZOffset{ naive_datetime: ~N[1998-03-18 06:25:12.123456], timezone_offset: 7200 } ] == DecoderV2.decode( {0x46, <<0xCA, 0x35, 0xF, 0x68, 0xC8, 0xCA, 0x7, 0x5B, 0xCA, 0x0, 0xC9, 0x1C, 0x20>>, 3}, 2 ) end test "Duration" do assert [ %Duration{ days: 11, hours: 15, minutes: 0, months: 8, nanoseconds: 5550, seconds: 21, weeks: 0, years: 3 } ] == DecoderV2.decode( {0x45, <<0x2C, 0xB, 0xCA, 0x0, 0x0, 0xD3, 0x5, 0xC9, 0x15, 0xAE>>, 4}, 2 ) end test "Point2D (cartesian)" do assert [ %Point{ crs: "cartesian", height: nil, latitude: nil, longitude: nil, srid: 7203, x: 45.0003, y: 34.5434, z: nil } ] == DecoderV2.decode( {0x58, <<0xC9, 0x1C, 0x23, 0xC1, 0x40, 0x46, 0x80, 0x9, 0xD4, 0x95, 0x18, 0x2B, 0xC1, 0x40, 0x41, 0x45, 0x8E, 0x21, 0x96, 0x52, 0xBD>>, 3}, 2 ) end test "Point2D (geographic)" do assert [ %Point{ crs: "wgs-84", height: nil, latitude: 15.00943, longitude: 20.45352, srid: 4326, x: 20.45352, y: 15.00943, z: nil } ] == DecoderV2.decode( {0x58, <<0xC9, 0x10, 0xE6, 0xC1, 0x40, 0x34, 0x74, 0x19, 0xE3, 0x0, 0x14, 0xF9, 0xC1, 0x40, 0x2E, 0x4, 0xD4, 0x2, 0x4B, 0x33, 0xDB>>, 3}, 2 ) end test "Point3D (cartesian)" do assert [ %Point{ crs: "cartesian-3d", height: nil, latitude: nil, longitude: nil, srid: 9157, x: 48.8354, y: 12.72468, z: 50.004 } ] == DecoderV2.decode( {0x59, <<0xC9, 0x23, 0xC5, 0xC1, 0x40, 0x48, 0x6A, 0xEE, 0x63, 0x1F, 0x8A, 0x9, 0xC1, 0x40, 0x29, 0x73, 0x9, 0x41, 0xC8, 0x21, 0x6C, 0xC1, 0x40, 0x49, 0x0, 0x83, 0x12, 0x6E, 0x97, 0x8D>>, 4}, 2 ) end test "Point3D (geographic)" do assert [ %Point{ crs: "wgs-84-3d", height: -123.0004, latitude: 70.40958, longitude: 13.39538, srid: 4979, x: 13.39538, y: 70.40958, z: -123.0004 } ] == DecoderV2.decode( {0x59, <<0xC9, 0x13, 0x73, 0xC1, 0x40, 0x2A, 0xCA, 0x6F, 0x3F, 0x52, 0xFC, 0x26, 0xC1, 0x40, 0x51, 0x9A, 0x36, 0x8F, 0x8, 0x46, 0x20, 0xC1, 0xC0, 0x5E, 0xC0, 0x6, 0x8D, 0xB8, 0xBA, 0xC7>>, 4}, 2 ) end end end ================================================ FILE: test/bolt_sips/internals/pack_stream/encoder_helper_test.exs ================================================ defmodule Bolt.Sips.Internals.PackStream.EncoderHelperTest do use ExUnit.Case, async: true alias Bolt.Sips.Internals.PackStream.EncoderHelper alias Bolt.Sips.Internals.PackStreamError describe "call_encode/3" do test "successfull when call with existing bolt_version" do assert <<_::binary>> = EncoderHelper.call_encode(:atom, true, 1) end test "successfull when call with superior bolt_version" do assert <<_::binary>> = EncoderHelper.call_encode(:atom, true, 4) end test "fails when call with bolt_version <= 0" do assert_raise PackStreamError, fn -> EncoderHelper.call_encode(:atom, true, -1) end end test "fails when call with a non integer bolt_version" do assert_raise PackStreamError, fn -> EncoderHelper.call_encode(:atom, true, :invalid) end end test "fails when call with a non supported data type" do assert_raise PackStreamError, fn -> EncoderHelper.call_encode(:non_supported, true, 1) end end end end ================================================ FILE: test/bolt_sips/internals/pack_stream/encoder_test.exs ================================================ defmodule Bolt.Sips.Internals.PackStream.EncoderTest do use ExUnit.Case, async: false alias Bolt.Sips.Internals.PackStream.Encoder alias Bolt.Sips.Internals.BoltVersionHelper alias Bolt.Sips.Types alias Bolt.Sips.TypesHelper defmodule TestStruct do defstruct foo: "bar" end describe "Encode common types:" do Enum.each(BoltVersionHelper.available_versions(), fn bolt_version -> test "Null (bolt_version: #{bolt_version})" do assert <<0xC0>> == :erlang.iolist_to_binary(Encoder.encode(nil, unquote(bolt_version))) end test "Boolean (bolt_version: #{bolt_version})" do assert <<0xC3>> == :erlang.iolist_to_binary(Encoder.encode(true, unquote(bolt_version))) assert <<0xC2>> == :erlang.iolist_to_binary(Encoder.encode(false, unquote(bolt_version))) end test "Atom (bolt_version: #{bolt_version})" do assert <<0x85, 0x68, 0x65, 0x6C, 0x6C, 0x6F>> == :erlang.iolist_to_binary(Encoder.encode(:hello, unquote(bolt_version))) end test "String (bolt_version: #{bolt_version})" do assert <<0x85, 0x68, 0x65, 0x6C, 0x6C, 0x6F>> == :erlang.iolist_to_binary(Encoder.encode("hello", unquote(bolt_version))) end test "Integer (bolt_version: #{bolt_version})" do assert <<0x7>> == :erlang.iolist_to_binary(Encoder.encode(7, unquote(bolt_version))) end test "Float (bolt_version: #{bolt_version})" do assert <<0xC1, 0x40, 0x1E, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCD>> == :erlang.iolist_to_binary(Encoder.encode(7.7, unquote(bolt_version))) end test "List (bolt_version: #{bolt_version})" do assert <<0x90>> == :erlang.iolist_to_binary(Encoder.encode([], unquote(bolt_version))) assert <<0x92, 0x2, 0x4>> == :erlang.iolist_to_binary(Encoder.encode([2, 4], unquote(bolt_version))) end test "Map (bolt_version: #{bolt_version})" do assert <<0xA1, 0x82, 0x6F, 0x6B, 0x5>> == :erlang.iolist_to_binary(Encoder.encode(%{ok: 5}, unquote(bolt_version))) end test "Struct (bolt_version: #{bolt_version})" do assert <<0xB3, 0x1, 0x81, 0x69, 0x82, 0x61, 0x6D, 0x86, 0x70, 0x61, 0x72, 0x61, 0x6D, 0x73>> == :erlang.iolist_to_binary(Encoder.encode({0x01, ["i", "am", "params"]}, unquote(bolt_version))) end test "raises error when trying to encode with unknown signature (bolt_version: #{ bolt_version })" do assert_raise Bolt.Sips.Internals.PackStreamError, ~r/^unable to encode/i, fn -> Encoder.encode({128, []}, unquote(bolt_version)) end assert_raise Bolt.Sips.Internals.PackStreamError, ~r/^unable to encode/i, fn -> Encoder.encode({-1, []}, unquote(bolt_version)) end assert_raise Bolt.Sips.Internals.PackStreamError, ~r/^unable to encode/i, fn -> Encoder.encode({"a", []}, unquote(bolt_version)) end end test "unkown type (bolt_version: #{bolt_version})" do assert_raise Bolt.Sips.Internals.PackStreamError, fn -> Encoder.encode({:error, "unencodable"}, unquote(bolt_version)) end end end) end describe "Encode types for bolt >= 2" do BoltVersionHelper.available_versions() |> Enum.filter(&(&1 >= 2)) |> Enum.each(fn bolt_version -> test "Local time (bolt_version: #{bolt_version})" do assert <<0xB1, 0x74, _::binary>> = :erlang.iolist_to_binary(Encoder.encode(~T[14:45:53.34], unquote(bolt_version))) end test "Time with TZ Offset (bolt_version: #{bolt_version})" do assert <<0xB2, 0x54, _::binary>> = :erlang.iolist_to_binary(Encoder.encode( Types.TimeWithTZOffset.create(~T[12:45:30.250000], 3600), unquote(bolt_version) )) end test "Date (bolt_version: #{bolt_version})" do assert <<0xB1, 0x44, _::binary>> = :erlang.iolist_to_binary(Encoder.encode(~D[2013-05-06], unquote(bolt_version))) end test "Local date time: NaiveDateTime (bolt_version: #{bolt_version})" do assert <<0xB2, 0x64, _::binary>> = :erlang.iolist_to_binary(Encoder.encode(~N[2018-04-05 12:34:00.543], unquote(bolt_version))) end test "Datetime with timezone offset (bolt_version: #{bolt_version})" do assert <<0xB3, 0x46, _::binary>> = :erlang.iolist_to_binary(Encoder.encode( Types.DateTimeWithTZOffset.create(~N[2016-05-24 13:26:08.543], 7200), unquote(bolt_version) )) end test "Datetime with timezone id (bolt_version: #{bolt_version})" do assert <<0xB3, 0x66, _::binary>> = :erlang.iolist_to_binary(Encoder.encode( TypesHelper.datetime_with_micro(~N[2016-05-24 13:26:08.543], "Europe/Berlin"), unquote(bolt_version) )) end test "Duration (bolt_version: #{bolt_version})" do duration = %Types.Duration{ years: 2, months: 3, weeks: 2, days: 23, hours: 8, minutes: 2, seconds: 4, nanoseconds: 3234 } assert <<0xB4, 0x45, _::binary>> = :erlang.iolist_to_binary(Encoder.encode(duration, unquote(bolt_version))) end test "Point 2D cartesian (bolt_version: #{bolt_version})" do assert <<0xB3, 0x58, _::binary>> = :erlang.iolist_to_binary(Encoder.encode(Types.Point.create(:cartesian, 40, 45), unquote(bolt_version))) end test "Point 2D geographic (bolt_version: #{bolt_version})" do assert <<0xB3, 0x58, _::binary>> = :erlang.iolist_to_binary(Encoder.encode(Types.Point.create(:wgs_84, 40, 45), unquote(bolt_version))) end test "Point 3D cartesian (bolt_version: #{bolt_version})" do assert <<0xB4, 0x59, _::binary>> = :erlang.iolist_to_binary(Encoder.encode( Types.Point.create(:cartesian, 40, 45, 150), unquote(bolt_version) )) end test "Point 3D geographic (bolt_version: #{bolt_version})" do assert <<0xB4, 0x59, _::binary>> = :erlang.iolist_to_binary(Encoder.encode(Types.Point.create(:wgs_84, 40, 45, 150), unquote(bolt_version))) end end) end end ================================================ FILE: test/bolt_sips/internals/pack_stream/encoder_v1_test.exs ================================================ defmodule Bolt.Sips.Internals.PackStream.EncoderV1Test do use ExUnit.Case, async: true alias Bolt.Sips.Internals.PackStream.EncoderV1 defmodule TestStruct do defstruct foo: "bar" end doctest Bolt.Sips.Internals.PackStream.EncoderV1 test "encodes null" do assert :erlang.iolist_to_binary(EncoderV1.encode_atom(nil, 1)) == <<0xC0>> end test "encodes boolean" do assert :erlang.iolist_to_binary(EncoderV1.encode_atom(true, 1)) == <<0xC3>> assert :erlang.iolist_to_binary(EncoderV1.encode_atom(false, 1)) == <<0xC2>> end test "encodes atom" do assert :erlang.iolist_to_binary(EncoderV1.encode_atom(:hello, 1)) == <<0x85, 0x68, 0x65, 0x6C, 0x6C, 0x6F>> end test "encodes string" do assert :erlang.iolist_to_binary(EncoderV1.encode_string("", 1)) == <<0x80>> assert :erlang.iolist_to_binary(EncoderV1.encode_string("Short", 1)) == <<0x85, 0x53, 0x68, 0x6F, 0x72, 0x74>> # 30 bytes due to umlauts long_8 = "This is a räther löng string" assert <<0xD0, 0x1E, _::binary-size(30)>> = :erlang.iolist_to_binary(EncoderV1.encode_string(long_8, 1)) long_16 = """ For encoded string containing fewer than 16 bytes, including empty strings, the marker byte should contain the high-order nibble `1000` followed by a low-order nibble containing the size. The encoded data then immediately follows the marker. For encoded string containing 16 bytes or more, the marker 0xD0, 0xD1 or 0xD2 should be used, depending on scale. This marker is followed by the size and the UTF-8 encoded data. """ assert <<0xD1, 0x01, 0xA5, _::binary-size(421)>> = :erlang.iolist_to_binary(EncoderV1.encode_string(long_16, 1)) long_32 = String.duplicate("a", 66_000) assert <<0xD2, 66_000::32, _::binary-size(66_000)>> = :erlang.iolist_to_binary(EncoderV1.encode_string(long_32, 1)) end test "encodes integer" do assert :erlang.iolist_to_binary(EncoderV1.encode_integer(0, 1)) == <<0x00>> assert :erlang.iolist_to_binary(EncoderV1.encode_integer(42, 1)) == <<0x2A>> assert :erlang.iolist_to_binary(EncoderV1.encode_integer(-42, 1)) == <<0xC8, 0xD6>> assert :erlang.iolist_to_binary(EncoderV1.encode_integer(420, 1)) == <<0xC9, 0x01, 0xA4>> assert :erlang.iolist_to_binary(EncoderV1.encode_integer(33_000, 1)) == <<0xCA, 0x00, 0x00, 0x80, 0xE8>> assert :erlang.iolist_to_binary(EncoderV1.encode_integer(2_150_000_000, 1)) == <<0xCB, 0x00, 0x00, 0x00, 0x00, 0x80, 0x26, 0x65, 0x80>> end test "encodes float" do assert :erlang.iolist_to_binary(EncoderV1.encode_float(+1.1, 1)) == <<0xC1, 0x3F, 0xF1, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9A>> assert :erlang.iolist_to_binary(EncoderV1.encode_float(-1.1, 1)) == <<0xC1, 0xBF, 0xF1, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9A>> end test "encodes list" do assert :erlang.iolist_to_binary(EncoderV1.encode_list([], 1)) == <<0x90>> list_8 = Stream.repeatedly(fn -> "a" end) |> Enum.take(16) assert <<0xD4, 16::8, _::binary-size(32)>> = :erlang.iolist_to_binary(EncoderV1.encode_list(list_8, 1)) list_16 = Stream.repeatedly(fn -> "a" end) |> Enum.take(256) assert <<0xD5, 256::16, _::binary-size(512)>> = :erlang.iolist_to_binary(EncoderV1.encode_list(list_16, 1)) list_32 = Stream.repeatedly(fn -> "a" end) |> Enum.take(66_000) assert <<0xD6, 66_000::32, _::binary-size(132_000)>> = :erlang.iolist_to_binary(EncoderV1.encode_list(list_32, 1)) end test "encodes map" do assert :erlang.iolist_to_binary(EncoderV1.encode_map(%{}, 1)) == <<0xA0>> map_8 = 1..16 |> Enum.map(&{&1, "a"}) |> Map.new() assert <<0xD8, 16::8>> <> _rest = :erlang.iolist_to_binary(EncoderV1.encode_map(map_8, 1)) map_16 = 1..256 |> Enum.map(&{&1, "a"}) |> Map.new() assert <<0xD9, 256::16>> <> _rest = :erlang.iolist_to_binary(EncoderV1.encode_map(map_16, 1)) map_32 = 1..66_000 |> Enum.map(&{&1, "a"}) |> Map.new() assert <<0xDA, 66_000::32>> <> _rest = :erlang.iolist_to_binary(EncoderV1.encode_map(map_32, 1)) end test "encodes a struct" do assert <<0xB2, 0x1, 0x85, 0x66, 0x69, 0x72, 0x73, 0x74, 0x86, 0x73, 0x65, 0x63, 0x6F, 0x6E, 0x64>> == :erlang.iolist_to_binary(EncoderV1.encode_struct({0x01, ["first", "second"]}, 1)) assert <<0xDC, 0x6F, _::binary>> = :erlang.iolist_to_binary(EncoderV1.encode_struct({0x01, Enum.into(1..111, [])}, 1)) assert <<0xDD, 0x1, 0x4D, _::binary>> = :erlang.iolist_to_binary(EncoderV1.encode_struct({0x01, Enum.into(1..333, [])}, 1)) # Test for a fixed bug assert <<0xB1, 0x1, 0xA1, 0x83, 0x66, 0x6F, 0x6F, 0x83, 0x62, 0x61, 0x72>> == :erlang.iolist_to_binary(EncoderV1.encode_struct({0x01, [%TestStruct{}]}, 1)) end end ================================================ FILE: test/bolt_sips/internals/pack_stream/encoder_v2_test.exs ================================================ defmodule Bolt.Sips.Internals.PackStream.EncoderV2Test do use ExUnit.Case, async: true alias Bolt.Sips.Internals.PackStream.EncoderV2 alias Bolt.Sips.Types.{TimeWithTZOffset, DateTimeWithTZOffset, Duration, Point} alias Bolt.Sips.TypesHelper doctest Bolt.Sips.Internals.PackStream.EncoderV2 describe "Encode temporal types:" do test "time without timezone" do assert <<0xB1, 0x74, 0xCB, 0x0, 0x0, 0x39, 0x8E, 0xD6, 0xF1, 0xF7, 0x68>> == :erlang.iolist_to_binary(EncoderV2.encode_local_time(~T[17:34:45.654321], 2)) end test "time with timezone" do ttz = TimeWithTZOffset.create(~T[12:45:30.250000], 3600) assert <<0xB2, 0x54, 0xCB, 0x0, 0x0, 0x29, 0xC5, 0xF8, 0x3C, 0x56, 0x80, 0xC9, 0xE, 0x10>> == :erlang.iolist_to_binary(EncoderV2.encode_time_with_tz(ttz, 2)) end test "date post 1970-01-01" do assert <<0xB1, 0x44, 0xC9, 0x45, 0x4D>> == :erlang.iolist_to_binary(EncoderV2.encode_date(~D[2018-07-29], 2)) end test "date pre 1970-01-01" do assert <<0xB1, 0x44, 0xC9, 0xB6, 0xA0>> == :erlang.iolist_to_binary(EncoderV2.encode_date(~D[1918-07-29], 2)) end test "local datetime" do assert <<0xB2, 0x64, 0xCA, 0x5A, 0xC6, 0x17, 0xB8, 0xCA, 0x27, 0x0, 0x25, 0x68>> :erlang.iolist_to_binary(EncoderV2.encode_local_datetime(~N[2018-04-05 12:34:00.654321], 2)) end test "datetime with timezone id" do dt = TypesHelper.datetime_with_micro(~N[2016-05-24 13:26:08.654321], "Europe/Berlin") assert <<0xB3, 0x66, 0xCA, 0x57, 0x44, 0x56, 0x70, 0xCA, 0x27, 0x0, 0x25, 0x68, 0x8D, 0x45, 0x75, 0x72, 0x6F, 0x70, 0x65, 0x2F, 0x42, 0x65, 0x72, 0x6C, 0x69, 0x6E>> == :erlang.iolist_to_binary(EncoderV2.encode_datetime_with_tz_id(dt, 2)) end test "datetime with timezone offset" do dt = DateTimeWithTZOffset.create(~N[2016-05-24 13:26:08.654321], 7200) assert <<0xB3, 0x46, 0xCA, 0x57, 0x44, 0x56, 0x70, 0xCA, 0x27, 0x0, 0x25, 0x68, 0xC9, 0x1C, 0x20>> == :erlang.iolist_to_binary(EncoderV2.encode_datetime_with_tz_offset(dt, 2)) end test "duration with all values" do duration = %Duration{ years: 1, months: 3, weeks: 2, days: 20, hours: 2, minutes: 32, seconds: 54, nanoseconds: 5550 } assert <<0xB4, 0x45, 0xF, 0x22, 0xC9, 0x23, 0xD6, 0xC9, 0x15, 0xAE>> == :erlang.iolist_to_binary(EncoderV2.encode_duration(duration, 2)) end end describe "Encode spatial types:" do test "cartesian point 2D" do assert <<0xB3, 0x58, 0xC9, 0x1C, 0x23, 0xC1, 0x40, 0x44, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC1, 0x40, 0x46, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0>> = :erlang.iolist_to_binary( EncoderV2.encode_point(Point.create(:cartesian, 40, 45), 2) ) end test "geographic point 2D" do assert <<0xB3, 0x58, 0xC9, 0x10, 0xE6, 0xC1, 0x40, 0x44, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC1, 0x40, 0x46, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0>> = :erlang.iolist_to_binary(EncoderV2.encode_point(Point.create(:wgs_84, 40, 45), 2)) end test "cartesian point 3D" do assert <<0xB4, 0x59, 0xC9, 0x23, 0xC5, 0xC1, 0x40, 0x44, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC1, 0x40, 0x46, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC1, 0x40, 0x62, 0xC0, 0x0, 0x0, 0x0, 0x0, 0x0>> = :erlang.iolist_to_binary( EncoderV2.encode_point(Point.create(:cartesian, 40, 45, 150), 2) ) end test "geographic point 3D" do assert <<0xB4, 0x59, 0xC9, 0x13, 0x73, 0xC1, 0x40, 0x44, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC1, 0x40, 0x46, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC1, 0x40, 0x62, 0xC0, 0x0, 0x0, 0x0, 0x0, 0x0>> = :erlang.iolist_to_binary( EncoderV2.encode_point(Point.create(:wgs_84, 40, 45, 150), 2) ) end end end ================================================ FILE: test/bolt_sips/internals/pack_stream/message/decoder_test.exs ================================================ defmodule Bolt.Sips.Internals.PackStream.Message.DecoderTest do use ExUnit.Case, async: true alias Bolt.Sips.Internals.PackStream.Message.Decoder alias Bolt.Sips.Internals.BoltVersionHelper describe "Decode common messages" do Enum.each(BoltVersionHelper.available_versions(), fn bolt_version -> test "SUCCESS (bolt_version: #{bolt_version})" do success_hex = <<0xB1, 0x70, 0xA1, 0x86, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x8B, 0x4E, 0x65, 0x6F, 0x34, 0x6A, 0x2F, 0x33, 0x2E, 0x34, 0x2E, 0x31>> assert {:success, %{"server" => "Neo4j/3.4.1"}} == Decoder.decode(success_hex, unquote(bolt_version)) end test "FAILURE (bolt_version: #{bolt_version})" do failure_hex = <<0xB1, 0x7F, 0xA2, 0x84, 0x63, 0x6F, 0x64, 0x65, 0xD0, 0x25, 0x4E, 0x65, 0x6F, 0x2E, 0x43, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x2E, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2E, 0x55, 0x6E, 0x61, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x7A, 0x65, 0x64, 0x87, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0xD0, 0x39, 0x54, 0x68, 0x65, 0x20, 0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x20, 0x69, 0x73, 0x20, 0x75, 0x6E, 0x61, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x7A, 0x65, 0x64, 0x20, 0x64, 0x75, 0x65, 0x20, 0x74, 0x6F, 0x20, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6E, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x66, 0x61, 0x69, 0x6C, 0x75, 0x72, 0x65, 0x2E>> failure = {:failure, %{ "code" => "Neo.ClientError.Security.Unauthorized", "message" => "The client is unauthorized due to authentication failure." }} assert failure == Decoder.decode(failure_hex, unquote(bolt_version)) end test "RECORD (bolt_version: #{bolt_version})" do assert {:record, [1]} == Decoder.decode(<<0xB1, 0x71, 0x91, 0x1>>, unquote(bolt_version)) end test "IGNORED (bolt_version: #{bolt_version})" do assert {:ignored, _} = Decoder.decode(<<0xB0, 0x7E>>, unquote(bolt_version)) end end) end end ================================================ FILE: test/bolt_sips/internals/pack_stream/message/encoder_test.exs ================================================ defmodule Bolt.Sips.Internals.PackStream.Message.EncoderTest do use ExUnit.Case, async: true doctest Bolt.Sips.Internals.PackStream.Message.Encoder alias Bolt.Sips.Internals.PackStream.Message.Encoder alias Bolt.Sips.Metadata alias Bolt.Sips.Internals.BoltVersionHelper defmodule TestUser do defstruct name: "", bolt_sips: true end describe "Encode common messages" do Enum.each(BoltVersionHelper.available_versions(), fn bolt_version -> test "DISCARD_ALL (bolt_version: #{bolt_version})" do assert <<0x0, 0x2, 0xB0, 0x2F, 0x0, 0x0>> == :erlang.iolist_to_binary( Encoder.encode({:discard_all, []}, unquote(bolt_version)) ) end test "PULL_ALL (bolt_version: #{bolt_version})" do assert <<0x0, 0x2, 0xB0, 0x3F, 0x0, 0x0>> == :erlang.iolist_to_binary(Encoder.encode({:pull_all, []}, unquote(bolt_version))) end test "RESET (bolt_version: #{bolt_version})" do assert <<0x0, 0x2, 0xB0, 0xF, 0x0, 0x0>> == :erlang.iolist_to_binary(Encoder.encode({:reset, []}, unquote(bolt_version))) end end) end @doc """ INIT is not valid in bolt >= 3 RUN has one more params (metadata) in bolt >=3 """ describe "Encode message available only in Bolt <= 2" do BoltVersionHelper.available_versions() |> Enum.filter(&(&1 <= 2)) |> Enum.each(fn bolt_version -> test "INIT without auth (bolt_version: #{bolt_version})" do assert <<0x0, _, 0xB2, 0x1, _::binary>> = :erlang.iolist_to_binary(Encoder.encode({:init, []}, unquote(bolt_version))) end test "INIT wit auth (bolt_version: #{bolt_version})" do assert <<0x0, _, 0xB2, 0x1, _::binary>> = :erlang.iolist_to_binary( Encoder.encode({:init, [{"neo4j", "test"}]}, unquote(bolt_version)) ) end test "ACK_FAILURE (bolt_version: #{bolt_version})" do assert <<0x0, 0x2, 0xB0, 0xE, 0x0, 0x0>> == :erlang.iolist_to_binary( Encoder.encode({:ack_failure, []}, unquote(bolt_version)) ) end test "RUN without params (bolt_version: #{bolt_version})" do assert <<0x0, 0x13, 0xB2, 0x10, 0x8F, 0x52, 0x45, 0x54, 0x55, 0x52, 0x4E, 0x20, 0x31, 0x20, 0x41, 0x53, 0x20, 0x6E, 0x75, 0x6D, 0xA0, 0x0, 0x0>> == :erlang.iolist_to_binary( Encoder.encode({:run, ["RETURN 1 AS num"]}, unquote(bolt_version)) ) end test "RUN with params (bolt_version: #{bolt_version})" do assert <<0x0, 0x1C, 0xB2, 0x10, 0xD0, 0x12, 0x52, 0x45, 0x54, 0x55, 0x52, 0x4E, 0x20, 0x24, 0x6E, 0x75, 0x6D, 0x20, 0x41, 0x53, 0x20, 0x6E, 0x75, 0x6D, 0xA1, 0x83, 0x6E, 0x75, 0x6D, 0x5, 0x0, 0x0>> == :erlang.iolist_to_binary( Encoder.encode( {:run, ["RETURN $num AS num", %{num: 5}]}, unquote(bolt_version) ) ) end test "Bug fix: encoding struct fails (bolt_version: #{bolt_version})" do query = "CREATE (n:User $props)" params = %{props: %TestUser{bolt_sips: true, name: "Strut"}} assert <<0x0, 0x38, 0xB2, 0x10, 0xD0, 0x16, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x20, 0x28, 0x6E, 0x3A, 0x55, 0x73, 0x65, 0x72, 0x20, 0x24, 0x70, 0x72, 0x6F, 0x70, 0x73, 0x29, 0xA1, 0x85, 0x70, 0x72, 0x6F, 0x70, 0x73, 0xA2, 0x89, 0x62, 0x6F, 0x6C, 0x74, 0x5F, 0x73, 0x69, 0x70, 0x73, 0xC3, 0x84, _::binary>> = :erlang.iolist_to_binary( Encoder.encode({:run, [query, params]}, unquote(bolt_version)) ) end end) end describe "Encode message available in Bolt >= 3" do BoltVersionHelper.available_versions() |> Enum.filter(&(&1 >= 3)) |> Enum.each(fn bolt_version -> nil test "HELLO without params (bolt_version: #{bolt_version})" do assert <<0x0, _, 0xB1, 0x1, _::binary>> = :erlang.iolist_to_binary(Encoder.encode({:hello, []}, unquote(bolt_version))) end test "HELLO with params (bolt_version: #{bolt_version})" do assert <<0x0, _, 0xB1, 0x1, _::binary>> = :erlang.iolist_to_binary( Encoder.encode({:hello, [{"neo4j", "test"}]}, unquote(bolt_version)) ) end test "Encode GOODBYE (bolt_version: #{bolt_version})" do assert assert <<0x0, 0x2, 0xB0, 0x02, 0x0, 0x0>> == :erlang.iolist_to_binary( Encoder.encode({:goodbye, []}, unquote(bolt_version)) ) end test "BEGIN without params (bolt_version: #{bolt_version})" do assert <<0x0, 0x3, 0xB1, 0x11, 0xA0, 0x0, 0x0>> == :erlang.iolist_to_binary(Encoder.encode({:begin, []}, unquote(bolt_version))) end test "BEGIN with params (bolt_version: #{bolt_version})" do {:ok, metadata} = Metadata.new(%{tx_timeout: 15000}) assert <<0x0, 0x11, 0xB1, 0x11, 0xA1, 0x8A, 0x74, 0x78, 0x5F, 0x74, 0x69, 0x6D, 0x65, 0x6F, 0x75, 0x74, 0xC9, 0x3A, 0x98, 0x0, 0x0>> == :erlang.iolist_to_binary( Encoder.encode({:begin, [metadata]}, unquote(bolt_version)) ) end test "Encode COMMIT (bolt_version: #{bolt_version})" do assert <<0x0, 0x2, 0xB0, 0x12, 0x0, 0x0>> == :erlang.iolist_to_binary(Encoder.encode({:commit, []}, unquote(bolt_version))) end test "Encode ROLLBACK (bolt_version: #{bolt_version})" do assert <<0x0, 0x2, 0xB0, 0x13, 0x0, 0x0>> == :erlang.iolist_to_binary(Encoder.encode({:rollback, []}, unquote(bolt_version))) end test "RUN without params nor metadata (bolt_version: #{bolt_version})" do assert <<0x0, 0x16, 0xB3, 0x10, 0xD0, 0x10, 0x52, 0x45, 0x54, 0x55, 0x52, 0x4E, 0x20, 0x31, 0x36, 0x20, 0x41, 0x53, 0x20, 0x6E, 0x75, 0x6D, 0xA0, 0xA0, 0x0, 0x0>> == :erlang.iolist_to_binary( Encoder.encode({:run, ["RETURN 16 AS num"]}, unquote(bolt_version)) ) end test "RUN without params but with metadata (bolt_version: #{bolt_version})" do {:ok, metadata} = Metadata.new(%{tx_timeout: 15000}) assert <<0x0, 0x24, 0xB3, 0x10, 0xD0, 0x10, 0x52, 0x45, 0x54, 0x55, 0x52, 0x4E, 0x20, 0x31, 0x36, 0x20, 0x41, 0x53, 0x20, 0x6E, 0x75, 0x6D, 0xA0, 0xA1, 0x8A, 0x74, 0x78, 0x5F, 0x74, 0x69, 0x6D, 0x65, 0x6F, 0x75, 0x74, 0xC9, 0x3A, 0x98, 0x0, 0x0>> == :erlang.iolist_to_binary( Encoder.encode( {:run, ["RETURN 16 AS num", %{}, metadata]}, unquote(bolt_version) ) ) end test "RUN with params but without metadata (bolt_version: #{bolt_version})" do assert <<0x0, 0x1D, 0xB3, 0x10, 0xD0, 0x12, 0x52, 0x45, 0x54, 0x55, 0x52, 0x4E, 0x20, 0x24, 0x6E, 0x75, 0x6D, 0x20, 0x41, 0x53, 0x20, 0x6E, 0x75, 0x6D, 0xA1, 0x83, 0x6E, 0x75, 0x6D, 0x10, 0xA0, 0x0, 0x0>> == :erlang.iolist_to_binary( Encoder.encode( {:run, ["RETURN $num AS num", %{num: 16}]}, unquote(bolt_version) ) ) end test "RUN with params and metadata (bolt_version: #{bolt_version})" do {:ok, metadata} = Metadata.new(%{tx_timeout: 15000}) assert <<0x0, 0x2B, 0xB3, 0x10, 0xD0, 0x12, 0x52, 0x45, 0x54, 0x55, 0x52, 0x4E, 0x20, 0x24, 0x6E, 0x75, 0x6D, 0x20, 0x41, 0x53, 0x20, 0x6E, 0x75, 0x6D, 0xA1, 0x83, 0x6E, 0x75, 0x6D, 0x10, 0xA1, 0x8A, 0x74, 0x78, 0x5F, 0x74, 0x69, 0x6D, 0x65, 0x6F, 0x75, 0x74, 0xC9, 0x3A, 0x98, 0x0, 0x0>> == :erlang.iolist_to_binary( Encoder.encode( {:run, ["RETURN $num AS num", %{num: 16}, metadata]}, unquote(bolt_version) ) ) end test "Bug fix: encoding struct fails (bolt_version: #{bolt_version})" do query = "CREATE (n:User $props)" params = %{props: %TestUser{bolt_sips: true, name: "Strut"}} assert <<0x0, 0x39, 0xB3, 0x10, 0xD0, 0x16, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x20, 0x28, 0x6E, 0x3A, 0x55, 0x73, 0x65, 0x72, 0x20, 0x24, 0x70, 0x72, 0x6F, 0x70, 0x73, 0x29, 0xA1, 0x85, 0x70, 0x72, 0x6F, 0x70, 0x73, 0xA2, 0x89, 0x62, 0x6F, 0x6C, 0x74, 0x5F, 0x73, 0x69, 0x70, 0x73, 0xC3, 0x84, _::binary>> = :erlang.iolist_to_binary( Encoder.encode({:run, [query, params]}, unquote(bolt_version)) ) end end) end end ================================================ FILE: test/bolt_sips/internals/pack_stream/message/encoder_v1_test.exs ================================================ defmodule Bolt.Sips.Internals.PackStream.Message.EncoderV1Test do use ExUnit.Case, async: true doctest Bolt.Sips.Internals.PackStream.Message.EncoderV1 alias Bolt.Sips.Internals.PackStream.Message.EncoderV1 test "Encode ACK_FAILURE" do assert <<0x0, 0x2, 0xB0, 0xE, 0x0, 0x0>> == :erlang.iolist_to_binary(EncoderV1.encode({:ack_failure, []}, 1)) end test "Encode DISCARD_ALL" do assert <<0x0, 0x2, 0xB0, 0x2F, 0x0, 0x0>> == :erlang.iolist_to_binary(EncoderV1.encode({:discard_all, []}, 1)) end describe "Encode INIT:" do test "without params" do assert <<0x0, _, 0xB2, 0x1, _::binary>> = :erlang.iolist_to_binary(EncoderV1.encode({:init, []}, 1)) end test "with params" do assert <<0x0, _, 0xB2, 0x1, _::binary>> = :erlang.iolist_to_binary(EncoderV1.encode({:init, [{"neo4j", "test"}]}, 1)) end end test "Encode PULL_ALL" do assert <<0x0, 0x2, 0xB0, 0x3F, 0x0, 0x0>> == :erlang.iolist_to_binary(EncoderV1.encode({:pull_all, []}, 1)) end test "Encode RESET" do assert <<0x0, 0x2, 0xB0, 0xF, 0x0, 0x0>> == :erlang.iolist_to_binary(EncoderV1.encode({:reset, []}, 1)) end describe "Encode RUN:" do test "without params" do assert <<0x0, 0x13, 0xB2, 0x10, 0x8F, 0x52, 0x45, 0x54, 0x55, 0x52, 0x4E, 0x20, 0x35, 0x20, 0x41, 0x53, 0x20, 0x6E, 0x75, 0x6D, 0xA0, 0x0, 0x0>> == :erlang.iolist_to_binary(EncoderV1.encode({:run, ["RETURN 5 AS num"]}, 1)) end test "with params" do assert <<0x0, 0x21, 0xB2, 0x10, 0xD0, 0x12, 0x52, 0x45, 0x54, 0x55, 0x52, 0x4E, 0x20, 0x24, 0x73, 0x74, 0x72, 0x20, 0x41, 0x53, 0x20, 0x73, 0x74, 0x72, 0xA1, 0x83, 0x73, 0x74, 0x72, 0x85, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x0, 0x0>> == :erlang.iolist_to_binary( EncoderV1.encode({:run, ["RETURN $str AS str", %{str: "hello"}]}, 1) ) end end test "fails for unknown message type" do assert {:error, :not_implemented} == EncoderV1.encode({:invalid, []}, 1) end end ================================================ FILE: test/bolt_sips/internals/pack_stream/message/encoder_v3_test.exs ================================================ defmodule Bolt.Sips.Internals.PackStream.Message.EncoderV3Test do use ExUnit.Case, async: true doctest Bolt.Sips.Internals.PackStream.Message.EncoderV3 alias Bolt.Sips.Internals.PackStream.Message.EncoderV3 alias Bolt.Sips.Metadata describe "Encode BEGIN" do test "without params" do assert <<0x0, 0x3, 0xB1, 0x11, 0xA0, 0x0, 0x0>> == :erlang.iolist_to_binary(EncoderV3.encode({:begin, []}, 3)) end test "with empty params" do assert <<0x0, 0x3, 0xB1, 0x11, 0xA0, 0x0, 0x0>> == :erlang.iolist_to_binary(EncoderV3.encode({:begin, [%{}]}, 3)) end test "with params" do {:ok, metadata} = Metadata.new(%{tx_timeout: 5000}) assert <<0x0, 0x11, 0xB1, 0x11, 0xA1, 0x8A, 0x74, 0x78, 0x5F, 0x74, 0x69, 0x6D, 0x65, 0x6F, 0x75, 0x74, 0xC9, 0x13, 0x88, 0x0, 0x0>> == :erlang.iolist_to_binary(EncoderV3.encode({:begin, [metadata]}, 3)) end test "fails with non-metadata params" do assert {:error, _} = EncoderV3.encode({:begin, [%{tx_timeout: 5000}]}, 3) end end test "Encode COMMIT" do assert <<0x0, 0x2, 0xB0, 0x12, 0x0, 0x0>> == :erlang.iolist_to_binary(EncoderV3.encode({:commit, []}, 3)) end test "Encode GOODBYE" do assert assert <<0x0, 0x2, 0xB0, 0x02, 0x0, 0x0>> == :erlang.iolist_to_binary(EncoderV3.encode({:goodbye, []}, 3)) end describe "Encode HELLO" do test "without params" do assert <<0x0, _, 0xB1, 0x1, _::binary>> = :erlang.iolist_to_binary(EncoderV3.encode({:hello, []}, 3)) end test "with params" do assert <<0x0, _, 0xB1, 0x1, _::binary>> = :erlang.iolist_to_binary(EncoderV3.encode({:hello, [{"neo4j", "test"}]}, 3)) end end test "Encode ROLLBACK" do assert <<0x0, 0x2, 0xB0, 0x13, 0x0, 0x0>> == :erlang.iolist_to_binary(EncoderV3.encode({:rollback, []}, 3)) end describe "Encode RUN" do test "without params nor metadata" do assert <<0x0, 0x14, 0xB3, 0x10, 0x8F, 0x52, 0x45, 0x54, 0x55, 0x52, 0x4E, 0x20, 0x31, 0x20, 0x41, 0x53, 0x20, 0x6E, 0x75, 0x6D, 0xA0, 0xA0, 0x0, 0x0>> == :erlang.iolist_to_binary(EncoderV3.encode({:run, ["RETURN 1 AS num"]}, 3)) end test "without params but with metadata" do {:ok, metadata} = Metadata.new(%{tx_timeout: 5000}) assert <<0x0, 0x22, 0xB3, 0x10, 0x8F, 0x52, 0x45, 0x54, 0x55, 0x52, 0x4E, 0x20, 0x31, 0x20, 0x41, 0x53, 0x20, 0x6E, 0x75, 0x6D, 0xA0, 0xA1, 0x8A, 0x74, 0x78, 0x5F, 0x74, 0x69, 0x6D, 0x65, 0x6F, 0x75, 0x74, 0xC9, 0x13, 0x88, 0x0, 0x0>> == :erlang.iolist_to_binary( EncoderV3.encode({:run, ["RETURN 1 AS num", %{}, metadata]}, 3) ) end test "with params but without metadata" do assert <<0x0, 0x1D, 0xB3, 0x10, 0xD0, 0x12, 0x52, 0x45, 0x54, 0x55, 0x52, 0x4E, 0x20, 0x24, 0x6E, 0x75, 0x6D, 0x20, 0x41, 0x53, 0x20, 0x6E, 0x75, 0x6D, 0xA1, 0x83, 0x6E, 0x75, 0x6D, 0x5, 0xA0, 0x0, 0x0>> == :erlang.iolist_to_binary( EncoderV3.encode({:run, ["RETURN $num AS num", %{num: 5}]}, 3) ) end test "with params and metadata" do {:ok, metadata} = Metadata.new(%{tx_timeout: 5000}) assert <<0x0, 0x2B, 0xB3, 0x10, 0xD0, 0x12, 0x52, 0x45, 0x54, 0x55, 0x52, 0x4E, 0x20, 0x24, 0x6E, 0x75, 0x6D, 0x20, 0x41, 0x53, 0x20, 0x6E, 0x75, 0x6D, 0xA1, 0x83, 0x6E, 0x75, 0x6D, 0x5, 0xA1, 0x8A, 0x74, 0x78, 0x5F, 0x74, 0x69, 0x6D, 0x65, 0x6F, 0x75, 0x74, 0xC9, 0x13, 0x88, 0x0, 0x0>> == :erlang.iolist_to_binary( EncoderV3.encode({:run, ["RETURN $num AS num", %{num: 5}, metadata]}, 3) ) end end end ================================================ FILE: test/bolt_sips/internals/pack_stream/message_test.exs ================================================ defmodule Bolt.Sips.Internals.PackStream.MessageTest do use ExUnit.Case, async: true alias Bolt.Sips.Internals.PackStream.Message alias Bolt.Sips.Metadata alias Bolt.Sips.Internals.BoltVersionHelper describe "Encode all-bolt-version-compliant message:" do Enum.each(BoltVersionHelper.available_versions(), fn bolt_version -> test "DISCARD_ALL (bolt_version: #{bolt_version})" do assert <<_, _, _, 0x2F, _::binary>> = :erlang.iolist_to_binary( Message.encode({:discard_all, []}, unquote(bolt_version)) ) end test "PULL_ALL (bolt_version: #{bolt_version})" do assert <<_, _, _, 0x3F, _::binary>> = :erlang.iolist_to_binary(Message.encode({:pull_all, []}, unquote(bolt_version))) end test "RESET (bolt_version: #{bolt_version})" do assert <<_, _, _, 0x0F, _::binary>> = :erlang.iolist_to_binary(Message.encode({:reset, []}, unquote(bolt_version))) end test "RUN without params (bolt_version: #{bolt_version})" do assert <<_, _, _, 0x10, _::binary>> = :erlang.iolist_to_binary( Message.encode({:run, ["RETURN 1 AS num"]}, unquote(bolt_version)) ) end test "RUN with params (bolt_version: #{bolt_version})" do assert <<_, _, _, 0x10, _::binary>> = :erlang.iolist_to_binary( Message.encode( {:run, ["RETURN $num AS num", %{num: 5}]}, unquote(bolt_version) ) ) end end) end describe "Encode Bolt <= 2 only message:" do BoltVersionHelper.available_versions() |> Enum.filter(&(&1 <= 2)) |> Enum.each(fn bolt_version -> test "ACK_FAILURE (bolt_version: #{bolt_version})" do assert <<_, _, _, 0x0E, _::binary>> = :erlang.iolist_to_binary( Message.encode({:ack_failure, []}, unquote(bolt_version)) ) end test "INIT without auth (bolt_version: #{bolt_version})" do assert <<_, _, _, 0x01, _::binary>> = :erlang.iolist_to_binary(Message.encode({:init, []}, unquote(bolt_version))) end test "INIT with auth (bolt_version: #{bolt_version})" do assert <<_, _, _, 0x01, _::binary>> = :erlang.iolist_to_binary( Message.encode({:init, [{"neo4j", "password"}]}, unquote(bolt_version)) ) end end) end describe "Encode Bolt >= 3 only message" do BoltVersionHelper.available_versions() |> Enum.filter(&(&1 >= 3)) |> Enum.each(fn bolt_version -> test "HELLO without auth (bolt_version: #{bolt_version})" do assert <<_, _, _, 0x01, _::binary>> = :erlang.iolist_to_binary(Message.encode({:hello, []}, unquote(bolt_version))) end test "HELLO with auth (bolt_version: #{bolt_version})" do assert <<_, _, _, 0x01, _::binary>> = :erlang.iolist_to_binary( Message.encode({:hello, [{"neo4j", "password"}]}, unquote(bolt_version)) ) end test "GOODBYE (bolt_version: #{bolt_version})" do assert assert <<_, _, _, 0x02, _::binary>> = :erlang.iolist_to_binary( Message.encode({:goodbye, []}, unquote(bolt_version)) ) end test "BEGIN without params (bolt_version: #{bolt_version})" do assert <<_, _, _, 0x11, _::binary>> = :erlang.iolist_to_binary(Message.encode({:begin, []}, unquote(bolt_version))) end test "BEGIN with params (bolt_version: #{bolt_version})" do {:ok, metadata} = Metadata.new(%{tx_timeout: 15000}) assert <<_, _, _, 0x11, _::binary>> = :erlang.iolist_to_binary( Message.encode({:begin, [metadata]}, unquote(bolt_version)) ) end test "COMMIT (bolt_version: #{bolt_version})" do assert <<_, _, _, 0x12, _::binary>> = :erlang.iolist_to_binary(Message.encode({:commit, []}, unquote(bolt_version))) end test "ROLLBACK (bolt_version: #{bolt_version})" do assert <<_, _, _, 0x13, _::binary>> = :erlang.iolist_to_binary(Message.encode({:rollback, []}, unquote(bolt_version))) end test "RUN without params but with metadata (bolt_version: #{bolt_version})" do {:ok, metadata} = Metadata.new(%{tx_timeout: 15000}) assert <<_, _, _, 0x10, _::binary>> = :erlang.iolist_to_binary( Message.encode( {:run, ["RETURN 16 AS num", %{}, metadata]}, unquote(bolt_version) ) ) end test "RUN with params and metadata (bolt_version: #{bolt_version})" do {:ok, metadata} = Metadata.new(%{tx_timeout: 15000}) assert <<_, _, _, 0x10, _::binary>> = :erlang.iolist_to_binary( Message.encode( {:run, ["RETURN $num AS num", %{num: 16}, metadata]}, unquote(bolt_version) ) ) end end) end describe "Decode all-bolt-version-compliant message:" do Enum.each(BoltVersionHelper.available_versions(), fn bolt_version -> test "SUCESS (bolt_version: #{bolt_version})" do success_hex = <<0xB1, 0x70, 0xA1, 0x86, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x8B, 0x4E, 0x65, 0x6F, 0x34, 0x6A, 0x2F, 0x33, 0x2E, 0x34, 0x2E, 0x31>> assert {:success, _} = Message.decode(success_hex, unquote(bolt_version)) end test "FAILURE (bolt_version: #{bolt_version})" do failure_hex = <<0xB1, 0x7F, 0xA2, 0x84, 0x63, 0x6F, 0x64, 0x65, 0xD0, 0x25, 0x4E, 0x65, 0x6F, 0x2E, 0x43, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x2E, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2E, 0x55, 0x6E, 0x61, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x7A, 0x65, 0x64, 0x87, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0xD0, 0x39, 0x54, 0x68, 0x65, 0x20, 0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x20, 0x69, 0x73, 0x20, 0x75, 0x6E, 0x61, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x7A, 0x65, 0x64, 0x20, 0x64, 0x75, 0x65, 0x20, 0x74, 0x6F, 0x20, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6E, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x66, 0x61, 0x69, 0x6C, 0x75, 0x72, 0x65, 0x2E>> assert {:failure, _} = Message.decode(failure_hex, unquote(bolt_version)) end test "RECORD (bolt_version: #{bolt_version})" do assert {:record, _} = Message.decode(<<0xB1, 0x71, 0x91, 0x1>>, unquote(bolt_version)) end test "IGNORED (bolt_version: #{bolt_version})" do assert {:ignored, _} = Message.decode(<<0xB0, 0x7E>>, unquote(bolt_version)) end end) end end ================================================ FILE: test/bolt_sips/metadata_test.exs ================================================ defmodule Bolt.Sips.MetadataTest do use ExUnit.Case, async: true alias Bolt.Sips.Metadata @valid_metadata %{ bookmarks: ["neo4j:bookmark:v1:tx1111"], tx_timeout: 5000, metadata: %{ desc: "Not lost in transaction" } } describe "Create new metadata from map:" do test "with compelete data" do expected = %Metadata{ bookmarks: ["neo4j:bookmark:v1:tx1111"], tx_timeout: 5000, metadata: %{ desc: "Not lost in transaction" } } assert {:ok, result} = Metadata.new(@valid_metadata) assert expected == result end test "return error with invalid keys" do data = Map.put(@valid_metadata, :invalid, "invalid") assert {:error, _} = Metadata.new(data) end test "return error with invalid bookmarks" do data = Map.put(@valid_metadata, :bookmarks, "invalid") assert {:error, _} = Metadata.new(data) end test "return nil with empty bookmarks list" do data = Map.put(@valid_metadata, :bookmarks, []) expected = %Metadata{ bookmarks: nil, tx_timeout: data.tx_timeout, metadata: data.metadata } assert {:ok, result} = Metadata.new(data) assert expected == result end test "return error with invalid timeout" do data = Map.put(@valid_metadata, :tx_timeout, -12) assert {:error, _} = Metadata.new(data) end test "return error with invalid metadata" do data = Map.put(@valid_metadata, :metadata, "invalid") assert {:error, _} = Metadata.new(data) end test "return nil with empty metadata map" do data = Map.put(@valid_metadata, :metadata, %{}) expected = %Metadata{ bookmarks: data.bookmarks, tx_timeout: data.tx_timeout, metadata: nil } assert {:ok, result} = Metadata.new(data) assert expected == result end end test "to_map remove nullified data" do data = %{ bookmarks: ["neo4j:bookmark:v1:tx1111"], tx_timeout: 5000 } assert {:ok, metadata} = Metadata.new(data) assert %Metadata{bookmarks: ["neo4j:bookmark:v1:tx1111"], metadata: nil, tx_timeout: 5000} == metadata expected = %{ bookmarks: ["neo4j:bookmark:v1:tx1111"], tx_timeout: 5000 } assert expected == Metadata.to_map(metadata) end end ================================================ FILE: test/bolt_sips/performance_test.exs ================================================ defmodule Bolt.Sips.PerformanceTest do use Bolt.Sips.ConnCase, async: false setup(%{conn: conn} = context) do Bolt.Sips.Test.Support.Database.clear(conn) {:ok, context} end @tag :bench test "Querying 500 nodes should under 100ms", context do conn = context[:conn] cql_create = 1..500 |> Enum.map(fn x -> "CREATE (:Test {value: 'test_#{inspect(x)}'})" end) |> Enum.join("\n") assert %Bolt.Sips.Response{stats: %{"nodes-created" => 500}} = Bolt.Sips.query!(Bolt.Sips.conn(), cql_create) simple_cypher = """ MATCH (t:Test) RETURN t AS test """ output = Benchee.run( %{ # "run" => fn -> query.(conn, simple_cypher) end "run" => fn -> Bolt.Sips.Query.query(conn, simple_cypher) end # " new conn" => fn -> query.(Bolt.Sips.conn(), simple_cypher) end }, time: 5 ) # Query should take less than 50ms in average assert Enum.at(output.scenarios, 0).run_time_data.statistics.average < 125_000_000 end @tag :bench test "Creating nodes with properties and a long list should take less than 100ms", context do conn = context[:conn] long_list = Enum.to_list(1..10_000) simple_cypher = """ CREATE (t:Test $props) RETURN t AS test """ output = Benchee.run( %{ # "run" => fn -> query.(conn, simple_cypher) end "run with properties" => fn -> Bolt.Sips.Query.query(conn, simple_cypher, %{ props: %{test_int: 124, test_float: 12.5, list: long_list} }) end # " new conn" => fn -> query.(Bolt.Sips.conn(), simple_cypher) end }, time: 5 ) # Query should take less than 50ms in average assert Enum.at(output.scenarios, 0).run_time_data.statistics.average < 125_000_000 end end ================================================ FILE: test/bolt_sips/protocol_test.exs ================================================ defmodule Bolt.Sips.ProtocolTest do use ExUnit.Case, async: false alias Bolt.Sips.Protocol # Transactions are not tested as BEGIN fails # But works fine from Bolt.Sips.transaction test "connect/1 - disconnect/1 successful" do assert {:ok, %Protocol.ConnData{sock: _, bolt_version: _, configuration: _} = conn_data} = Protocol.connect([]) assert :ok = Protocol.disconnect(:stop, conn_data) end test "checkout/1 successful" do {:ok, %Protocol.ConnData{sock: _, bolt_version: _, configuration: _} = conn_data} = Protocol.connect([]) assert {:ok, %Protocol.ConnData{sock: _, bolt_version: _, configuration: _} = conn_data} = Protocol.checkout(conn_data) :ok = Protocol.disconnect(:stop, conn_data) end test "checkin/1 successful" do {:ok, %Protocol.ConnData{sock: _, bolt_version: _, configuration: _} = conn_data} = Protocol.connect([]) assert {:ok, %Protocol.ConnData{sock: _, bolt_version: _, configuration: _} = conn_data} = Protocol.checkin(conn_data) :ok = Protocol.disconnect(:stop, conn_data) end end ================================================ FILE: test/bolt_sips/response_encoder/json_implementations_test.exs ================================================ defmodule Bolt.Sips.JsonImplementationsTest do use ExUnit.Case, async: true alias Bolt.Sips.Types.{ DateTimeWithTZOffset, TimeWithTZOffset, Duration, Point, Node, Relationship, UnboundRelationship, Path } defmodule TestStruct do defstruct [:id, :name] end test "Jason implementation OK" do assert result(:jason) == Jason.encode!(fixture()) end test "Poison implementation OK" do assert result(:poison) == Poison.encode!(fixture()) end defp fixture() do %Path{ nodes: [ %Node{ id: 56, labels: [], properties: %{ "bolt_sips" => true, "name" => "Alice", geoloc: Point.create(:wgs_84, 45.006, 40.32332, 50), duration: %Duration{ days: 0, hours: 0, minutes: 54, months: 12, nanoseconds: 0, seconds: 65, weeks: 0, years: 1 } } }, %Node{ id: 57, labels: [], properties: %{ "bolt_sips" => true, "name" => "Bob", created: DateTimeWithTZOffset.create(~N[2019-03-05 12:34:56], 3600), user_strut: %TestStruct{id: 43, name: "Test"} } } ], relationships: [ %UnboundRelationship{ end: nil, id: 58, properties: %{ creation_time: TimeWithTZOffset.create(~T[12:34:56], 7200) }, start: nil, type: "KNOWS" }, %Relationship{ end: 57, id: 58, properties: %{}, start: 56, type: "LIKES" } ], sequence: [1, 1] } end # Poison and Jason doesn't order keys the same way defp result(:jason) do # Pretty formated: # { # "nodes": [ # { # "id": 56, # "labels": [], # "properties": { # "duration": "P1Y12MT54M65.0S", # "geoloc": { # "crs": "wgs-84-3d", # "height": 50.0, # "latitude": 40.32332, # "longitude": 45.006, # "x": 45.006, # "y": 40.32332, # "z": 50.0 # }, # "bolt_sips": true, # "name": "Alice" # } # }, # { # "id": 57, # "labels": [], # "properties": { # "created": "2019-03-05T12:34:56+01:00", # "user_struct": { # id: 43, # name: "Test" # }, # "bolt_sips": true, # "name": "Bob" # } # } # ], # "relationships": [ # { # "end": null, # "id": 58, # "properties": { # "creation_time": "12:34:56+02:00" # }, # "start": null, # "type": "KNOWS" # }, # { # "end": 57, # "id": 58, # "properties": {}, # "start": 56, # "type": "LIKES" # } # ], # "sequence": [ # 1, # 1 # ] # } "{\"nodes\":[{\"id\":56,\"labels\":[],\"properties\":{\"duration\":\"P1Y12MT54M65.0S\",\"geoloc\":{\"crs\":\"wgs-84-3d\",\"height\":50.0,\"latitude\":40.32332,\"longitude\":45.006,\"x\":45.006,\"y\":40.32332,\"z\":50.0},\"bolt_sips\":true,\"name\":\"Alice\"}},{\"id\":57,\"labels\":[],\"properties\":{\"created\":\"2019-03-05T12:34:56+01:00\",\"user_strut\":{\"id\":43,\"name\":\"Test\"},\"bolt_sips\":true,\"name\":\"Bob\"}}],\"relationships\":[{\"end\":null,\"id\":58,\"properties\":{\"creation_time\":\"12:34:56+02:00\"},\"start\":null,\"type\":\"KNOWS\"},{\"end\":57,\"id\":58,\"properties\":{},\"start\":56,\"type\":\"LIKES\"}],\"sequence\":[1,1]}" end defp result(:poison) do "{\"sequence\":[1,1],\"relationships\":[{\"type\":\"KNOWS\",\"start\":null,\"properties\":{\"creation_time\":\"12:34:56+02:00\"},\"id\":58,\"end\":null},{\"type\":\"LIKES\",\"start\":56,\"properties\":{},\"id\":58,\"end\":57}],\"nodes\":[{\"properties\":{\"name\":\"Alice\",\"bolt_sips\":true,\"geoloc\":{\"z\":50.0,\"y\":40.32332,\"x\":45.006,\"longitude\":45.006,\"latitude\":40.32332,\"height\":50.0,\"crs\":\"wgs-84-3d\"},\"duration\":\"P1Y12MT54M65.0S\"},\"labels\":[],\"id\":56},{\"properties\":{\"name\":\"Bob\",\"bolt_sips\":true,\"user_strut\":{\"name\":\"Test\",\"id\":43},\"created\":\"2019-03-05T12:34:56+01:00\"},\"labels\":[],\"id\":57}]}" end end ================================================ FILE: test/bolt_sips/response_encoder/json_test.exs ================================================ defmodule Bolt.Sips.ResponseEncode.JsonTest do use ExUnit.Case, async: true alias Bolt.Sips.Types.{ DateTimeWithTZOffset, TimeWithTZOffset, Duration, Point, Node, Relationship, UnboundRelationship, Path } alias Bolt.Sips.ResponseEncoder.Json defmodule TestStruct do defstruct [:id, :name] end test "Encode a DateTimeWithTZOffset" do dt = DateTimeWithTZOffset.create(~N[2019-03-05 12:34:56], 3600) assert "2019-03-05T12:34:56+01:00" == Json.encode(dt) end test "Encode a TimeWithTZOffset" do t = TimeWithTZOffset.create(~T[12:34:56], 7200) assert "12:34:56+02:00" == Json.encode(t) end test "Encode a Duration" do d = %Duration{ days: 0, hours: 0, minutes: 54, months: 12, nanoseconds: 0, seconds: 65, weeks: 0, years: 1 } assert "P1Y12MT54M65.0S" == Json.encode(d) end test "Encode a Point" do p = Point.create(:cartesian, 50, 60.5) assert %{crs: "cartesian", x: 50.0, y: 60.5} == Json.encode(p) end test "Encode a Node" do n = %Node{ id: 69, labels: ["Test"], properties: %{ "uuid" => 12345, "name" => "First node" } } expected = %{id: 69, labels: ["Test"], properties: %{"name" => "First node", "uuid" => 12345}} assert expected == Json.encode(n) end test "Encode a Relationship" do r = %Relationship{ end: 30, id: 5, properties: %{ is_valid: true }, start: 69, type: "UPDATED_TO" } expected = %{end: 30, id: 5, properties: %{is_valid: true}, start: 69, type: "UPDATED_TO"} assert expected == Json.encode(r) end test "Encode a UnboundRelationship" do r = %UnboundRelationship{ end: 30, id: 5, properties: %{ is_valid: true }, start: 69, type: "UPDATED_TO" } expected = %{end: 30, id: 5, properties: %{is_valid: true}, start: 69, type: "UPDATED_TO"} assert expected == Json.encode(r) end test "Encode a Path" do p = %Path{ nodes: [ %Node{ id: 56, labels: [], properties: %{"bolt_sips" => true, "name" => "Alice"} }, %Node{ id: 57, labels: [], properties: %{"bolt_sips" => true, "name" => "Bob"} } ], relationships: [ %UnboundRelationship{ end: nil, id: 58, properties: %{}, start: nil, type: "KNOWS" } ], sequence: [1, 1] } expected = %{ nodes: [ %{id: 56, labels: [], properties: %{"bolt_sips" => true, "name" => "Alice"}}, %{id: 57, labels: [], properties: %{"bolt_sips" => true, "name" => "Bob"}} ], relationships: [%{end: nil, id: 58, properties: %{}, start: nil, type: "KNOWS"}], sequence: [1, 1] } assert expected == Json.encode(p) end test "Encode user-defined struct" do s = %TestStruct{id: 1, name: "test"} expected = %{id: 1, name: "test"} assert expected == Json.encode(s) end test "Encode Nested types" do nested = [ %{ "rel" => %Relationship{ end: 30, id: 5, properties: %{ "created" => %DateTimeWithTZOffset{ naive_datetime: ~N[2016-05-24 13:26:08.543], timezone_offset: 7200 } }, start: 69, type: "UPDATED_TO_" }, "t1" => %Node{ id: 69, labels: ["Test"], properties: %{ "created" => %DateTimeWithTZOffset{ naive_datetime: ~N[2016-05-24 13:26:08.543], timezone_offset: 7200 }, "uuid" => 12345 } }, "t2" => %Node{ id: 30, labels: ["Test"], properties: %{"uuid" => 6789} } } ] expected = [ %{ "rel" => %{ end: 30, id: 5, properties: %{"created" => "2016-05-24T13:26:08.543+02:00"}, start: 69, type: "UPDATED_TO_" }, "t1" => %{ id: 69, labels: ["Test"], properties: %{ "created" => "2016-05-24T13:26:08.543+02:00", "uuid" => 12345 } }, "t2" => %{ id: 30, labels: ["Test"], properties: %{"uuid" => 6789} } } ] assert expected == Json.encode(nested) end end ================================================ FILE: test/bolt_sips/response_encoder_test.exs ================================================ defmodule Bolt.Sips.ResponseEncoderTest do use ExUnit.Case, async: true doctest Bolt.Sips.ResponseEncoder end ================================================ FILE: test/bolt_sips/types_helpers_test.exs ================================================ defmodule Bolt.Sips.TypesHelperTest do use ExUnit.Case, async: true alias Bolt.Sips.TypesHelper describe "decompose_in_hms/1:" do test "Ok if more than a hour" do assert {1, 6, 27} = TypesHelper.decompose_in_hms(3987) end test "Ok if less than a hour" do assert {0, 44, 35} = TypesHelper.decompose_in_hms(2675) end test "Ok if less than a minute" do assert {0, 0, 43} = TypesHelper.decompose_in_hms(43) end test "edge case: 1 hour" do assert {1, 0, 0} = TypesHelper.decompose_in_hms(3600) end test "edge case: 1 minute" do assert {0, 1, 0} = TypesHelper.decompose_in_hms(60) end end describe "datetime_with_micro/2:" do test "Successful with valid data" do expected = %DateTime{ calendar: Calendar.ISO, day: 1, hour: 23, microsecond: {0, 0}, minute: 0, month: 1, second: 7, std_offset: 0, time_zone: "Europe/Paris", utc_offset: 3600, year: 2000, zone_abbr: "CET" } assert ^expected = TypesHelper.datetime_with_micro(~N[2000-01-01 23:00:07], "Europe/Paris") end test "Fails with invalid timezone" do assert_raise ArgumentError, fn -> TypesHelper.datetime_with_micro(~N[2000-01-01 23:00:07], "Invalid") end end end describe "formated-time_offset/1" do test "Valid positive offset" do assert "+01:03" == TypesHelper.formated_time_offset(3783) end test "Valid negative offset" do assert "-01:03" == TypesHelper.formated_time_offset(-3783) end end end ================================================ FILE: test/bolt_sips/types_test.exs ================================================ defmodule Bolt.Sips.TypesTest do use ExUnit.Case, async: true alias Bolt.Sips.Types.{DateTimeWithTZOffset, Duration, TimeWithTZOffset, Point} describe "TimeWithTZOffset struct:" do test "create/2" do expected = %TimeWithTZOffset{time: ~T[20:00:43], timezone_offset: 3600} assert ^expected = TimeWithTZOffset.create(~T[20:00:43], 3600) end test "format_param/1 successful with valid data" do t = %TimeWithTZOffset{time: ~T[23:00:07], timezone_offset: 3600} assert {:ok, "23:00:07+01:00"} = TimeWithTZOffset.format_param(t) end test "format_param/1 fails for invalid data" do t = %TimeWithTZOffset{time: ~T[23:00:07], timezone_offset: 3600.543} assert {:error, ^t} = TimeWithTZOffset.format_param(t) end end describe "DateTimeWithTZOffset struct:" do test "create/2" do expected = %DateTimeWithTZOffset{ naive_datetime: ~N[2000-01-01 23:00:07], timezone_offset: 3600 } assert ^expected = DateTimeWithTZOffset.create(~N[2000-01-01 23:00:07], 3600) end test "format_param/1 successful with valid data" do t = %DateTimeWithTZOffset{ naive_datetime: ~N[2000-01-01 23:00:07], timezone_offset: 3600 } assert {:ok, "2000-01-01T23:00:07+01:00"} = DateTimeWithTZOffset.format_param(t) end test "format_param/2 fails for invalid data" do # timezone_offset can't be a float t = %DateTimeWithTZOffset{ naive_datetime: ~N[2000-01-01 23:00:07], timezone_offset: 3600.43 } assert {:error, ^t} = DateTimeWithTZOffset.format_param(t) end end describe "Duration struct:" do test "create/4" do expected = %Duration{ days: 53, hours: 0, minutes: 2, months: 3, nanoseconds: 54, seconds: 5, weeks: 0, years: 1 } assert expected == Duration.create(15, 53, 125, 54) end test "format_param/1 successful with valid data" do duration = Duration.create(15, 53, 125, 54) assert {:ok, "P1Y3M53DT2M5.000000054S"} = Duration.format_param(duration) end test "format_param/1 successfull with large amount of nanoseconds (use create/4 to build struct)" do duration = Duration.create(0, 0, 0, 12_545_876_654) assert {:ok, "PT12.545876654S"} = Duration.format_param(duration) end test "format_param/1 successfull with large amount of nanoseconds (use %Duration{} to build struct)" do duration = %Duration{ days: 0, hours: 0, minutes: 0, months: 0, nanoseconds: 12_545_876_654, seconds: 0, weeks: 0, years: 0 } assert {:ok, "PT12.545876654S"} = Duration.format_param(duration) end test "format_param/1 fails for invalid data" do duration = %Duration{ days: 53.45, hours: 0, minutes: 2, months: 3, nanoseconds: 54, seconds: 5, weeks: 0, years: 1 } assert {:error, ^duration} = Duration.format_param(duration) end end describe "Point struct:" do test "create/3 succesfully creates a CARTESIAN point 2D" do expected = %Point{ crs: "cartesian", srid: 7203, latitude: nil, longitude: nil, height: nil, x: 10.0, y: 20.0, z: nil } assert expected == Point.create(:cartesian, 10, 20.0) end test "create/3 succesfully creates a GEOGRAPHIC point 2D" do expected = %Point{ crs: "wgs-84", srid: 4326, latitude: 20.0, longitude: 10.0, height: nil, x: 10.0, y: 20.0, z: nil } assert expected == Point.create(:wgs_84, 10, 20.0) end test "create/4 succesfully creates a CARTESIAN point 3D" do expected = %Point{ crs: "cartesian-3d", srid: 9157, latitude: nil, longitude: nil, height: nil, x: 10.0, y: 20.0, z: 25.43 } assert expected == Point.create(:cartesian, 10, 20.0, 25.43) end test "create/4 succesfully creates a GEOGRAPHIC point 3D" do expected = %Point{ crs: "wgs-84-3d", srid: 4979, latitude: 20.0, longitude: 10.0, height: 25.43, x: 10.0, y: 20.0, z: 25.43 } assert expected == Point.create(:wgs_84, 10, 20.0, 25.43) end test "format_param/1 successful with valid param" do point = Point.create(:wgs_84, 10, 20.0, 25.43) expected = %{ crs: "wgs-84-3d", height: 25.43, latitude: 20.0, longitude: 10.0, x: 10.0, y: 20.0, z: 25.43 } assert {:ok, expected} == Point.format_param(point) end test "format_param/2 fails for invalid param" do point = %Point{ crs: "wgs-84-3d", srid: 4979, latitude: 20.0, longitude: 10.0, height: 25.43, x: 10.0, y: 20.0, z: "invalid" } assert {:error, ^point} = Point.format_param(point) end end end ================================================ FILE: test/boltkit_test.exs ================================================ defmodule Bolt.Sips.BoltStubTest do @moduledoc """ !!Remember!! you cannot reuse the boltstub across the tests, therefore must use a unique prefix or use the one returned by the BoltKitCase """ use Bolt.Sips.BoltKitCase, async: false @moduletag :boltkit @tag boltkit: %{ url: "bolt://127.0.0.1:9001", scripts: [ {"test/scripts/return_x.bolt", 9001} ], debug: true } test "test/scripts/return_x.bolt", %{prefix: prefix} do assert %Bolt.Sips.Response{results: [%{"x" => 1}]} = Bolt.Sips.conn(:direct, prefix: prefix) |> Bolt.Sips.query!("RETURN $x", %{x: 1}) end @tag boltkit: %{ url: "bolt://127.0.0.1:9001", scripts: [ {"test/scripts/count.bolt", 9001} ] } test "test/scripts/count.bolt", %{prefix: prefix} do assert %Bolt.Sips.Response{ results: [ %{"n" => 1}, %{"n" => 2}, %{"n" => 3}, %{"n" => 4}, %{"n" => 5}, %{"n" => 6}, %{"n" => 7}, %{"n" => 8}, %{"n" => 9}, %{"n" => 10} ] } = Bolt.Sips.conn(:direct, prefix: prefix) |> Bolt.Sips.query!("UNWIND range(1, 10) AS n RETURN n") end end ================================================ FILE: test/config_test.exs ================================================ defmodule Config.Test do use ExUnit.Case alias Bolt.Sips.Utils doctest Bolt.Sips @graphenedb_like_url "bolt://hobby-happyHoHoHo.dbs.graphenedb.com:24786" @basic_tls_config [ url: @graphenedb_like_url, basic_auth: [username: "xmas", password: "Kr1ngl3"], ssl: [verify: :verify_none] ] @light_tls_config [ url: "bolt://xmas:Kr1ngl3@hobby-happyHoHoHo.dbs.graphenedb.com:24786", ssl: true ] @mixed_config [ hostname: "127.0.0.1", port: 0, url: @graphenedb_like_url ] @basic_config [ hostname: "hobby", basic_auth: [username: "neo4j", password: "neo4j"], port: 1234, pool_size: 15, max_overflow: 3, prefix: :default ] test "parsing the host and the port, from a url string config parameter" do config = Utils.default_config(@basic_tls_config) assert config[:url] == @graphenedb_like_url assert config[:hostname] == "hobby-happyHoHoHo.dbs.graphenedb.com" assert config[:basic_auth] == [username: "xmas", password: "Kr1ngl3"] assert config[:port] == 24786 assert config[:ssl] == [verify: :verify_none] assert config[:prefix] == :default end test "url string in config overides the :hostname and the :port" do config = Utils.default_config(@mixed_config) assert config[:url] == @graphenedb_like_url assert config[:hostname] == "hobby-happyHoHoHo.dbs.graphenedb.com" assert config[:port] == 24786 end test "standard Bolt.Sips configuration parameters" do config = Utils.default_config(@basic_config) assert config[:url] == nil assert config[:hostname] == "hobby" assert config[:basic_auth] == [username: "neo4j", password: "neo4j"] assert config[:port] == 1234 assert config[:ssl] == false end test "url containing authentication details, the hostname, the protocol and port, all together" do config = Utils.default_config(@light_tls_config) assert config[:url] != nil assert config[:hostname] == "hobby-happyHoHoHo.dbs.graphenedb.com" assert config[:basic_auth] == [username: "xmas", password: "Kr1ngl3"] assert config[:port] == 24786 assert config[:ssl] == true end test "standard Bolt.Sips default configuration" do config = Utils.default_config([]) assert config[:hostname] == "localhost" assert config[:port] == 7687 assert config[:ssl] == false end test "invalid url in configuration and explicit :port" do config = Utils.default_config(url: "example.com", port: 123) assert config[:hostname] == nil assert config[:port] == 7687 end test "url with routing context" do config = [url: "bolt+routing://neo4j:password@neo01.graph.example.com:123456?policy=europe"] |> Utils.default_config() assert config[:routing_context] == %{"policy" => "europe"} assert config[:schema] == "bolt+routing" assert config[:hostname] == "neo01.graph.example.com" assert config[:port] == 123_456 assert config[:query] == "policy=europe" assert config[:basic_auth] == [username: "neo4j", password: "password"] end end ================================================ FILE: test/errors_test.exs ================================================ defmodule ErrorsTest do @moduledoc """ every new error, and related tests """ use ExUnit.Case, async: true @simple_map %{foo: "bar", bolt_sips: true} @nested_map %{ foo: "bar", bolt_sips: true, a_map: %{unu: 1, doi: 2, baz: "foo"}, a_list: [1, 2, 3.14] } test "create a node using SET properties and a simple map" do %Bolt.Sips.Response{stats: stats, type: type} = Bolt.Sips.query!(Bolt.Sips.conn(), "CREATE (report:Report) SET report = $props", %{ props: @simple_map }) assert %{"labels-added" => 1, "nodes-created" => 1, "properties-set" => 2} == stats assert "w" == type end test "exception when creating a node using SET properties with a nested map" do err = "Property values can only be of primitive types or arrays thereof" assert_raise Bolt.Sips.Exception, err, fn -> Bolt.Sips.query!( Bolt.Sips.conn(), "CREATE (report:Report) SET report = $props", %{props: @nested_map} ) end end test "exception when creating a node using SET properties with a list" do assert_raise Bolt.Sips.Exception, fn -> Bolt.Sips.query!(Bolt.Sips.conn(), "CREATE (report:Report) SET report = $props", %{ props: ["foo", "bar"] }) end end end ================================================ FILE: test/invalid_param_type_test.exs ================================================ defmodule Bolt.Sips.InvalidParamType.Test do use ExUnit.Case setup_all do Bolt.Sips.ConnectionSupervisor.connections() {:ok, [conn: Bolt.Sips.conn()]} end test "executing a Cypher query, with invalid parameter value yields an error", context do conn = context[:conn] cypher = """ MATCH (n:Person {invalid: {an_elixir_datetime}}) RETURN TRUE """ {:error, %Bolt.Sips.Error{message: message}} = Bolt.Sips.query(conn, cypher, %{an_elixir_tuple: {:not, :valid}}) assert String.match?(message, ~r/unable to encode value: {:not, :valid}/i) end end ================================================ FILE: test/one_test.exs ================================================ defmodule One.Test do # use Bolt.Sips.RoutingConnCase # @moduletag :routing # # alias Bolt.Sips.{Success, Error, Response} # # alias Bolt.Sips.Types.{Node, Relationship, UnboundRelationship, Path} # @tag :routing # test "temporary placeholder for focused tests during development/debugging" do # assert %{"r" => 300} == # Bolt.Sips.conn(:write) |> Bolt.Sips.query!("RETURN 300 AS r") |> List.first() # end use ExUnit.Case alias Bolt.Sips.Response test "a simple query" do conn = Bolt.Sips.conn() response = Bolt.Sips.query!(conn, "RETURN 300 AS r") assert %Response{results: [%{"r" => 300}]} = response assert response |> Enum.member?("r") assert 1 = response |> Enum.count() assert [%{"r" => 300}] = response |> Enum.take(1) assert %{"r" => 300} = response |> Response.first() end # @tag :skip test "multiple statements" do conn = Bolt.Sips.conn() q = """ MATCH (n {bolt_sips: true}) OPTIONAL MATCH (n)-[r]-() DELETE n,r; CREATE (BoltSip:BoltSip {title:'Elixir sipping from Neo4j, using Bolt', released:2016, license:'MIT', bolt_sips: true}); MATCH (b:BoltSips{bolt_sips: true}) RETURN b """ l = Bolt.Sips.query!(conn, q) assert is_list(l) assert 3 == Enum.filter(l, fn %Response{} -> true _ -> false end) |> Enum.count() end end ================================================ FILE: test/query_bolt_v2_test.exs ================================================ defmodule Bolt.Sips.QueryBoltV2Test do use Bolt.Sips.ConnCase, async: true @moduletag :bolt_v2 alias Bolt.Sips.Types.{Duration, DateTimeWithTZOffset, Point, TimeWithTZOffset} alias Bolt.Sips.{TypesHelper, Response} setup_all do # reuse the same connection for all the tests in the suite conn = Bolt.Sips.conn() {:ok, [conn: conn]} end test "transform Point in cypher-compliant data", context do conn = context[:conn] query = "RETURN point($point_data) AS pt" params = %{point_data: Point.create(:cartesian, 50, 60.5)} assert {:ok, %Response{results: res}} = Bolt.Sips.query(conn, query, params) assert res == [ %{ "pt" => %Bolt.Sips.Types.Point{ crs: "cartesian", height: nil, latitude: nil, longitude: nil, srid: 7203, x: 50.0, y: 60.5, z: nil } } ] end test "transform Duration in cypher-compliant data", context do conn = context[:conn] query = "RETURN duration($d) AS d" params = %{ d: %Duration{ days: 0, hours: 0, minutes: 54, months: 12, nanoseconds: 0, seconds: 65, weeks: 0, years: 1 } } expected = %Duration{ days: 0, hours: 0, minutes: 55, months: 0, nanoseconds: 0, seconds: 5, weeks: 0, years: 2 } assert {:ok, %Response{results: [%{"d" => ^expected}]}} = Bolt.Sips.query(conn, query, params) end test "transform Date in cypher-compliant data", context do conn = context[:conn] query = "RETURN date($d) AS d" params = %{d: ~D[2019-02-04]} assert {:ok, %Response{results: res}} = Bolt.Sips.query(conn, query, params) assert res == [%{"d" => ~D[2019-02-04]}] end test "transform TimeWithTZOffset in cypher-compliant data", context do conn = context[:conn] query = "RETURN time($t) AS t" time_with_tz = %TimeWithTZOffset{time: ~T[12:45:30.250876], timezone_offset: 3600} params = %{t: time_with_tz} assert {:ok, %Response{results: [%{"t" => ^time_with_tz}]}} = Bolt.Sips.query(conn, query, params) end test "transform DateTimeWithTZOffset in cypher-compliant data", context do conn = context[:conn] query = "RETURN datetime($t) AS t" date_time_with_tz = %DateTimeWithTZOffset{ naive_datetime: ~N[2016-05-24 13:26:08.543267], timezone_offset: 7200 } params = %{t: date_time_with_tz} assert {:ok, %Response{results: [%{"t" => ^date_time_with_tz}]}} = Bolt.Sips.query(conn, query, params) end test "transform DateTime With TimeZone id (UTC) in cypher-compliant data", context do conn = context[:conn] query = "RETURN datetime($t) AS t" date_time_with_tz_id = TypesHelper.datetime_with_micro(~N[2016-05-24 13:26:08.543218], "Etc/UTC") params = %{t: date_time_with_tz_id} assert {:ok, %Response{results: [%{"t" => ^date_time_with_tz_id}]}} = Bolt.Sips.query(conn, query, params) end test "transform DateTime With TimeZone id (Non-UTC) in cypher-compliant data", context do conn = context[:conn] query = "RETURN datetime($t) AS t" date_time_with_tz_id = TypesHelper.datetime_with_micro(~N[2016-05-24 13:26:08.543789], "Europe/Paris") params = %{t: date_time_with_tz_id} assert {:ok, %Response{results: [%{"t" => ^date_time_with_tz_id}]}} = Bolt.Sips.query(conn, query, params) end test "transform NaiveDateTime in cypher-compliant data", context do conn = context[:conn] query = "RETURN localdatetime($t) AS t" ndt = ~N[2016-05-24 13:26:08.543156] params = %{t: ndt} assert {:ok, %Response{results: [%{"t" => ^ndt}]}} = Bolt.Sips.query(conn, query, params) end test "transform Time in cypher-compliant data", context do conn = context[:conn] query = "RETURN localtime($t) AS t" t = ~T[13:26:08.543440] params = %{t: t} assert {:ok, %Response{results: [%{"t" => ^t}]}} = Bolt.Sips.query(conn, query, params) end end ================================================ FILE: test/query_test.exs ================================================ defmodule Query.Test do use Bolt.Sips.ConnCase, async: true alias Query.Test alias Bolt.Sips.Test.Support.Database alias Bolt.Sips.Response defmodule TestUser do defstruct name: "", bolt_sips: true end defp rebuild_fixtures(conn) do Database.clear(conn) Bolt.Sips.Fixture.create_graph(conn, :bolt_sips) end setup(%{conn: conn} = context) do rebuild_fixtures(conn) {:ok, context} end test "a simple query that should work", context do conn = context[:conn] cyp = """ MATCH (n:Person {bolt_sips: true}) RETURN n.name AS Name ORDER BY Name DESC LIMIT 5 """ {:ok, %Response{} = row} = Bolt.Sips.query(conn, cyp) assert Response.first(row)["Name"] == "Patrick Rothfuss", "missing 'The Name of the Wind' database, or data incomplete" end test "A procedure call failure should send reset and not lock the db", context do expected_neo4j = System.get_env("NEO4J_VERSION") || "3.0.0" if Version.match?(expected_neo4j, "~> 3.5.0") do conn = context[:conn] cyp_fail = """ CALL db.index.fulltext.queryNodes(\"topic_label\", \"badparen)\") YIELD node RETURN node """ {:error, %Bolt.Sips.Error{code: "Neo.ClientError.Procedure.ProcedureCallFailed"}} = Bolt.Sips.query(conn, cyp_fail) cyp = """ MATCH (n:Person {bolt_sips: true}) RETURN n.name AS Name ORDER BY Name DESC LIMIT 5 """ {:ok, %Response{} = row} = Bolt.Sips.query(conn, cyp) assert Response.first(row)["Name"] == "Patrick Rothfuss", "missing 'The Name of the Wind' database, or data incomplete" end end @tag :apoc test "Passing a timeout option to the query should prevent a timeout", context do conn = context[:conn] cyp_wait = """ CALL apoc.util.sleep(20000) RETURN 1 as test """ {:ok, %Response{} = _row} = Bolt.Sips.query(conn, cyp_wait, %{}, timeout: 21_000) end @tag :apoc test "After a timeout, subsequent queries should work", context do conn = context[:conn] cyp_wait = """ CALL apoc.util.sleep(10000) RETURN 1 as test """ {:error, _} = Bolt.Sips.query(conn, cyp_wait, %{}, timeout: 5_000) cyp = """ MATCH (n:Person {bolt_sips: true}) RETURN n.name AS Name ORDER BY Name DESC LIMIT 5 """ {:ok, %Response{} = row} = Bolt.Sips.query(conn, cyp) assert Response.first(row)["Name"] == "Patrick Rothfuss", "missing 'The Name of the Wind' database, or data incomplete" end test "executing a Cypher query, with parameters", context do conn = context[:conn] cypher = """ MATCH (n:Person {bolt_sips: true}) WHERE n.name = $name RETURN n.name AS name """ case Bolt.Sips.query(conn, cypher, %{name: "Kote"}) do {:ok, %Response{} = rows} -> refute Enum.count(rows) == 0, "Did you initialize the 'The Name of the Wind' database?" refute Enum.count(rows) > 1, "Kote?! There is only one!" assert Response.first(rows)["name"] == "Kote", "expecting to find Kote" {:error, reason} -> IO.puts("Error: #{reason["message"]}") end end test "executing a Cypher query, with struct parameters", context do conn = context[:conn] cypher = """ CREATE(n:User $props) """ assert {:ok, %Response{ stats: %{ "labels-added" => 1, "nodes-created" => 1, "properties-set" => 2 }, type: "w" }} = Bolt.Sips.query(conn, cypher, %{ props: %Test.TestUser{name: "Strut", bolt_sips: true} }) end test "executing a Cpyher query, with map parameters", context do conn = context[:conn] cypher = """ CREATE(n:User $props) """ assert {:ok, %Response{}} = Bolt.Sips.query(conn, cypher, %{props: %{name: "Mep", bolt_sips: true}}) end test "executing a raw Cypher query with alias, and no parameters", context do conn = context[:conn] cypher = """ MATCH (p:Person {bolt_sips: true}) RETURN p, p.name AS name, toUpper(p.name) as NAME, coalesce(p.nickname,"n/a") AS nickname, { name: p.name, label:head(labels(p))} AS person ORDER BY name DESC """ {:ok, %Response{} = r} = Bolt.Sips.query(conn, cypher) assert Enum.count(r) == 3, "you're missing some characters from the 'The Name of the Wind' db" if row = Response.first(r) do assert row["p"].properties["name"] == "Patrick Rothfuss" assert is_map(row["p"]), "was expecting a map `p`" assert row["person"]["label"] == "Person" assert row["NAME"] == "PATRICK ROTHFUSS" assert row["nickname"] == "n/a" assert row["p"].properties["bolt_sips"] == true else IO.puts("Did you initialize the 'The Name of the Wind' database?") end end test "if Patrick Rothfuss wrote The Name of the Wind", context do conn = context[:conn] cypher = """ MATCH (p:Person)-[r:WROTE]->(b:Book {title: 'The Name of the Wind'}) RETURN p """ %Response{} = rows = Bolt.Sips.query!(conn, cypher) assert Response.first(rows)["p"].properties["name"] == "Patrick Rothfuss" end test "it returns only known role names", context do conn = context[:conn] cypher = """ MATCH (p)-[r:ACTED_IN]->() where p.bolt_sips RETURN r.roles as roles LIMIT 25 """ %Response{results: rows} = Bolt.Sips.query!(conn, cypher) roles = ["killer", "sword fighter", "magician", "musician", "many talents"] my_roles = Enum.map(rows, & &1["roles"]) |> List.flatten() assert my_roles -- roles == [], "found more roles in the db than expected" end test "path from: MERGE p=({name:'Alice'})-[:KNOWS]-> ...", context do conn = context[:conn] cypher = """ MERGE p = ({name:'Alice', bolt_sips: true})-[:KNOWS]->({name:'Bob', bolt_sips: true}) RETURN p """ path = Bolt.Sips.query!(conn, cypher) |> Response.first() |> Map.get("p") assert {2, 1} == {length(path.nodes), length(path.relationships)} end test "return a single number from a statement with params", context do conn = context[:conn] row = Bolt.Sips.query!(conn, "RETURN $n AS num", %{n: 10}) |> Response.first() assert row["num"] == 10 end test "run simple statement with complex params", context do conn = context[:conn] row = Bolt.Sips.query!(conn, "RETURN $x AS n", %{x: %{abc: ["d", "e", "f"]}}) |> Response.first() assert row["n"]["abc"] == ["d", "e", "f"] end test "return an array of numbers", context do conn = context[:conn] row = Bolt.Sips.query!(conn, "RETURN [10,11,21] AS arr") |> Response.first() assert row["arr"] == [10, 11, 21] end test "return a string", context do conn = context[:conn] row = Bolt.Sips.query!(conn, "RETURN 'Hello' AS salute") |> Response.first() assert row["salute"] == "Hello" end test "UNWIND range(1, 10) AS n RETURN n", context do conn = context[:conn] assert %Response{results: rows} = Bolt.Sips.query!(conn, "UNWIND range(1, 10) AS n RETURN n") assert {1, 10} == rows |> Enum.map(& &1["n"]) |> Enum.min_max() end test "MERGE (k:Person {name:'Kote'}) RETURN k", context do conn = context[:conn] k = Bolt.Sips.query!(conn, "MERGE (k:Person {name:'Kote', bolt_sips: true}) RETURN k LIMIT 1") |> Response.first() |> Map.get("k") assert k.labels == ["Person"] assert k.properties["name"] == "Kote" end test "query/2 and query!/2", context do conn = context[:conn] assert r = Bolt.Sips.query!(conn, "RETURN [10,11,21] AS arr") assert [10, 11, 21] = Response.first(r)["arr"] assert {:ok, %Response{} = r} = Bolt.Sips.query(conn, "RETURN [10,11,21] AS arr") assert [10, 11, 21] = Response.first(r)["arr"] end test "create a Bob node and check it was deleted afterwards", context do conn = context[:conn] assert %Response{stats: stats} = Bolt.Sips.query!(conn, "CREATE (a:Person {name:'Bob'})") assert stats == %{"labels-added" => 1, "nodes-created" => 1, "properties-set" => 1} assert ["Bob"] == Bolt.Sips.query!(conn, "MATCH (a:Person {name: 'Bob'}) RETURN a.name AS name") |> Enum.map(& &1["name"]) assert %Response{stats: stats} = Bolt.Sips.query!(conn, "MATCH (a:Person {name:'Bob'}) DELETE a") assert stats["nodes-deleted"] == 1 end test "Cypher version 3", context do conn = context[:conn] assert %Response{plan: plan} = Bolt.Sips.query!(conn, "EXPLAIN RETURN 1") refute plan == nil assert Regex.match?(~r/CYPHER [3|4]/iu, plan["args"]["version"]) end test "EXPLAIN MATCH (n), (m) RETURN n, m", context do conn = context[:conn] assert %Response{notifications: notifications, plan: plan} = Bolt.Sips.query!(conn, "EXPLAIN MATCH (n), (m) RETURN n, m") refute notifications == nil refute plan == nil if Regex.match?(~r/CYPHER 3/iu, plan["args"]["version"]) do assert "CartesianProduct" == plan["children"] |> List.first() |> Map.get("operatorType") else assert( "CartesianProduct@neo4j" == plan["children"] |> List.first() |> Map.get("operatorType") ) end end test "can execute a query after a failure", context do conn = context[:conn] assert {:error, _} = Bolt.Sips.query(conn, "INVALID CYPHER") assert {:ok, %Response{results: [%{"n" => 22}]}} = Bolt.Sips.query(conn, "RETURN 22 as n") end test "negative numbers are returned as negative numbers", context do conn = context[:conn] assert {:ok, %Response{results: [%{"n" => -1}]}} = Bolt.Sips.query(conn, "RETURN -1 as n") end test "return a simple node", context do conn = context[:conn] assert %Response{ results: [ %{ "p" => %Bolt.Sips.Types.Node{ id: _, labels: ["Person"], properties: %{"bolt_sips" => true, "name" => "Patrick Rothfuss"} } } ] } = Bolt.Sips.query!(conn, "MATCH (p:Person {name: 'Patrick Rothfuss'}) RETURN p") end test "Simple relationship", context do conn = context[:conn] cypher = """ MATCH (p:Person)-[r:WROTE]->(b:Book {title: 'The Name of the Wind'}) RETURN r """ assert %Response{ results: [ %{ "r" => %Bolt.Sips.Types.Relationship{ end: _, id: _, properties: %{}, start: _, type: "WROTE" } } ] } = Bolt.Sips.query!(conn, cypher) end test "simple path", context do conn = context[:conn] cypher = """ MERGE p = ({name:'Alice', bolt_sips: true})-[:KNOWS]->({name:'Bob', bolt_sips: true}) RETURN p """ assert %Response{ results: [ %{ "p" => %Bolt.Sips.Types.Path{ nodes: [ %Bolt.Sips.Types.Node{ id: _, labels: [], properties: %{"bolt_sips" => true, "name" => "Alice"} }, %Bolt.Sips.Types.Node{ id: _, labels: [], properties: %{"bolt_sips" => true, "name" => "Bob"} } ], relationships: [ %Bolt.Sips.Types.UnboundRelationship{ end: nil, id: _, properties: %{}, start: nil, type: "KNOWS" } ], sequence: [1, 1] } } ] } = Bolt.Sips.query!(conn, cypher) end test "transaction (commit)", context do conn = context[:conn] Bolt.Sips.transaction(conn, fn conn -> book = Bolt.Sips.query!(conn, "CREATE (b:Book {title: \"The Game Of Trolls\"}) return b") |> Response.first() assert %{"b" => g_o_t} = book assert g_o_t.properties["title"] == "The Game Of Trolls" end) %Response{} = books = Bolt.Sips.query!(conn, "MATCH (b:Book {title: \"The Game Of Trolls\"}) return b") assert 1 == Enum.count(books) # Clean data rem_books = "MATCH (b:Book {title: \"The Game Of Trolls\"}) DELETE b" Bolt.Sips.query!(conn, rem_books) end test "transaction (rollback)", context do conn = context[:conn] Bolt.Sips.transaction(conn, fn conn -> book = Bolt.Sips.query!(conn, "CREATE (b:Book {title: \"The Game Of Trolls\"}) return b") |> Response.first() assert %{"b" => g_o_t} = book assert g_o_t.properties["title"] == "The Game Of Trolls" Bolt.Sips.rollback(conn, :changed_my_mind) end) assert %Response{} = r = Bolt.Sips.query!(conn, "MATCH (b:Book {title: \"The Game Of Trolls\"}) return b") assert Enum.count(r) == 0 end end ================================================ FILE: test/response_test.exs ================================================ defmodule ResponseTest do use ExUnit.Case alias Bolt.Sips.Response # import ExUnit.CaptureLog @explain [ success: %{"fields" => ["n"], "t_first" => 1}, success: %{ "bookmark" => "neo4j:bookmark:v1:tx13440", "plan" => %{ "args" => %{ "EstimatedRows" => 1.0, "planner" => "COST", "planner-impl" => "IDP", "planner-version" => "3.5", "runtime" => "INTERPRETED", "runtime-impl" => "INTERPRETED", "runtime-version" => "3.5", "version" => "CYPHER 3.5" }, "children" => [ %{ "args" => %{"EstimatedRows" => 1.0}, "children" => [], "identifiers" => ["n"], "operatorType" => "Create" } ], "identifiers" => ["n"], "operatorType" => "ProduceResults" }, "t_last" => 0, "type" => "rw" } ] @notifications [ success: %{"fields" => ["n", "m"], "t_first" => 0}, success: %{ "bookmark" => "neo4j:bookmark:v1:tx13440", "notifications" => [ %{ "code" => "Neo.ClientNotification.Statement.CartesianProductWarning", "description" => "bad juju", "position" => %{"column" => 9, "line" => 1, "offset" => 8}, "severity" => "WARNING", "title" => "This query builds a cartesian product between disconnected patterns." } ], "plan" => %{ "args" => %{ "EstimatedRows" => 36.0, "planner" => "COST", "planner-impl" => "IDP", "planner-version" => "3.5", "runtime" => "INTERPRETED", "runtime-impl" => "INTERPRETED", "runtime-version" => "3.5", "version" => "CYPHER 3.5" }, "children" => [ %{ "args" => %{"EstimatedRows" => 36.0}, "children" => [ %{ "args" => %{"EstimatedRows" => 6.0}, "children" => [], "identifiers" => ["n"], "operatorType" => "AllNodesScan" }, %{ "args" => %{"EstimatedRows" => 6.0}, "children" => [], "identifiers" => ["m"], "operatorType" => "AllNodesScan" } ], "identifiers" => ["m", "n"], "operatorType" => "CartesianProduct" } ], "identifiers" => ["m", "n"], "operatorType" => "ProduceResults" }, "t_last" => 0, "type" => "r" } ] @profile_no_results [ success: %{"fields" => [], "t_first" => 20}, success: %{ "bookmark" => "neo4j:bookmark:v1:tx48642", "profile" => %{ "args" => %{ "DbHits" => 0, "EstimatedRows" => 1.0, "PageCacheHitRatio" => 0.0, "PageCacheHits" => 0, "PageCacheMisses" => 0, "Rows" => 0, "planner" => "COST", "planner-impl" => "IDP", "planner-version" => "3.5", "runtime" => "SLOTTED", "runtime-impl" => "SLOTTED", "runtime-version" => "3.5", "version" => "CYPHER 3.5" }, "children" => [ %{ "args" => %{ "DbHits" => 0, "EstimatedRows" => 1.0, "PageCacheHitRatio" => 0.0, "PageCacheHits" => 0, "PageCacheMisses" => 0, "Rows" => 0 }, "children" => [ %{ "args" => %{ "DbHits" => 3, "EstimatedRows" => 1.0, "PageCacheHitRatio" => 0.0, "PageCacheHits" => 0, "PageCacheMisses" => 0, "Rows" => 1 }, "children" => [], "dbHits" => 3, "identifiers" => ["n"], "operatorType" => "Create", "pageCacheHitRatio" => 0.0, "pageCacheHits" => 0, "pageCacheMisses" => 0, "rows" => 1 } ], "dbHits" => 0, "identifiers" => ["n"], "operatorType" => "EmptyResult", "pageCacheHitRatio" => 0.0, "pageCacheHits" => 0, "pageCacheMisses" => 0, "rows" => 0 } ], "dbHits" => 0, "identifiers" => ["n"], "operatorType" => "ProduceResults", "pageCacheHitRatio" => 0.0, "pageCacheHits" => 0, "pageCacheMisses" => 0, "rows" => 0 }, "stats" => %{ "labels-added" => 1, "nodes-created" => 1, "properties-set" => 1 }, "t_last" => 0, "type" => "w" } ] @profile_results [ success: %{"fields" => ["num"], "t_first" => 1}, record: [1], success: %{ "bookmark" => "neo4j:bookmark:v1:tx48642", "profile" => %{ "args" => %{ "DbHits" => 0, "EstimatedRows" => 1.0, "PageCacheHitRatio" => 0.0, "PageCacheHits" => 0, "PageCacheMisses" => 0, "Rows" => 1, "Time" => 25980, "planner" => "COST", "planner-impl" => "IDP", "planner-version" => "3.5", "runtime" => "COMPILED", "runtime-impl" => "COMPILED", "runtime-version" => "3.5", "version" => "CYPHER 3.5" }, "children" => [ %{ "args" => %{ "DbHits" => 0, "EstimatedRows" => 1.0, "Expressions" => "{num : $` AUTOINT0`}", "PageCacheHitRatio" => 0.0, "PageCacheHits" => 0, "PageCacheMisses" => 0, "Rows" => 1, "Time" => 42285 }, "children" => [], "dbHits" => 0, "identifiers" => ["num"], "operatorType" => "Projection", "pageCacheHitRatio" => 0.0, "pageCacheHits" => 0, "pageCacheMisses" => 0, "rows" => 1 } ], "dbHits" => 0, "identifiers" => ["num"], "operatorType" => "ProduceResults", "pageCacheHitRatio" => 0.0, "pageCacheHits" => 0, "pageCacheMisses" => 0, "rows" => 1 }, "t_last" => 0, "type" => "r" } ] describe "Response as Enumerable" do test "a simple query" do conn = Bolt.Sips.conn() response = Bolt.Sips.query!(conn, "RETURN 300 AS r") assert %Response{results: [%{"r" => 300}]} = response assert response |> Enum.member?("r") assert 1 = response |> Enum.count() assert [%{"r" => 300}] = response |> Enum.take(1) assert %{"r" => 300} = response |> Response.first() end @unwind %Bolt.Sips.Response{ records: [[1], [2], [3], [4], [5], [6], '\a', '\b', '\t', '\n'], results: [ %{"n" => 1}, %{"n" => 2}, %{"n" => 3}, %{"n" => 4}, %{"n" => 5}, %{"n" => 6}, %{"n" => 7}, %{"n" => 8}, %{"n" => 9}, %{"n" => 10} ] } test "reduce: UNWIND range(1, 10) AS n RETURN n" do sum = Enum.reduce(@unwind, 0, &(&1["n"] + &2)) assert 55 == sum end test "slice: UNWIND range(1, 10) AS n RETURN n" do slice = Enum.slice(@unwind, 0..2) assert [%{"n" => 1}, %{"n" => 2}, %{"n" => 3}] == slice end end describe "Success" do test "with valid EXPLAIN" do assert %Response{ bookmark: nil, fields: ["n"], notifications: [], plan: %{ "args" => %{ "EstimatedRows" => 1.0, "planner" => "COST", "planner-impl" => "IDP", "planner-version" => "3.5", "runtime" => "INTERPRETED", "runtime-impl" => "INTERPRETED", "runtime-version" => "3.5", "version" => "CYPHER 3.5" }, "children" => [ %{ "args" => %{"EstimatedRows" => 1.0}, "children" => [], "identifiers" => ["n"], "operatorType" => "Create" } ], "identifiers" => ["n"], "operatorType" => "ProduceResults" }, profile: nil, records: [], results: [], stats: [], type: "rw" } = Response.transform!(@explain) end test "with Notifications" do %Response{notifications: [notifications | _rest]} = Response.transform!(@notifications) assert %{ "code" => "Neo.ClientNotification.Statement.CartesianProductWarning", "description" => "bad juju", "position" => %{"column" => 9, "line" => 1, "offset" => 8}, "severity" => "WARNING", "title" => "This query builds a cartesian product between disconnected patterns." } = notifications end test "with Profile (without results)" do %Response{plan: nil, profile: profile, stats: stats} = Response.transform!(@profile_no_results) assert %{ "args" => %{ "DbHits" => 0, "EstimatedRows" => 1.0, "PageCacheHitRatio" => 0.0, "PageCacheHits" => 0, "PageCacheMisses" => 0, "Rows" => 0, "planner" => "COST", "planner-impl" => "IDP", "planner-version" => "3.5", "runtime" => "SLOTTED", "runtime-impl" => "SLOTTED", "runtime-version" => "3.5", "version" => "CYPHER 3.5" }, "children" => [ %{ "args" => %{ "DbHits" => 0, "EstimatedRows" => 1.0, "PageCacheHitRatio" => 0.0, "PageCacheHits" => 0, "PageCacheMisses" => 0, "Rows" => 0 }, "children" => [ %{ "args" => %{ "DbHits" => 3, "EstimatedRows" => 1.0, "PageCacheHitRatio" => 0.0, "PageCacheHits" => 0, "PageCacheMisses" => 0, "Rows" => 1 }, "children" => [], "dbHits" => 3, "identifiers" => ["n"], "operatorType" => "Create", "pageCacheHitRatio" => 0.0, "pageCacheHits" => 0, "pageCacheMisses" => 0, "rows" => 1 } ], "dbHits" => 0, "identifiers" => ["n"], "operatorType" => "EmptyResult", "pageCacheHitRatio" => 0.0, "pageCacheHits" => 0, "pageCacheMisses" => 0, "rows" => 0 } ], "dbHits" => 0, "identifiers" => ["n"], "operatorType" => "ProduceResults", "pageCacheHitRatio" => 0.0, "pageCacheHits" => 0, "pageCacheMisses" => 0, "rows" => 0 } = profile assert %{ "labels-added" => 1, "nodes-created" => 1, "properties-set" => 1 } = stats end test "with Profile (with results)" do %Response{plan: nil, profile: profile, stats: [], records: _records, results: results} = Response.transform!(@profile_results) assert %{ "args" => %{ "DbHits" => 0, "EstimatedRows" => 1.0, "PageCacheHitRatio" => 0.0, "PageCacheHits" => 0, "PageCacheMisses" => 0, "Rows" => 1, "Time" => 25980, "planner" => "COST", "planner-impl" => "IDP", "planner-version" => "3.5", "runtime" => "COMPILED", "runtime-impl" => "COMPILED", "runtime-version" => "3.5", "version" => "CYPHER 3.5" }, "children" => [ %{ "args" => %{ "DbHits" => 0, "EstimatedRows" => 1.0, "Expressions" => "{num : $` AUTOINT0`}", "PageCacheHitRatio" => 0.0, "PageCacheHits" => 0, "PageCacheMisses" => 0, "Rows" => 1, "Time" => 42285 }, "children" => [], "dbHits" => 0, "identifiers" => ["num"], "operatorType" => "Projection", "pageCacheHitRatio" => 0.0, "pageCacheHits" => 0, "pageCacheMisses" => 0, "rows" => 1 } ], "dbHits" => 0, "identifiers" => ["num"], "operatorType" => "ProduceResults", "pageCacheHitRatio" => 0.0, "pageCacheHits" => 0, "pageCacheMisses" => 0, "rows" => 1 } = profile assert [%{"num" => 1}] = results end end end ================================================ FILE: test/router_test.exs ================================================ defmodule Bolt.Sips.Routing.RouterTest do use ExUnit.Case doctest Bolt.Sips.Router alias Bolt.Sips.Response # @routing_table %{ # "servers" => [ # %{"addresses" => ["localhost:7687"], "role" => "WRITE"}, # %{"addresses" => ["localhost:7688", "localhost:7689"], "role" => "READ"}, # %{ # "addresses" => ["localhost:7688", "localhost:7687", "localhost:7689"], # "role" => "ROUTE" # } # ], # "ttl" => 300 # } # @connections %{ # read: %{"localhost:7688" => 0, "localhost:7689" => 0}, # route: %{ # "localhost:7687" => 0, # "localhost:7688" => 0, # "localhost:7689" => 0 # }, # write: %{"localhost:7687" => 0}, # ttl: 300 # } @router_address "bolt+routing://localhost:7687?key=value,foo=bar;policy=EU" @bolt_sips_config [ url: @router_address, ssl: true ] @role_based_configuration [ url: "bolt://localhost", basic_auth: [username: "neo4j", password: "test"], pool_size: 10, max_overflow: 2, role: :zorba ] describe "Role based configuration" do test "context attributes for routed connections" do conf = Bolt.Sips.Utils.default_config(@bolt_sips_config) assert "bolt+routing" == conf[:schema] assert "key=value,foo=bar;policy=EU" == conf[:query] assert %{"key" => "value", "foo" => "bar", "policy" => "EU"} == conf[:routing_context] end test "user defined ad-hoc roles for standard (community) instances" do assert {:ok, _pid} = Bolt.Sips.start_link(@role_based_configuration) assert conn = Bolt.Sips.conn(@role_based_configuration[:role]) assert %Response{results: [%{"n" => 1}]} = Bolt.Sips.query!(conn, "RETURN 1 as n") end test "user defined ad-hoc roles can coexist, and act as distinct connection pools" do assert {:ok, pid1} = @role_based_configuration |> Keyword.put(:role, :alpha) |> Bolt.Sips.start_link() assert conn1 = Bolt.Sips.conn(:alpha) assert %Response{results: [%{"n" => 1}]} = Bolt.Sips.query!(conn1, "RETURN 1 as n") assert {:ok, pid2} = Bolt.Sips.start_link(@role_based_configuration) assert pid1 == pid2 assert conn2 = Bolt.Sips.conn(@role_based_configuration[:role]) refute conn1 == conn2 assert %Response{results: [%{"n" => 1}]} = Bolt.Sips.query!(conn2, "RETURN 1 as n") assert %{ default: %{ connections: %{ alpha: %{"localhost:7687" => 0}, direct: %{"localhost:7687" => 0}, routing_query: nil, zorba: %{"localhost:7687" => 0} } } } = Bolt.Sips.info() assert :ok == Bolt.Sips.terminate_connections(:alpha) assert_raise Bolt.Sips.Exception, "no connection exists with this role: alpha (prefix: default)", fn -> Bolt.Sips.conn(:alpha) end refute Map.has_key?(Bolt.Sips.info(), :alpha) end end end ================================================ FILE: test/routing/connections_test.exs ================================================ defmodule Bolt.Sips.Routing.ConnectionsTest do use ExUnit.Case, async: true @moduletag :routing alias Bolt.Sips.Router @current_connections %{ read: %{"localhost:7688" => 10, "localhost:7689" => 20}, route: %{ "localhost:7687" => 2, "localhost:7688" => 1, "localhost:7689" => 9 }, routing_query: %{ params: %{props: %{}}, query: "call dbms.cluster.routing.getRoutingTable($props)" }, ttl: 300, updated_at: 1_555_705_797, write: %{"localhost:7687" => 200, "localhost:7690" => 500} } @new_connections %{ read: %{"localhost:7688" => 0}, route: %{"localhost:7687" => 0}, write: %{"localhost:7689" => 0} } describe "Router" do test "connection information, after refresh" do assert %{ read: %{"localhost:7688" => 0}, route: %{"localhost:7687" => 0}, write: %{"localhost:7689" => 0}, routing_query: %{ params: %{props: %{}}, query: "call dbms.cluster.routing.getRoutingTable($props)" }, ttl: 300 } = Router.merge_connections_maps(@current_connections, @new_connections) end end end ================================================ FILE: test/routing/crud_test.exs ================================================ defmodule Bolt.Sips.Routing.CrudTest do use Bolt.Sips.RoutingConnCase @moduletag :routing alias Bolt.Sips describe "Basic Read/Write; " do test "read" do cypher = "return 10 as n" assert [%{"n" => 10}] == Sips.query!(Sips.conn(:read), cypher) end test "write" do conn = Sips.conn(:write) cypher = "CREATE (elf:Elf { name: $name, from: $from, klout: 99 })" assert %{ stats: %{ "labels-added" => 1, "nodes-created" => 1, "properties-set" => 3 }, type: "w" } == Sips.query!(conn, cypher, %{name: "Arameil", from: "Sweden"}) end # https://neo4j.com/docs/cypher-manual/current/clauses/set/#set-adding-properties-from-maps test "update" do create_cypher = "CREATE (p:Person { first: $person.first, last: $person.last })" update_cypher = """ MATCH (p:Person{ first: 'Green', last: 'Alien' }) SET p.first = { person }.first, p.last = $person.last RETURN p.first as first_name, p.last as last_name """ conn = Sips.conn(:write) assert %{ stats: %{ "labels-added" => 1, "nodes-created" => 1, "properties-set" => 2 }, type: "w" } == Sips.query!(conn, create_cypher, %{person: %{first: "Green", last: "Alien"}}) assert [%{"last_name" => "Alien"}] == Sips.query!( conn, "MATCH (p:Person { first: 'Green', last: 'Alien' }) RETURN p.last AS last_name" ) assert [%{"first_name" => "Florin", "last_name" => "Pătraşcu"}] == Sips.query!(conn, update_cypher, %{person: %{first: "Florin", last: "Pătraşcu"}}) end test "upsert" do # MERGE (p:Person{ first: { map }.name, last: { map }.last } # ON CREATE SET n = { map } # ON MATCH SET n += { map } end end end ================================================ FILE: test/routing/routing_table_parser_test.exs ================================================ defmodule Routing.Routing.TableParserTest do use ExUnit.Case, async: true @moduletag :routing alias Bolt.Sips.Routing.RoutingTable @valid_routing_table %{ "servers" => [ %{ "addresses" => ["127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"], "role" => "ROUTE" }, %{"addresses" => ["127.0.0.1:9004", "127.0.0.1:9005"], "role" => "READ"}, %{"addresses" => ["127.0.0.1:9006"], "role" => "WRITE"} ], "ttl" => 300 } @magic_routing_table %{ "servers" => [ %{ "addresses" => ["127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"], "role" => "ROUTE" }, %{"addresses" => ["127.0.0.1:9004", "127.0.0.1:9005"], "role" => "READ"}, %{"addresses" => ["127.0.0.1:9006"], "role" => "WRITE"}, %{"addresses" => ["127.0.0.1:9005"], "role" => "WARLOCK"}, %{"addresses" => ["127.0.0.1:9004"], "role" => "WIZARD"} ], "ttl" => 300 } describe "Routing table" do test "parse a valid server response having valid roles" do assert %Bolt.Sips.Routing.RoutingTable{ roles: %{ read: %{"127.0.0.1:9004" => 0, "127.0.0.1:9005" => 0}, route: %{ "127.0.0.1:9001" => 0, "127.0.0.1:9002" => 0, "127.0.0.1:9003" => 0 }, write: %{"127.0.0.1:9006" => 0} }, updated_at: route_updated_at, ttl: ttl } = RoutingTable.parse(@valid_routing_table) refute RoutingTable.ttl_expired?(route_updated_at, ttl) end test "parse a valid server response containing some Magic roles" do assert %Bolt.Sips.Routing.RoutingTable{ roles: %{ read: %{"127.0.0.1:9004" => 0, "127.0.0.1:9005" => 0}, route: %{ "127.0.0.1:9001" => 0, "127.0.0.1:9002" => 0, "127.0.0.1:9003" => 0 }, write: %{"127.0.0.1:9006" => 0} }, updated_at: route_updated_at, ttl: ttl } = RoutingTable.parse(@magic_routing_table) refute RoutingTable.ttl_expired?(route_updated_at, ttl) end end end ================================================ FILE: test/routing/routing_test.exs ================================================ defmodule Bolt.Sips.RoutingTest do @moduledoc """ """ use Bolt.Sips.BoltKitCase, async: false alias Bolt.Sips.Response @moduletag :boltkit @tag boltkit: %{ url: "neo4j://127.0.0.1:9001", scripts: [ {"test/scripts/non_router.script", 9001} ] } test "non_router.script", %{prefix: prefix} do assert %{error: error} = Bolt.Sips.routing_table(prefix) assert error =~ ~r/not a router/i end @tag boltkit: %{ url: "neo4j://127.0.0.1:9001", scripts: [ {"test/scripts/get_routing_table.script", 9001} ] } test "get_routing_table.script", %{prefix: prefix} do assert %{ read: %{"127.0.0.1:9002" => 0}, route: %{"127.0.0.1:9001" => 0, "127.0.0.1:9002" => 0}, write: %{"127.0.0.1:9001" => 0} } = Bolt.Sips.routing_table(prefix) assert %Bolt.Sips.Response{ results: [ %{"name" => "Alice"}, %{"name" => "Bob"}, %{"name" => "Eve"} ] } = Bolt.Sips.conn(:read, prefix: prefix) |> Bolt.Sips.query!("MATCH (n) RETURN n.name AS name") end @tag boltkit: %{ url: "neo4j://127.0.0.1:9001/?name=molly&age=1", scripts: [ {"test/scripts/get_routing_table_with_context.script", 9001}, {"test/scripts/return_x.bolt", 9002} ] } test "get_routing_table_with_context.script", %{prefix: prefix} do assert %{ read: %{"127.0.0.1:9002" => 0}, route: %{"127.0.0.1:9001" => 0, "127.0.0.1:9002" => 0}, write: %{"127.0.0.1:9001" => 0} } = Bolt.Sips.routing_table(prefix) Bolt.Sips.conn(:read, prefix: prefix) |> Bolt.Sips.query!("RETURN $x", %{x: 1}) end @tag boltkit: %{ url: "neo4j://127.0.0.1:9001", scripts: [ {"test/scripts/router.script", 9001}, {"test/scripts/create_a.script", 9006} ] } test "create_a.script", %{prefix: prefix} do assert %{write: %{"127.0.0.1:9006" => 0}} = Bolt.Sips.routing_table(prefix) assert %Response{results: []} = Bolt.Sips.conn(:write, prefix: prefix) |> Bolt.Sips.query!("CREATE (a $x)", %{x: %{name: "Alice"}}) end @tag boltkit: %{ url: "neo4j://127.0.0.1:9001", scripts: [ {"test/scripts/router.script", 9001}, {"test/scripts/return_1.script", 9004} ] } test "return_1.script", %{prefix: prefix} do assert %{read: %{"127.0.0.1:9004" => 0}} = Bolt.Sips.routing_table(prefix) assert %Response{results: [%{"x" => 1}]} = Bolt.Sips.conn(:read, prefix: prefix) |> Bolt.Sips.query!("RETURN $x", %{x: 1}) end @tag boltkit: %{ url: "neo4j://127.0.0.1:9001", scripts: [ {"test/scripts/router.script", 9001}, {"test/scripts/return_1_in_tx_twice.script", 9004}, {"test/scripts/return_1_in_tx_twice.script", 9005} ] } test "return_1_in_tx_twice.script", %{prefix: prefix} do Bolt.Sips.conn(:read, prefix: prefix) |> Bolt.Sips.transaction(fn conn -> assert %Response{fields: ["1"]} = Bolt.Sips.query!(conn, "RETURN 1") end) end @tag boltkit: %{ url: "neo4j://127.0.0.1:9001", scripts: [ {"test/scripts/router.script", 9001}, {"test/scripts/return_1_twice.script", 9004}, {"test/scripts/return_1_twice.script", 9005} ] } test "return_1_twice.script", %{prefix: prefix} do rconn1 = Bolt.Sips.conn(:read, prefix: prefix) rconn2 = Bolt.Sips.conn(:read, prefix: prefix) assert %Response{results: [%{"x" => 1}]} = Bolt.Sips.query!(rconn1, "RETURN $x", %{x: 1}) assert %Response{results: [%{"x" => 1}]} = Bolt.Sips.query!(rconn2, "RETURN $x", %{x: 1}) end @tag boltkit: %{ url: "neo4j://127.0.0.1:9001", scripts: [ {"test/scripts/router.script", 9001}, {"test/scripts/forbidden_on_read_only_database.script", 9006} ] } test "forbidden_on_read_only_database.script", %{prefix: prefix} do conn = Bolt.Sips.conn(:write, prefix: prefix) assert_raise Bolt.Sips.Exception, ~r/unable to write/i, fn -> Bolt.Sips.query!(conn, "CREATE (n {name:'Bob'})") end end end ================================================ FILE: test/routing/transaction_test.exs ================================================ defmodule Bolt.Sips.Routing.TransactionTest do use Bolt.Sips.RoutingConnCase @moduletag :routing setup do {:ok, [write_conn: Bolt.Sips.conn(:write)]} end test "execute statements in transaction", %{write_conn: write_conn} do Bolt.Sips.transaction(write_conn, fn conn -> book = Bolt.Sips.query!(conn, "CREATE (b:Book {title: \"The Game Of Trolls\"}) return b") |> List.first() assert %{"b" => g_o_t} = book assert g_o_t.properties["title"] == "The Game Of Trolls" Bolt.Sips.rollback(conn, :changed_my_mind) end) books = Bolt.Sips.query!(write_conn, "MATCH (b:Book {title: \"The Game Of Trolls\"}) return b") assert length(books) == 0 end ### ### NOTE: ### ### The labels used in these examples MUST be unique across all tests! ### These tests depend on being able to expect that a node either exists ### or does not, and asynchronous testing with the same names will cause ### random cases where the underlying state changes. ### test "rollback statements in transaction", %{write_conn: write_conn} do try do # In case there's already a copy in our DB, count them... {:ok, [result]} = Bolt.Sips.query(write_conn, "MATCH (x:XactRollback) RETURN count(x)") original_count = result["count(x)"] Bolt.Sips.transaction(write_conn, fn conn -> book = Bolt.Sips.query(conn, "CREATE (x:XactRollback {title:\"The Game Of Trolls\"}) return x") assert {:ok, [row]} = book assert row["x"].properties["title"] == "The Game Of Trolls" # Original connection (outside the transaction) should not see this node. {:ok, [result]} = Bolt.Sips.query(write_conn, "MATCH (x:XactRollback) RETURN count(x)") assert result["count(x)"] == original_count, "Main connection should not be able to see transactional change" Bolt.Sips.rollback(conn, :changed_my_mind) end) # Original connection should still not see this node committed. {:ok, [result]} = Bolt.Sips.query(write_conn, "MATCH (x:XactRollback) RETURN count(x)") assert result["count(x)"] == original_count after # Delete all XactRollback nodes in case the rollback() didn't work! Bolt.Sips.query(write_conn, "MATCH (x:XactRollback) DETACH DELETE x") end end test "commit statements in transaction", %{write_conn: write_conn} do try do Bolt.Sips.transaction(write_conn, fn conn -> book = Bolt.Sips.query(conn, "CREATE (x:XactCommit {foo: 'bar'}) return x") assert {:ok, [row]} = book assert row["x"].properties["foo"] == "bar" # Main connection should not see this new node. {:ok, results} = Bolt.Sips.query(write_conn, "MATCH (x:XactCommit) RETURN x") assert is_list(results) assert Enum.count(results) == 0, "Main connection should not be able to see transactional changes" end) # And we should see it now with the main connection. {:ok, [%{"x" => node}]} = Bolt.Sips.query(write_conn, "MATCH (x:XactCommit) RETURN x") assert node.labels == ["XactCommit"] assert node.properties["foo"] == "bar" after # Delete any XactCommit nodes that were succesfully committed! Bolt.Sips.query(write_conn, "MATCH (x:XactCommit) DETACH DELETE x") end end end ================================================ FILE: test/scripts/count.bolt ================================================ !: AUTO INIT !: AUTO RESET !: AUTO PULL_ALL C: RUN "UNWIND range(1, 10) AS n RETURN n" {} S: SUCCESS {"fields": ["n"]} C: PULL_ALL S: RECORD [1] RECORD [2] RECORD [3] RECORD [4] RECORD [5] RECORD [6] RECORD [7] RECORD [8] RECORD [9] RECORD [10] SUCCESS {"type": "r"} ================================================ FILE: test/scripts/create_a.script ================================================ !: AUTO INIT !: AUTO RESET C: RUN "CREATE (a $x)" {"x": {"name": "Alice"}} S: SUCCESS {"fields": []} SUCCESS {} ================================================ FILE: test/scripts/forbidden_on_read_only_database.script ================================================ !: AUTO INIT !: AUTO RESET !: AUTO DISCARD_ALL !: AUTO RUN "ROLLBACK" {} !: AUTO RUN "BEGIN" {} !: AUTO RUN "COMMIT" {} C: RUN "CREATE (n {name:'Bob'})" {} S: FAILURE {"code": "Neo.ClientError.General.ForbiddenOnReadOnlyDatabase", "message": "Unable to write"} S: IGNORED ================================================ FILE: test/scripts/get_routing_table.script ================================================ !: AUTO INIT !: AUTO RESET !: AUTO PULL_ALL S: SUCCESS {"server": "Neo4j/3.2.3"} C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {}} S: SUCCESS {"fields": ["ttl", "servers"]} RECORD [9223372036854775807, [{"addresses": ["127.0.0.1:9001", "127.0.0.1:9002"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9001", "127.0.0.1:9002"], "role": "READ"},{"addresses": ["127.0.0.1:9001", "127.0.0.1:9002"], "role": "ROUTE"}]] C: RUN "MATCH (n) RETURN n.name AS name" {} S: SUCCESS {"fields": ["name"]} RECORD ["Alice"] RECORD ["Bob"] RECORD ["Eve"] SUCCESS {} S: ================================================ FILE: test/scripts/get_routing_table_with_context.script ================================================ !: AUTO INIT !: AUTO RESET !: AUTO PULL_ALL S: SUCCESS {"server": "Neo4j/3.2.3"} C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {"name": "molly", "age": "1"}} S: SUCCESS {"fields": ["ttl", "servers"]} RECORD [9223372036854775807, [{"addresses": ["127.0.0.1:9001"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9002"], "role": "READ"},{"addresses": ["127.0.0.1:9001", "127.0.0.1:9002"], "role": "ROUTE"}]] SUCCESS {} ================================================ FILE: test/scripts/non_router.script ================================================ !: AUTO INIT !: AUTO RESET C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {}} S: FAILURE {"code": "Neo.ClientError.Procedure.ProcedureNotFound", "message": "Not a router"} IGNORED C: RESET S: SUCCESS {} ================================================ FILE: test/scripts/return_1.script ================================================ !: AUTO INIT !: AUTO RESET C: RUN "RETURN $x" {"x": 1} S: SUCCESS {"fields": ["x"]} RECORD [1] SUCCESS {} ================================================ FILE: test/scripts/return_1_in_tx_twice.script ================================================ !: AUTO INIT !: AUTO RESET C: RUN "BEGIN" {} S: SUCCESS {"fields": []} SUCCESS {} C: RUN "RETURN 1" {} S: SUCCESS {"fields": ["1"]} RECORD [1] SUCCESS {} C: RUN "COMMIT" {} DISCARD_ALL S: SUCCESS {"bookmark": "bookmark:1", "bookmarks": ["bookmark:1"]} SUCCESS {} C: RUN "BEGIN" {"bookmark": "bookmark:1", "bookmarks": ["bookmark:1"]} DISCARD_ALL S: SUCCESS {"fields": []} SUCCESS {} C: RUN "" {} S: SUCCESS {"fields": ["1"]} RECORD [1] SUCCESS {} C: RUN "COMMIT" {} DISCARD_ALL S: SUCCESS {"bookmark": "bookmark:2", "bookmarks": ["bookmark:2"]} SUCCESS {} ================================================ FILE: test/scripts/return_1_twice.script ================================================ !: AUTO INIT !: AUTO RESET C: RUN "RETURN $x" {"x": 1} S: SUCCESS {"fields": ["x"]} RECORD [1] SUCCESS {} C: RUN "RETURN $x" {"x": 1} S: SUCCESS {"fields": ["x"]} RECORD [1] SUCCESS {} ================================================ FILE: test/scripts/return_x.bolt ================================================ !: AUTO INIT !: AUTO RESET !: AUTO PULL_ALL C: RUN "RETURN $x" {"x": 1} S: SUCCESS {"fields": ["x"]} RECORD [1] SUCCESS {} ================================================ FILE: test/scripts/router.script ================================================ !: AUTO INIT !: AUTO RESET C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {}} S: SUCCESS {"fields": ["ttl", "servers"]} RECORD [300, [{"role":"ROUTE","addresses":["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"]},{"role":"READ","addresses":["127.0.0.1:9004","127.0.0.1:9005"]},{"role":"WRITE","addresses":["127.0.0.1:9006"]}]] SUCCESS {} ================================================ FILE: test/scripts/router_no_readers.script ================================================ !: AUTO INIT !: AUTO RESET C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {}} PULL_ALL S: SUCCESS {"fields": ["ttl", "servers"]} RECORD [300, [{"role":"ROUTE","addresses":["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"]},{"role":"READ","addresses":[]},{"role":"WRITE","addresses":["127.0.0.1:9006"]}]] SUCCESS {} ================================================ FILE: test/scripts/router_no_writers.script ================================================ !: AUTO INIT !: AUTO RESET C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {}} PULL_ALL S: SUCCESS {"fields": ["ttl", "servers"]} RECORD [300, [{"role":"ROUTE","addresses":["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"]},{"role":"READ","addresses":["127.0.0.1:9004","127.0.0.1:9005"]},{"role":"WRITE","addresses":[]}]] SUCCESS {} ================================================ FILE: test/support/boltkit_case.ex ================================================ defmodule Bolt.Sips.BoltKitCase do _doc = """ tag your tests with `boltkit`, like this: @tag boltkit: %{ url: "neo4j://127.0.0.1:9001/?name=molly&age=1", scripts: [ {"test/scripts/get_routing_table_with_context.script", 9001}, {"test/scripts/return_x.bolt", 9002} ], debug: true } and then use the prefix returned via the context, for working with the stubbed connection(s) test "get_routing_table_with_context.script", %{prefix: prefix} do assert ... end """ use ExUnit.CaseTemplate alias Porcelain.Process, as: Proc require Logger setup_all do Porcelain.reinit(Porcelain.Driver.Basic) end setup %{boltkit: boltkit} do prefix = Map.get(boltkit, :prefix, UUID.uuid4()) url = Map.get(boltkit, :url, "bolt://127.0.0.1") porcelains = stub_servers(boltkit) pid = with {:ok, pid} <- connect(url, prefix) do pid else _ -> raise RuntimeError, "cannot create a Bolt.Sips process" end on_exit(fn -> porcelains |> Enum.each(fn {:ok, porcelain} -> # wait for boltstub to finish :timer.sleep(150) with true <- Proc.alive?(porcelain), %Proc{out: out} <- porcelain do try do Enum.into(out, IO.stream(:stdio, :line)) rescue _ -> Logger.debug("BoltStub's out was flushed.") :rescued end else _e -> Logger.debug("BoltStub ended prematurely.") end e -> Logger.error(inspect(e)) end) end) {:ok, porcelains: porcelains, prefix: prefix, sips: pid, url: url} end defp stub_servers(%{scripts: scripts} = args) do opts = if Map.get(args, :debug, false) do [out: IO.stream(:stderr, :line)] else [] end scripts |> Enum.map(fn {script, port} -> with true <- File.exists?(script) do sport = Integer.to_string(port) porcelain = Porcelain.spawn("boltstub", [sport, script], opts) wait_for_socket('127.0.0.1', port) {:ok, porcelain} else _ -> {:error, script <> ", not found."} end end) end @sock_opts [:binary, active: false] defp wait_for_socket(address, port) do with {:ok, socket} <- :gen_tcp.connect(address, port, @sock_opts, 1000) do socket end end defp connect(url, prefix) do conf = [ url: url, basic_auth: [username: "neo4j", password: "password"], # pool: DBConnection.Ownership, pool_size: 1, prefix: prefix # after_connect_timeout: fn _ -> nil end, # queue_timeout: 100, # queue_target: 100, # queue_interval: 10 ] Logger.debug("creating #{url}, prefix: #{prefix}") Bolt.Sips.start_link(conf) end end ================================================ FILE: test/support/conn_case.ex ================================================ defmodule Bolt.Sips.ConnCase do use ExUnit.CaseTemplate setup_all do Bolt.Sips.start_link(Application.get_env(:bolt_sips, Bolt)) conn = Bolt.Sips.conn() on_exit(fn -> Bolt.Sips.Test.Support.Database.clear(conn) end) {:ok, conn: conn} end end ================================================ FILE: test/support/conn_routing_case.ex ================================================ defmodule Bolt.Sips.RoutingConnCase do @moduletag :routing use ExUnit.CaseTemplate alias Bolt.Sips @routing_connection_config [ url: "bolt+routing://localhost:9001", basic_auth: [username: "neo4j", password: "test"], pool_size: 10, max_overflow: 2, queue_interval: 500, queue_target: 1500, tag: @moduletag ] setup_all do {:ok, _pid} = Sips.start_link(@routing_connection_config) conn = Sips.conn(:write) on_exit(fn -> with conn when not is_nil(conn) <- Sips.conn(:write) do Sips.Test.Support.Database.clear(conn) else e -> {:error, e} end end) {:ok, write_conn: conn} end end ================================================ FILE: test/support/database.ex ================================================ defmodule Bolt.Sips.Test.Support.Database do def clear(conn) do Bolt.Sips.query!(conn, "MATCH (n) DETACH DELETE n") end end ================================================ FILE: test/support/fixture.ex ================================================ defmodule Bolt.Sips.Fixture do def create_graph(conn, :movie) do Bolt.Sips.query!(conn, movie_cypher()) end def create_graph(conn, :bolt_sips) do Bolt.Sips.query!(conn, bolt_sips_cypher()) end def bolt_sips_cypher() do """ MATCH (n {bolt_sips: true}) OPTIONAL MATCH (n)-[r]-() DELETE n,r; CREATE (BoltSips:BoltSips {title:'Elixir sipping from Neo4j, using Bolt', released:2016, license:'MIT', bolt_sips: true}) CREATE (TNOTW:Book {title:'The Name of the Wind', released:2007, genre:'fantasy', bolt_sips: true}) CREATE (Patrick:Person {name:'Patrick Rothfuss', bolt_sips: true}) CREATE (Kvothe:Person {name:'Kote', bolt_sips: true}) CREATE (Denna:Person {name:'Denna', bolt_sips: true}) CREATE (Chandrian:Deamon {name:'Chandrian', bolt_sips: true}) CREATE (Kvothe)-[:ACTED_IN {roles:['sword fighter', 'magician', 'musician']}]->(TNOTW), (Denna)-[:ACTED_IN {roles:['many talents']}]->(TNOTW), (Chandrian)-[:ACTED_IN {roles:['killer']}]->(TNOTW), (Patrick)-[:WROTE]->(TNOTW) """ end def movie_cypher() do """ CREATE (TheMatrix:Movie {title:'The Matrix', released:1999, tagline:'Welcome to the Real World'}) CREATE (Keanu:Person {name:'Keanu Reeves', born:1964}) CREATE (Carrie:Person {name:'Carrie-Anne Moss', born:1967}) CREATE (Laurence:Person {name:'Laurence Fishburne', born:1961}) CREATE (Hugo:Person {name:'Hugo Weaving', born:1960}) CREATE (LillyW:Person {name:'Lilly Wachowski', born:1967}) CREATE (LanaW:Person {name:'Lana Wachowski', born:1965}) CREATE (JoelS:Person {name:'Joel Silver', born:1952}) CREATE (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrix), (Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrix), (Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrix), (Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrix), (LillyW)-[:DIRECTED]->(TheMatrix), (LanaW)-[:DIRECTED]->(TheMatrix), (JoelS)-[:PRODUCED]->(TheMatrix) CREATE (Emil:Person {name:"Emil Eifrem", born:1978}) CREATE (Emil)-[:ACTED_IN {roles:["Emil"]}]->(TheMatrix) CREATE (TheMatrixReloaded:Movie {title:'The Matrix Reloaded', released:2003, tagline:'Free your mind'}) CREATE (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixReloaded), (Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixReloaded), (Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixReloaded), (Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixReloaded), (LillyW)-[:DIRECTED]->(TheMatrixReloaded), (LanaW)-[:DIRECTED]->(TheMatrixReloaded), (JoelS)-[:PRODUCED]->(TheMatrixReloaded) CREATE (TheMatrixRevolutions:Movie {title:'The Matrix Revolutions', released:2003, tagline:'Everything that has a beginning has an end'}) CREATE (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixRevolutions), (Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixRevolutions), (Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixRevolutions), (Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixRevolutions), (LillyW)-[:DIRECTED]->(TheMatrixRevolutions), (LanaW)-[:DIRECTED]->(TheMatrixRevolutions), (JoelS)-[:PRODUCED]->(TheMatrixRevolutions) CREATE (TheDevilsAdvocate:Movie {title:"The Devil's Advocate", released:1997, tagline:'Evil has its winning ways'}) CREATE (Charlize:Person {name:'Charlize Theron', born:1975}) CREATE (Al:Person {name:'Al Pacino', born:1940}) CREATE (Taylor:Person {name:'Taylor Hackford', born:1944}) CREATE (Keanu)-[:ACTED_IN {roles:['Kevin Lomax']}]->(TheDevilsAdvocate), (Charlize)-[:ACTED_IN {roles:['Mary Ann Lomax']}]->(TheDevilsAdvocate), (Al)-[:ACTED_IN {roles:['John Milton']}]->(TheDevilsAdvocate), (Taylor)-[:DIRECTED]->(TheDevilsAdvocate) CREATE (AFewGoodMen:Movie {title:"A Few Good Men", released:1992, tagline:"In the heart of the nation's capital, in a courthouse of the U.S. government, one man will stop at nothing to keep his honor, and one will stop at nothing to find the truth."}) CREATE (TomC:Person {name:'Tom Cruise', born:1962}) CREATE (JackN:Person {name:'Jack Nicholson', born:1937}) CREATE (DemiM:Person {name:'Demi Moore', born:1962}) CREATE (KevinB:Person {name:'Kevin Bacon', born:1958}) CREATE (KieferS:Person {name:'Kiefer Sutherland', born:1966}) CREATE (NoahW:Person {name:'Noah Wyle', born:1971}) CREATE (CubaG:Person {name:'Cuba Gooding Jr.', born:1968}) CREATE (KevinP:Person {name:'Kevin Pollak', born:1957}) CREATE (JTW:Person {name:'J.T. Walsh', born:1943}) CREATE (JamesM:Person {name:'James Marshall', born:1967}) CREATE (ChristopherG:Person {name:'Christopher Guest', born:1948}) CREATE (RobR:Person {name:'Rob Reiner', born:1947}) CREATE (AaronS:Person {name:'Aaron Sorkin', born:1961}) CREATE (TomC)-[:ACTED_IN {roles:['Lt. Daniel Kaffee']}]->(AFewGoodMen), (JackN)-[:ACTED_IN {roles:['Col. Nathan R. Jessup']}]->(AFewGoodMen), (DemiM)-[:ACTED_IN {roles:['Lt. Cdr. JoAnne Galloway']}]->(AFewGoodMen), (KevinB)-[:ACTED_IN {roles:['Capt. Jack Ross']}]->(AFewGoodMen), (KieferS)-[:ACTED_IN {roles:['Lt. Jonathan Kendrick']}]->(AFewGoodMen), (NoahW)-[:ACTED_IN {roles:['Cpl. Jeffrey Barnes']}]->(AFewGoodMen), (CubaG)-[:ACTED_IN {roles:['Cpl. Carl Hammaker']}]->(AFewGoodMen), (KevinP)-[:ACTED_IN {roles:['Lt. Sam Weinberg']}]->(AFewGoodMen), (JTW)-[:ACTED_IN {roles:['Lt. Col. Matthew Andrew Markinson']}]->(AFewGoodMen), (JamesM)-[:ACTED_IN {roles:['Pfc. Louden Downey']}]->(AFewGoodMen), (ChristopherG)-[:ACTED_IN {roles:['Dr. Stone']}]->(AFewGoodMen), (AaronS)-[:ACTED_IN {roles:['Man in Bar']}]->(AFewGoodMen), (RobR)-[:DIRECTED]->(AFewGoodMen), (AaronS)-[:WROTE]->(AFewGoodMen) CREATE (TopGun:Movie {title:"Top Gun", released:1986, tagline:'I feel the need, the need for speed.'}) CREATE (KellyM:Person {name:'Kelly McGillis', born:1957}) CREATE (ValK:Person {name:'Val Kilmer', born:1959}) CREATE (AnthonyE:Person {name:'Anthony Edwards', born:1962}) CREATE (TomS:Person {name:'Tom Skerritt', born:1933}) CREATE (MegR:Person {name:'Meg Ryan', born:1961}) CREATE (TonyS:Person {name:'Tony Scott', born:1944}) CREATE (JimC:Person {name:'Jim Cash', born:1941}) CREATE (TomC)-[:ACTED_IN {roles:['Maverick']}]->(TopGun), (KellyM)-[:ACTED_IN {roles:['Charlie']}]->(TopGun), (ValK)-[:ACTED_IN {roles:['Iceman']}]->(TopGun), (AnthonyE)-[:ACTED_IN {roles:['Goose']}]->(TopGun), (TomS)-[:ACTED_IN {roles:['Viper']}]->(TopGun), (MegR)-[:ACTED_IN {roles:['Carole']}]->(TopGun), (TonyS)-[:DIRECTED]->(TopGun), (JimC)-[:WROTE]->(TopGun) CREATE (JerryMaguire:Movie {title:'Jerry Maguire', released:2000, tagline:'The rest of his life begins now.'}) CREATE (ReneeZ:Person {name:'Renee Zellweger', born:1969}) CREATE (KellyP:Person {name:'Kelly Preston', born:1962}) CREATE (JerryO:Person {name:"Jerry O'Connell", born:1974}) CREATE (JayM:Person {name:'Jay Mohr', born:1970}) CREATE (BonnieH:Person {name:'Bonnie Hunt', born:1961}) CREATE (ReginaK:Person {name:'Regina King', born:1971}) CREATE (JonathanL:Person {name:'Jonathan Lipnicki', born:1996}) CREATE (CameronC:Person {name:'Cameron Crowe', born:1957}) CREATE (TomC)-[:ACTED_IN {roles:['Jerry Maguire']}]->(JerryMaguire), (CubaG)-[:ACTED_IN {roles:['Rod Tidwell']}]->(JerryMaguire), (ReneeZ)-[:ACTED_IN {roles:['Dorothy Boyd']}]->(JerryMaguire), (KellyP)-[:ACTED_IN {roles:['Avery Bishop']}]->(JerryMaguire), (JerryO)-[:ACTED_IN {roles:['Frank Cushman']}]->(JerryMaguire), (JayM)-[:ACTED_IN {roles:['Bob Sugar']}]->(JerryMaguire), (BonnieH)-[:ACTED_IN {roles:['Laurel Boyd']}]->(JerryMaguire), (ReginaK)-[:ACTED_IN {roles:['Marcee Tidwell']}]->(JerryMaguire), (JonathanL)-[:ACTED_IN {roles:['Ray Boyd']}]->(JerryMaguire), (CameronC)-[:DIRECTED]->(JerryMaguire), (CameronC)-[:PRODUCED]->(JerryMaguire), (CameronC)-[:WROTE]->(JerryMaguire) CREATE (StandByMe:Movie {title:"Stand By Me", released:1986, tagline:"For some, it's the last real taste of innocence, and the first real taste of life. But for everyone, it's the time that memories are made of."}) CREATE (RiverP:Person {name:'River Phoenix', born:1970}) CREATE (CoreyF:Person {name:'Corey Feldman', born:1971}) CREATE (WilW:Person {name:'Wil Wheaton', born:1972}) CREATE (JohnC:Person {name:'John Cusack', born:1966}) CREATE (MarshallB:Person {name:'Marshall Bell', born:1942}) CREATE (WilW)-[:ACTED_IN {roles:['Gordie Lachance']}]->(StandByMe), (RiverP)-[:ACTED_IN {roles:['Chris Chambers']}]->(StandByMe), (JerryO)-[:ACTED_IN {roles:['Vern Tessio']}]->(StandByMe), (CoreyF)-[:ACTED_IN {roles:['Teddy Duchamp']}]->(StandByMe), (JohnC)-[:ACTED_IN {roles:['Denny Lachance']}]->(StandByMe), (KieferS)-[:ACTED_IN {roles:['Ace Merrill']}]->(StandByMe), (MarshallB)-[:ACTED_IN {roles:['Mr. Lachance']}]->(StandByMe), (RobR)-[:DIRECTED]->(StandByMe) CREATE (AsGoodAsItGets:Movie {title:'As Good as It Gets', released:1997, tagline:'A comedy from the heart that goes for the throat.'}) CREATE (HelenH:Person {name:'Helen Hunt', born:1963}) CREATE (GregK:Person {name:'Greg Kinnear', born:1963}) CREATE (JamesB:Person {name:'James L. Brooks', born:1940}) CREATE (JackN)-[:ACTED_IN {roles:['Melvin Udall']}]->(AsGoodAsItGets), (HelenH)-[:ACTED_IN {roles:['Carol Connelly']}]->(AsGoodAsItGets), (GregK)-[:ACTED_IN {roles:['Simon Bishop']}]->(AsGoodAsItGets), (CubaG)-[:ACTED_IN {roles:['Frank Sachs']}]->(AsGoodAsItGets), (JamesB)-[:DIRECTED]->(AsGoodAsItGets) CREATE (WhatDreamsMayCome:Movie {title:'What Dreams May Come', released:1998, tagline:'After life there is more. The end is just the beginning.'}) CREATE (AnnabellaS:Person {name:'Annabella Sciorra', born:1960}) CREATE (MaxS:Person {name:'Max von Sydow', born:1929}) CREATE (WernerH:Person {name:'Werner Herzog', born:1942}) CREATE (Robin:Person {name:'Robin Williams', born:1951}) CREATE (VincentW:Person {name:'Vincent Ward', born:1956}) CREATE (Robin)-[:ACTED_IN {roles:['Chris Nielsen']}]->(WhatDreamsMayCome), (CubaG)-[:ACTED_IN {roles:['Albert Lewis']}]->(WhatDreamsMayCome), (AnnabellaS)-[:ACTED_IN {roles:['Annie Collins-Nielsen']}]->(WhatDreamsMayCome), (MaxS)-[:ACTED_IN {roles:['The Tracker']}]->(WhatDreamsMayCome), (WernerH)-[:ACTED_IN {roles:['The Face']}]->(WhatDreamsMayCome), (VincentW)-[:DIRECTED]->(WhatDreamsMayCome) CREATE (SnowFallingonCedars:Movie {title:'Snow Falling on Cedars', released:1999, tagline:'First loves last. Forever.'}) CREATE (EthanH:Person {name:'Ethan Hawke', born:1970}) CREATE (RickY:Person {name:'Rick Yune', born:1971}) CREATE (JamesC:Person {name:'James Cromwell', born:1940}) CREATE (ScottH:Person {name:'Scott Hicks', born:1953}) CREATE (EthanH)-[:ACTED_IN {roles:['Ishmael Chambers']}]->(SnowFallingonCedars), (RickY)-[:ACTED_IN {roles:['Kazuo Miyamoto']}]->(SnowFallingonCedars), (MaxS)-[:ACTED_IN {roles:['Nels Gudmundsson']}]->(SnowFallingonCedars), (JamesC)-[:ACTED_IN {roles:['Judge Fielding']}]->(SnowFallingonCedars), (ScottH)-[:DIRECTED]->(SnowFallingonCedars) CREATE (YouveGotMail:Movie {title:"You've Got Mail", released:1998, tagline:'At odds in life... in love on-line.'}) CREATE (ParkerP:Person {name:'Parker Posey', born:1968}) CREATE (DaveC:Person {name:'Dave Chappelle', born:1973}) CREATE (SteveZ:Person {name:'Steve Zahn', born:1967}) CREATE (TomH:Person {name:'Tom Hanks', born:1956}) CREATE (NoraE:Person {name:'Nora Ephron', born:1941}) CREATE (TomH)-[:ACTED_IN {roles:['Joe Fox']}]->(YouveGotMail), (MegR)-[:ACTED_IN {roles:['Kathleen Kelly']}]->(YouveGotMail), (GregK)-[:ACTED_IN {roles:['Frank Navasky']}]->(YouveGotMail), (ParkerP)-[:ACTED_IN {roles:['Patricia Eden']}]->(YouveGotMail), (DaveC)-[:ACTED_IN {roles:['Kevin Jackson']}]->(YouveGotMail), (SteveZ)-[:ACTED_IN {roles:['George Pappas']}]->(YouveGotMail), (NoraE)-[:DIRECTED]->(YouveGotMail) CREATE (SleeplessInSeattle:Movie {title:'Sleepless in Seattle', released:1993, tagline:'What if someone you never met, someone you never saw, someone you never knew was the only someone for you?'}) CREATE (RitaW:Person {name:'Rita Wilson', born:1956}) CREATE (BillPull:Person {name:'Bill Pullman', born:1953}) CREATE (VictorG:Person {name:'Victor Garber', born:1949}) CREATE (RosieO:Person {name:"Rosie O'Donnell", born:1962}) CREATE (TomH)-[:ACTED_IN {roles:['Sam Baldwin']}]->(SleeplessInSeattle), (MegR)-[:ACTED_IN {roles:['Annie Reed']}]->(SleeplessInSeattle), (RitaW)-[:ACTED_IN {roles:['Suzy']}]->(SleeplessInSeattle), (BillPull)-[:ACTED_IN {roles:['Walter']}]->(SleeplessInSeattle), (VictorG)-[:ACTED_IN {roles:['Greg']}]->(SleeplessInSeattle), (RosieO)-[:ACTED_IN {roles:['Becky']}]->(SleeplessInSeattle), (NoraE)-[:DIRECTED]->(SleeplessInSeattle) CREATE (JoeVersustheVolcano:Movie {title:'Joe Versus the Volcano', released:1990, tagline:'A story of love, lava and burning desire.'}) CREATE (JohnS:Person {name:'John Patrick Stanley', born:1950}) CREATE (Nathan:Person {name:'Nathan Lane', born:1956}) CREATE (TomH)-[:ACTED_IN {roles:['Joe Banks']}]->(JoeVersustheVolcano), (MegR)-[:ACTED_IN {roles:['DeDe', 'Angelica Graynamore', 'Patricia Graynamore']}]->(JoeVersustheVolcano), (Nathan)-[:ACTED_IN {roles:['Baw']}]->(JoeVersustheVolcano), (JohnS)-[:DIRECTED]->(JoeVersustheVolcano) CREATE (WhenHarryMetSally:Movie {title:'When Harry Met Sally', released:1998, tagline:'Can two friends sleep together and still love each other in the morning?'}) CREATE (BillyC:Person {name:'Billy Crystal', born:1948}) CREATE (CarrieF:Person {name:'Carrie Fisher', born:1956}) CREATE (BrunoK:Person {name:'Bruno Kirby', born:1949}) CREATE (BillyC)-[:ACTED_IN {roles:['Harry Burns']}]->(WhenHarryMetSally), (MegR)-[:ACTED_IN {roles:['Sally Albright']}]->(WhenHarryMetSally), (CarrieF)-[:ACTED_IN {roles:['Marie']}]->(WhenHarryMetSally), (BrunoK)-[:ACTED_IN {roles:['Jess']}]->(WhenHarryMetSally), (RobR)-[:DIRECTED]->(WhenHarryMetSally), (RobR)-[:PRODUCED]->(WhenHarryMetSally), (NoraE)-[:PRODUCED]->(WhenHarryMetSally), (NoraE)-[:WROTE]->(WhenHarryMetSally) CREATE (ThatThingYouDo:Movie {title:'That Thing You Do', released:1996, tagline:'In every life there comes a time when that thing you dream becomes that thing you do'}) CREATE (LivT:Person {name:'Liv Tyler', born:1977}) CREATE (TomH)-[:ACTED_IN {roles:['Mr. White']}]->(ThatThingYouDo), (LivT)-[:ACTED_IN {roles:['Faye Dolan']}]->(ThatThingYouDo), (Charlize)-[:ACTED_IN {roles:['Tina']}]->(ThatThingYouDo), (TomH)-[:DIRECTED]->(ThatThingYouDo) CREATE (TheReplacements:Movie {title:'The Replacements', released:2000, tagline:'Pain heals, Chicks dig scars... Glory lasts forever'}) CREATE (Brooke:Person {name:'Brooke Langton', born:1970}) CREATE (Gene:Person {name:'Gene Hackman', born:1930}) CREATE (Orlando:Person {name:'Orlando Jones', born:1968}) CREATE (Howard:Person {name:'Howard Deutch', born:1950}) CREATE (Keanu)-[:ACTED_IN {roles:['Shane Falco']}]->(TheReplacements), (Brooke)-[:ACTED_IN {roles:['Annabelle Farrell']}]->(TheReplacements), (Gene)-[:ACTED_IN {roles:['Jimmy McGinty']}]->(TheReplacements), (Orlando)-[:ACTED_IN {roles:['Clifford Franklin']}]->(TheReplacements), (Howard)-[:DIRECTED]->(TheReplacements) CREATE (RescueDawn:Movie {title:'RescueDawn', released:2006, tagline:"Based on the extraordinary true story of one man's fight for freedom"}) CREATE (ChristianB:Person {name:'Christian Bale', born:1974}) CREATE (ZachG:Person {name:'Zach Grenier', born:1954}) CREATE (MarshallB)-[:ACTED_IN {roles:['Admiral']}]->(RescueDawn), (ChristianB)-[:ACTED_IN {roles:['Dieter Dengler']}]->(RescueDawn), (ZachG)-[:ACTED_IN {roles:['Squad Leader']}]->(RescueDawn), (SteveZ)-[:ACTED_IN {roles:['Duane']}]->(RescueDawn), (WernerH)-[:DIRECTED]->(RescueDawn) CREATE (TheBirdcage:Movie {title:'The Birdcage', released:1996, tagline:'Come as you are'}) CREATE (MikeN:Person {name:'Mike Nichols', born:1931}) CREATE (Robin)-[:ACTED_IN {roles:['Armand Goldman']}]->(TheBirdcage), (Nathan)-[:ACTED_IN {roles:['Albert Goldman']}]->(TheBirdcage), (Gene)-[:ACTED_IN {roles:['Sen. Kevin Keeley']}]->(TheBirdcage), (MikeN)-[:DIRECTED]->(TheBirdcage) CREATE (Unforgiven:Movie {title:'Unforgiven', released:1992, tagline:"It's a hell of a thing, killing a man"}) CREATE (RichardH:Person {name:'Richard Harris', born:1930}) CREATE (ClintE:Person {name:'Clint Eastwood', born:1930}) CREATE (RichardH)-[:ACTED_IN {roles:['English Bob']}]->(Unforgiven), (ClintE)-[:ACTED_IN {roles:['Bill Munny']}]->(Unforgiven), (Gene)-[:ACTED_IN {roles:['Little Bill Daggett']}]->(Unforgiven), (ClintE)-[:DIRECTED]->(Unforgiven) CREATE (JohnnyMnemonic:Movie {title:'Johnny Mnemonic', released:1995, tagline:'The hottest data on earth. In the coolest head in town'}) CREATE (Takeshi:Person {name:'Takeshi Kitano', born:1947}) CREATE (Dina:Person {name:'Dina Meyer', born:1968}) CREATE (IceT:Person {name:'Ice-T', born:1958}) CREATE (RobertL:Person {name:'Robert Longo', born:1953}) CREATE (Keanu)-[:ACTED_IN {roles:['Johnny Mnemonic']}]->(JohnnyMnemonic), (Takeshi)-[:ACTED_IN {roles:['Takahashi']}]->(JohnnyMnemonic), (Dina)-[:ACTED_IN {roles:['Jane']}]->(JohnnyMnemonic), (IceT)-[:ACTED_IN {roles:['J-Bone']}]->(JohnnyMnemonic), (RobertL)-[:DIRECTED]->(JohnnyMnemonic) CREATE (CloudAtlas:Movie {title:'Cloud Atlas', released:2012, tagline:'Everything is connected'}) CREATE (HalleB:Person {name:'Halle Berry', born:1966}) CREATE (JimB:Person {name:'Jim Broadbent', born:1949}) CREATE (TomT:Person {name:'Tom Tykwer', born:1965}) CREATE (DavidMitchell:Person {name:'David Mitchell', born:1969}) CREATE (StefanArndt:Person {name:'Stefan Arndt', born:1961}) CREATE (TomH)-[:ACTED_IN {roles:['Zachry', 'Dr. Henry Goose', 'Isaac Sachs', 'Dermot Hoggins']}]->(CloudAtlas), (Hugo)-[:ACTED_IN {roles:['Bill Smoke', 'Haskell Moore', 'Tadeusz Kesselring', 'Nurse Noakes', 'Boardman Mephi', 'Old Georgie']}]->(CloudAtlas), (HalleB)-[:ACTED_IN {roles:['Luisa Rey', 'Jocasta Ayrs', 'Ovid', 'Meronym']}]->(CloudAtlas), (JimB)-[:ACTED_IN {roles:['Vyvyan Ayrs', 'Captain Molyneux', 'Timothy Cavendish']}]->(CloudAtlas), (TomT)-[:DIRECTED]->(CloudAtlas), (LillyW)-[:DIRECTED]->(CloudAtlas), (LanaW)-[:DIRECTED]->(CloudAtlas), (DavidMitchell)-[:WROTE]->(CloudAtlas), (StefanArndt)-[:PRODUCED]->(CloudAtlas) CREATE (TheDaVinciCode:Movie {title:'The Da Vinci Code', released:2006, tagline:'Break The Codes'}) CREATE (IanM:Person {name:'Ian McKellen', born:1939}) CREATE (AudreyT:Person {name:'Audrey Tautou', born:1976}) CREATE (PaulB:Person {name:'Paul Bettany', born:1971}) CREATE (RonH:Person {name:'Ron Howard', born:1954}) CREATE (TomH)-[:ACTED_IN {roles:['Dr. Robert Langdon']}]->(TheDaVinciCode), (IanM)-[:ACTED_IN {roles:['Sir Leight Teabing']}]->(TheDaVinciCode), (AudreyT)-[:ACTED_IN {roles:['Sophie Neveu']}]->(TheDaVinciCode), (PaulB)-[:ACTED_IN {roles:['Silas']}]->(TheDaVinciCode), (RonH)-[:DIRECTED]->(TheDaVinciCode) CREATE (VforVendetta:Movie {title:'V for Vendetta', released:2006, tagline:'Freedom! Forever!'}) CREATE (NatalieP:Person {name:'Natalie Portman', born:1981}) CREATE (StephenR:Person {name:'Stephen Rea', born:1946}) CREATE (JohnH:Person {name:'John Hurt', born:1940}) CREATE (BenM:Person {name: 'Ben Miles', born:1967}) CREATE (Hugo)-[:ACTED_IN {roles:['V']}]->(VforVendetta), (NatalieP)-[:ACTED_IN {roles:['Evey Hammond']}]->(VforVendetta), (StephenR)-[:ACTED_IN {roles:['Eric Finch']}]->(VforVendetta), (JohnH)-[:ACTED_IN {roles:['High Chancellor Adam Sutler']}]->(VforVendetta), (BenM)-[:ACTED_IN {roles:['Dascomb']}]->(VforVendetta), (JamesM)-[:DIRECTED]->(VforVendetta), (LillyW)-[:PRODUCED]->(VforVendetta), (LanaW)-[:PRODUCED]->(VforVendetta), (JoelS)-[:PRODUCED]->(VforVendetta), (LillyW)-[:WROTE]->(VforVendetta), (LanaW)-[:WROTE]->(VforVendetta) CREATE (SpeedRacer:Movie {title:'Speed Racer', released:2008, tagline:'Speed has no limits'}) CREATE (EmileH:Person {name:'Emile Hirsch', born:1985}) CREATE (JohnG:Person {name:'John Goodman', born:1960}) CREATE (SusanS:Person {name:'Susan Sarandon', born:1946}) CREATE (MatthewF:Person {name:'Matthew Fox', born:1966}) CREATE (ChristinaR:Person {name:'Christina Ricci', born:1980}) CREATE (Rain:Person {name:'Rain', born:1982}) CREATE (EmileH)-[:ACTED_IN {roles:['Speed Racer']}]->(SpeedRacer), (JohnG)-[:ACTED_IN {roles:['Pops']}]->(SpeedRacer), (SusanS)-[:ACTED_IN {roles:['Mom']}]->(SpeedRacer), (MatthewF)-[:ACTED_IN {roles:['Racer X']}]->(SpeedRacer), (ChristinaR)-[:ACTED_IN {roles:['Trixie']}]->(SpeedRacer), (Rain)-[:ACTED_IN {roles:['Taejo Togokahn']}]->(SpeedRacer), (BenM)-[:ACTED_IN {roles:['Cass Jones']}]->(SpeedRacer), (LillyW)-[:DIRECTED]->(SpeedRacer), (LanaW)-[:DIRECTED]->(SpeedRacer), (LillyW)-[:WROTE]->(SpeedRacer), (LanaW)-[:WROTE]->(SpeedRacer), (JoelS)-[:PRODUCED]->(SpeedRacer) CREATE (NinjaAssassin:Movie {title:'Ninja Assassin', released:2009, tagline:'Prepare to enter a secret world of assassins'}) CREATE (NaomieH:Person {name:'Naomie Harris'}) CREATE (Rain)-[:ACTED_IN {roles:['Raizo']}]->(NinjaAssassin), (NaomieH)-[:ACTED_IN {roles:['Mika Coretti']}]->(NinjaAssassin), (RickY)-[:ACTED_IN {roles:['Takeshi']}]->(NinjaAssassin), (BenM)-[:ACTED_IN {roles:['Ryan Maslow']}]->(NinjaAssassin), (JamesM)-[:DIRECTED]->(NinjaAssassin), (LillyW)-[:PRODUCED]->(NinjaAssassin), (LanaW)-[:PRODUCED]->(NinjaAssassin), (JoelS)-[:PRODUCED]->(NinjaAssassin) CREATE (TheGreenMile:Movie {title:'The Green Mile', released:1999, tagline:"Walk a mile you'll never forget."}) CREATE (MichaelD:Person {name:'Michael Clarke Duncan', born:1957}) CREATE (DavidM:Person {name:'David Morse', born:1953}) CREATE (SamR:Person {name:'Sam Rockwell', born:1968}) CREATE (GaryS:Person {name:'Gary Sinise', born:1955}) CREATE (PatriciaC:Person {name:'Patricia Clarkson', born:1959}) CREATE (FrankD:Person {name:'Frank Darabont', born:1959}) CREATE (TomH)-[:ACTED_IN {roles:['Paul Edgecomb']}]->(TheGreenMile), (MichaelD)-[:ACTED_IN {roles:['John Coffey']}]->(TheGreenMile), (DavidM)-[:ACTED_IN {roles:['Brutus "Brutal" Howell']}]->(TheGreenMile), (BonnieH)-[:ACTED_IN {roles:['Jan Edgecomb']}]->(TheGreenMile), (JamesC)-[:ACTED_IN {roles:['Warden Hal Moores']}]->(TheGreenMile), (SamR)-[:ACTED_IN {roles:['"Wild Bill" Wharton']}]->(TheGreenMile), (GaryS)-[:ACTED_IN {roles:['Burt Hammersmith']}]->(TheGreenMile), (PatriciaC)-[:ACTED_IN {roles:['Melinda Moores']}]->(TheGreenMile), (FrankD)-[:DIRECTED]->(TheGreenMile) CREATE (FrostNixon:Movie {title:'Frost/Nixon', released:2008, tagline:'400 million people were waiting for the truth.'}) CREATE (FrankL:Person {name:'Frank Langella', born:1938}) CREATE (MichaelS:Person {name:'Michael Sheen', born:1969}) CREATE (OliverP:Person {name:'Oliver Platt', born:1960}) CREATE (FrankL)-[:ACTED_IN {roles:['Richard Nixon']}]->(FrostNixon), (MichaelS)-[:ACTED_IN {roles:['David Frost']}]->(FrostNixon), (KevinB)-[:ACTED_IN {roles:['Jack Brennan']}]->(FrostNixon), (OliverP)-[:ACTED_IN {roles:['Bob Zelnick']}]->(FrostNixon), (SamR)-[:ACTED_IN {roles:['James Reston, Jr.']}]->(FrostNixon), (RonH)-[:DIRECTED]->(FrostNixon) CREATE (Hoffa:Movie {title:'Hoffa', released:1992, tagline:"He didn't want law. He wanted justice."}) CREATE (DannyD:Person {name:'Danny DeVito', born:1944}) CREATE (JohnR:Person {name:'John C. Reilly', born:1965}) CREATE (JackN)-[:ACTED_IN {roles:['Hoffa']}]->(Hoffa), (DannyD)-[:ACTED_IN {roles:['Robert "Bobby" Ciaro']}]->(Hoffa), (JTW)-[:ACTED_IN {roles:['Frank Fitzsimmons']}]->(Hoffa), (JohnR)-[:ACTED_IN {roles:['Peter "Pete" Connelly']}]->(Hoffa), (DannyD)-[:DIRECTED]->(Hoffa) CREATE (Apollo13:Movie {title:'Apollo 13', released:1995, tagline:'Houston, we have a problem.'}) CREATE (EdH:Person {name:'Ed Harris', born:1950}) CREATE (BillPax:Person {name:'Bill Paxton', born:1955}) CREATE (TomH)-[:ACTED_IN {roles:['Jim Lovell']}]->(Apollo13), (KevinB)-[:ACTED_IN {roles:['Jack Swigert']}]->(Apollo13), (EdH)-[:ACTED_IN {roles:['Gene Kranz']}]->(Apollo13), (BillPax)-[:ACTED_IN {roles:['Fred Haise']}]->(Apollo13), (GaryS)-[:ACTED_IN {roles:['Ken Mattingly']}]->(Apollo13), (RonH)-[:DIRECTED]->(Apollo13) CREATE (Twister:Movie {title:'Twister', released:1996, tagline:"Don't Breathe. Don't Look Back."}) CREATE (PhilipH:Person {name:'Philip Seymour Hoffman', born:1967}) CREATE (JanB:Person {name:'Jan de Bont', born:1943}) CREATE (BillPax)-[:ACTED_IN {roles:['Bill Harding']}]->(Twister), (HelenH)-[:ACTED_IN {roles:['Dr. Jo Harding']}]->(Twister), (ZachG)-[:ACTED_IN {roles:['Eddie']}]->(Twister), (PhilipH)-[:ACTED_IN {roles:['Dustin "Dusty" Davis']}]->(Twister), (JanB)-[:DIRECTED]->(Twister) CREATE (CastAway:Movie {title:'Cast Away', released:2000, tagline:'At the edge of the world, his journey begins.'}) CREATE (RobertZ:Person {name:'Robert Zemeckis', born:1951}) CREATE (TomH)-[:ACTED_IN {roles:['Chuck Noland']}]->(CastAway), (HelenH)-[:ACTED_IN {roles:['Kelly Frears']}]->(CastAway), (RobertZ)-[:DIRECTED]->(CastAway) CREATE (OneFlewOvertheCuckoosNest:Movie {title:"One Flew Over the Cuckoo's Nest", released:1975, tagline:"If he's crazy, what does that make you?"}) CREATE (MilosF:Person {name:'Milos Forman', born:1932}) CREATE (JackN)-[:ACTED_IN {roles:['Randle McMurphy']}]->(OneFlewOvertheCuckoosNest), (DannyD)-[:ACTED_IN {roles:['Martini']}]->(OneFlewOvertheCuckoosNest), (MilosF)-[:DIRECTED]->(OneFlewOvertheCuckoosNest) CREATE (SomethingsGottaGive:Movie {title:"Something's Gotta Give", released:2003}) CREATE (DianeK:Person {name:'Diane Keaton', born:1946}) CREATE (NancyM:Person {name:'Nancy Meyers', born:1949}) CREATE (JackN)-[:ACTED_IN {roles:['Harry Sanborn']}]->(SomethingsGottaGive), (DianeK)-[:ACTED_IN {roles:['Erica Barry']}]->(SomethingsGottaGive), (Keanu)-[:ACTED_IN {roles:['Julian Mercer']}]->(SomethingsGottaGive), (NancyM)-[:DIRECTED]->(SomethingsGottaGive), (NancyM)-[:PRODUCED]->(SomethingsGottaGive), (NancyM)-[:WROTE]->(SomethingsGottaGive) CREATE (BicentennialMan:Movie {title:'Bicentennial Man', released:1999, tagline:"One robot's 200 year journey to become an ordinary man."}) CREATE (ChrisC:Person {name:'Chris Columbus', born:1958}) CREATE (Robin)-[:ACTED_IN {roles:['Andrew Marin']}]->(BicentennialMan), (OliverP)-[:ACTED_IN {roles:['Rupert Burns']}]->(BicentennialMan), (ChrisC)-[:DIRECTED]->(BicentennialMan) CREATE (CharlieWilsonsWar:Movie {title:"Charlie Wilson's War", released:2007, tagline:"A stiff drink. A little mascara. A lot of nerve. Who said they couldn't bring down the Soviet empire."}) CREATE (JuliaR:Person {name:'Julia Roberts', born:1967}) CREATE (TomH)-[:ACTED_IN {roles:['Rep. Charlie Wilson']}]->(CharlieWilsonsWar), (JuliaR)-[:ACTED_IN {roles:['Joanne Herring']}]->(CharlieWilsonsWar), (PhilipH)-[:ACTED_IN {roles:['Gust Avrakotos']}]->(CharlieWilsonsWar), (MikeN)-[:DIRECTED]->(CharlieWilsonsWar) CREATE (ThePolarExpress:Movie {title:'The Polar Express', released:2004, tagline:'This Holiday Season… Believe'}) CREATE (TomH)-[:ACTED_IN {roles:['Hero Boy', 'Father', 'Conductor', 'Hobo', 'Scrooge', 'Santa Claus']}]->(ThePolarExpress), (RobertZ)-[:DIRECTED]->(ThePolarExpress) CREATE (ALeagueofTheirOwn:Movie {title:'A League of Their Own', released:1992, tagline:'Once in a lifetime you get a chance to do something different.'}) CREATE (Madonna:Person {name:'Madonna', born:1954}) CREATE (GeenaD:Person {name:'Geena Davis', born:1956}) CREATE (LoriP:Person {name:'Lori Petty', born:1963}) CREATE (PennyM:Person {name:'Penny Marshall', born:1943}) CREATE (TomH)-[:ACTED_IN {roles:['Jimmy Dugan']}]->(ALeagueofTheirOwn), (GeenaD)-[:ACTED_IN {roles:['Dottie Hinson']}]->(ALeagueofTheirOwn), (LoriP)-[:ACTED_IN {roles:['Kit Keller']}]->(ALeagueofTheirOwn), (RosieO)-[:ACTED_IN {roles:['Doris Murphy']}]->(ALeagueofTheirOwn), (Madonna)-[:ACTED_IN {roles:['"All the Way" Mae Mordabito']}]->(ALeagueofTheirOwn), (BillPax)-[:ACTED_IN {roles:['Bob Hinson']}]->(ALeagueofTheirOwn), (PennyM)-[:DIRECTED]->(ALeagueofTheirOwn) CREATE (PaulBlythe:Person {name:'Paul Blythe'}) CREATE (AngelaScope:Person {name:'Angela Scope'}) CREATE (JessicaThompson:Person {name:'Jessica Thompson'}) CREATE (JamesThompson:Person {name:'James Thompson'}) CREATE (JamesThompson)-[:FOLLOWS]->(JessicaThompson), (AngelaScope)-[:FOLLOWS]->(JessicaThompson), (PaulBlythe)-[:FOLLOWS]->(AngelaScope) CREATE (JessicaThompson)-[:REVIEWED {summary:'An amazing journey', rating:95}]->(CloudAtlas), (JessicaThompson)-[:REVIEWED {summary:'Silly, but fun', rating:65}]->(TheReplacements), (JamesThompson)-[:REVIEWED {summary:'The coolest football movie ever', rating:100}]->(TheReplacements), (AngelaScope)-[:REVIEWED {summary:'Pretty funny at times', rating:62}]->(TheReplacements), (JessicaThompson)-[:REVIEWED {summary:'Dark, but compelling', rating:85}]->(Unforgiven), (JessicaThompson)-[:REVIEWED {summary:"Slapstick redeemed only by the Robin Williams and Gene Hackman's stellar performances", rating:45}]->(TheBirdcage), (JessicaThompson)-[:REVIEWED {summary:'A solid romp', rating:68}]->(TheDaVinciCode), (JamesThompson)-[:REVIEWED {summary:'Fun, but a little far fetched', rating:65}]->(TheDaVinciCode), (JessicaThompson)-[:REVIEWED {summary:'You had me at Jerry', rating:92}]->(JerryMaguire) ; """ end end ================================================ FILE: test/support/internal_case.ex ================================================ defmodule Bolt.Sips.InternalCase do use ExUnit.CaseTemplate alias Bolt.Sips.Internals.BoltProtocol setup do uri = neo4j_uri() port_opts = [active: false, mode: :binary, packet: :raw] {:ok, port} = :gen_tcp.connect(uri.host, uri.port, port_opts) {:ok, bolt_version} = BoltProtocol.handshake(:gen_tcp, port) {:ok, _} = init(:gen_tcp, port, bolt_version, uri.userinfo) on_exit(fn -> :gen_tcp.close(port) end) {:ok, port: port, is_bolt_v2: bolt_version >= 2, bolt_version: bolt_version} end defp neo4j_uri do "bolt://neo4j:test@localhost:7687" |> URI.merge(System.get_env("NEO4J_TEST_URL") || "") |> URI.parse() |> Map.update!(:host, &String.to_charlist/1) |> Map.update!(:userinfo, fn nil -> {} userinfo -> userinfo |> String.split(":") |> List.to_tuple() end) end defp init(transport, port, 3, auth) do BoltProtocol.hello(transport, port, 3, auth) end defp init(transport, port, bolt_version, auth) do BoltProtocol.init(transport, port, bolt_version, auth) end end ================================================ FILE: test/test_helper.exs ================================================ Logger.configure(level: :debug) ExUnit.start(capture_log: true, assert_receive_timeout: 500, exclude: [:skip, :bench, :apoc]) Application.ensure_started(:porcelain) Code.require_file("test_support.exs", __DIR__) defmodule Bolt.Sips.TestHelper do @doc """ Read an entire file into a string. Return a tuple of success and data. """ def read_whole_file(path) do case File.read(path) do {:ok, file} -> file {:error, reason} -> {:error, "Could not open #{path} #{file_error_description(reason)}"} end end @doc """ Open a file stream, and join the lines into a string. """ def stream_file_join(filename) do stream = File.stream!(filename) Enum.join(stream) end defp file_error_description(:enoent), do: "because the file does not exist." defp file_error_description(reason), do: "due to #{reason}." end Bolt.Sips.start_link(Application.get_env(:bolt_sips, Bolt)) # I am using the test db for debugging and the line below will clear *everything* # Bolt.Sips.query(Bolt.Sips.conn, "MATCH (n) OPTIONAL MATCH (n)-[r]-() DELETE n,r") # # todo: The tests should cleanup the data they create. Process.flag(:trap_exit, true) ================================================ FILE: test/test_large_param_set.exs ================================================ defmodule Large.Param.Set.Test do use ExUnit.Case doctest Bolt.Sips setup_all do {:ok, [conn: Bolt.Sips.conn()]} end @doc """ Boltex.Bolt.generate_chunks fails with too much data test provided by @adri, for issue #16 """ test "executing a Cypher query, with large set of parameters", context do conn = context[:conn] cypher = """ MATCH (n:Person {bolt_sips: true}) FOREACH (i IN $largeRange| SET n.test = TRUE ) """ case Bolt.Sips.query(conn, cypher, %{largeRange: Enum.to_list(0..1_000_000)}) do {:ok, stats} -> assert stats["properties-set"] > 0, "Expecting many properties set" {:error, reason} -> IO.puts("Error: #{reason["message"]}") end end end ================================================ FILE: test/test_support.exs ================================================ defmodule TestSupport do @moduledoc false end ================================================ FILE: test/transaction_test.exs ================================================ defmodule Transaction.Test do use ExUnit.Case, async: true alias Bolt.Sips.Response setup do {:ok, [main_conn: Bolt.Sips.conn()]} end test "execute statements in transaction", %{main_conn: main_conn} do Bolt.Sips.transaction(main_conn, fn conn -> book = Bolt.Sips.query!(conn, "CREATE (b:Book {title: \"The Game Of Trolls\"}) return b") |> Response.first() assert %{"b" => g_o_t} = book assert g_o_t.properties["title"] == "The Game Of Trolls" Bolt.Sips.rollback(conn, :changed_my_mind) end) books = Bolt.Sips.query!(main_conn, "MATCH (b:Book {title: \"The Game Of Trolls\"}) return b") assert Enum.count(books) == 0 end ### ### NOTE: ### ### The labels used in these examples MUST be unique across all tests! ### These tests depend on being able to expect that a node either exists ### or does not, and asynchronous testing with the same names will cause ### random cases where the underlying state changes. ### test "rollback statements in transaction", %{main_conn: main_conn} do try do # In case there's already a copy in our DB, count them... {:ok, %Response{results: [result]}} = Bolt.Sips.query(main_conn, "MATCH (x:XactRollback) RETURN count(x)") original_count = result["count(x)"] Bolt.Sips.transaction(main_conn, fn conn -> assert {:ok, %Response{results: [row]}} = Bolt.Sips.query( conn, "CREATE (x:XactRollback {title:\"The Game Of Trolls\"}) return x" ) assert row["x"].properties["title"] == "The Game Of Trolls" # Original connection (outside the transaction) should not see this node. assert {:ok, %Response{results: [result]}} = Bolt.Sips.query(main_conn, "MATCH (x:XactRollback) RETURN count(x)") assert result["count(x)"] == original_count, "Main connection should not be able to see transactional change" Bolt.Sips.rollback(conn, :changed_my_mind) end) # Original connection should still not see this node committed. assert {:ok, %Response{results: [result]}} = Bolt.Sips.query(main_conn, "MATCH (x:XactRollback) RETURN count(x)") assert result["count(x)"] == original_count after # Delete all XactRollback nodes in case the rollback() didn't work! Bolt.Sips.query(main_conn, "MATCH (x:XactRollback) DETACH DELETE x") end end test "commit statements in transaction", %{main_conn: main_conn} do try do Bolt.Sips.transaction(main_conn, fn conn -> assert {:ok, %Response{results: books}} = Bolt.Sips.query(conn, "CREATE (x:XactCommit {foo: 'bar'}) return x") # TODO: maybe we can make Entity implement Access? That will avoid the Map gets below assert "bar" == books |> List.first() |> Map.get("x") |> Map.get(:properties) |> Map.get("foo") # Main connection should not see this new node. {:ok, %Response{results: results}} = Bolt.Sips.query(main_conn, "MATCH (x:XactCommit) RETURN x") assert is_list(results) assert Enum.count(results) == 0, "Main connection should not be able to see transactional changes" end) # And we should see it now with the main connection. {:ok, %Response{results: [%{"x" => node}]}} = Bolt.Sips.query(main_conn, "MATCH (x:XactCommit) RETURN x") assert node.labels == ["XactCommit"] assert node.properties["foo"] == "bar" after # Delete any XactCommit nodes that were succesfully committed! Bolt.Sips.query(main_conn, "MATCH (x:XactCommit) DETACH DELETE x") end end end