diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 8b13252..8f0f1b2 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -16,20 +16,26 @@ jobs: strategy: matrix: - iex: [1.14.3] - otp: [24.3.4, 25.2] + iex: [1.15.8, 1.16.3] + otp: [25.3, 26.2] + include: + - iex: 1.14.5 + otp: 24.3.4 + - iex: 1.17.3 + otp: 27.2 steps: - uses: actions/checkout@v3 - name: Set up Elixir - uses: erlef/setup-beam@988e02bfe678367a02564f65ca2e37726dc0268f + uses: erlef/setup-beam@v1 + id: beam with: elixir-version: ${{ matrix.iex }} otp-version: ${{ matrix.otp }} - name: Restore dependencies cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: deps key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} @@ -41,7 +47,7 @@ jobs: # Don't cache PLTs based on mix.lock hash, as Dialyzer can incrementally update even old ones # Cache key based on Elixir & Erlang version (also useful when running in matrix) - name: Restore PLT cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: plt_cache with: key: | diff --git a/.tool-versions b/.tool-versions index e709508..73d259e 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -elixir 1.14.0-otp-24 -erlang 24.3.4 +elixir 1.17.3 +erlang 27.2 diff --git a/README.md b/README.md index 55cf0f1..fba7ec6 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,15 @@ Most popular engine for those is Neo4j thus this library focuses on providing fu Currently only simple quering using raw Cypher queries is implemented, but there are few items on the Roadmap. -### Bolt_sips +## Existing libraries -One may say "there is already a library for communication with Neo4j". They are right **BUT** first and foremost, `bolt_sips` is left unmaintained ([discussion](https://github.com/florinpatrascu/bolt_sips/issues/109)). There were few attempts to continue that, but there is still no library that would take advantage of Elixir structs, protocols and behaviours to provide robust extensibility. Secondly, `bolt_sips` is just a driver. This library purpose will be to provide complete user experience when interacting with the DB. +There were already few attempts to write a driver for Bolt protocol but all of them seem to be clumsy in terms of protocol logic - many things are "hardcoded" as in the docs instead of being thought out for the server's operation and coding a reusable solution. They are not taking advantage of Elixir structs, protocols and behaviours to provide robust extensibility. +Secondly, those libs are just a driver and this library purpose is to provide complete user experience when interacting with the DB. This should be solved by building Ecto-like support for the Cypher query language. +At this point, it's worth noting that this library may not be faster than `bolt_sips` or `boltx` due to greater usage of Protocols and structs. +You can modify tasks from `example_app` to benchmark those on your data. + ## Installation The package can be installed by adding `neo4ex` to your list of dependencies in `mix.exs`: diff --git a/docker-compose.yml b/docker-compose.yml index 130d53e..8b5eb16 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,10 @@ -version: '3.1' - services: graph_db: - image: neo4j:4.4.28-community + image: neo4j:5.26.0-community environment: NEO4J_AUTH: 'neo4j/letmein' NEO4JLABS_PLUGINS: '["apoc"]' + NEO4J_dbms_security_auth__minimum__password__length: 6 ports: - "7474:7474" - "7687:7687" diff --git a/example_app/config/config.exs b/example_app/config/config.exs index 5c1a227..05af109 100644 --- a/example_app/config/config.exs +++ b/example_app/config/config.exs @@ -6,8 +6,11 @@ config :example_app, ExampleApp.Connector, credentials: "letmein", pool_size: 1 -config :bolt_sips, Bolt, - url: "bolt://localhost:7687", - basic_auth: [username: "neo4j", password: "letmein"], +config :boltx, Bolt, + uri: "bolt://localhost:7687", + auth: [username: "neo4j", password: "letmein"], + user_agent: "boltxTest/1", pool_size: 1, - max_overflow: 0 + max_overflow: 0, + prefix: :default, + name: Boltx diff --git a/example_app/lib/example_app/application.ex b/example_app/lib/example_app/application.ex index 396b426..a0e13ff 100644 --- a/example_app/lib/example_app/application.ex +++ b/example_app/lib/example_app/application.ex @@ -9,7 +9,7 @@ defmodule ExampleApp.Application do def start(_type, _args) do children = [ ExampleApp.Connector, - {Bolt.Sips, Application.get_env(:bolt_sips, Bolt)} + {Boltx, Application.get_env(:boltx, Bolt)} ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/example_app/lib/mix/tasks/bolt_sips_benchmark.ex b/example_app/lib/mix/tasks/boltx_benchmark.ex similarity index 68% rename from example_app/lib/mix/tasks/bolt_sips_benchmark.ex rename to example_app/lib/mix/tasks/boltx_benchmark.ex index 60d6f3a..70840ce 100644 --- a/example_app/lib/mix/tasks/bolt_sips_benchmark.ex +++ b/example_app/lib/mix/tasks/boltx_benchmark.ex @@ -1,19 +1,17 @@ -defmodule Mix.Tasks.ExampleApp.BoltSipsBenchmark do +defmodule Mix.Tasks.ExampleApp.BoltxBenchmark do use Mix.Task alias Neo4ex.Cypher - alias Bolt.Sips, as: Neo - alias ExampleApp.Connector @requirements ["app.start"] - @shortdoc "Runs benchmark to compare with bolt_sips library" + @shortdoc "Runs benchmark to compare with boltx library" def run(_args) do Benchee.run(%{ "Neo4ex" => fn -> neo4ex() end, - "Bolt.Sips" => fn -> bolt_sips() end + "Boltx" => fn -> boltx() end }) end @@ -22,9 +20,9 @@ defmodule Mix.Tasks.ExampleApp.BoltSipsBenchmark do Connector.run(%Cypher.Query{query: query, params: params}) end - def bolt_sips() do + def boltx() do %{query: query, params: params} = customer_query() - Neo.query!(Neo.conn(), query, params) + Boltx.query!(Boltx, query, params) end def customer_query() do @@ -32,7 +30,7 @@ defmodule Mix.Tasks.ExampleApp.BoltSipsBenchmark do MATCH (customer) RETURN customer, rand() as r ORDER BY r - LIMIT 10 + LIMIT 100 """ %{query: query, params: %{}} diff --git a/example_app/mix.exs b/example_app/mix.exs index 46c3ae7..15298d0 100644 --- a/example_app/mix.exs +++ b/example_app/mix.exs @@ -23,7 +23,7 @@ defmodule ExampleApp.MixProject do defp deps do [ {:neo4ex, path: "../"}, - {:bolt_sips, git: "https://github.com/florinpatrascu/bolt_sips", branch: "master"}, + {:boltx, "~> 0.0.6"}, {:faker, "~> 0.17.0"}, {:jason, "~> 1.2"}, {:benchee, "~> 1.0"} diff --git a/example_app/mix.lock b/example_app/mix.lock index e412a9b..8a363fc 100644 --- a/example_app/mix.lock +++ b/example_app/mix.lock @@ -1,11 +1,12 @@ %{ - "benchee": {:hex, :benchee, "1.2.0", "afd2f0caec06ce3a70d9c91c514c0b58114636db9d83c2dc6bfd416656618353", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "ee729e53217898b8fd30aaad3cce61973dab61574ae6f48229fe7ff42d5e4457"}, + "benchee": {:hex, :benchee, "1.3.1", "c786e6a76321121a44229dde3988fc772bca73ea75170a73fd5f4ddf1af95ccf", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "76224c58ea1d0391c8309a8ecbfe27d71062878f59bd41a390266bf4ac1cc56d"}, "bolt_sips": {:git, "https://github.com/florinpatrascu/bolt_sips", "b21901a46ed19b17d1c87a9ef9e56002f83f345c", [branch: "master"]}, + "boltx": {:hex, :boltx, "0.0.6", "c6a396b1538b258e4d5ee2a94aaf8fb2c7879240efffba94b9159dbdce963790", [:mix], [{:db_connection, "~> 2.6.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "576b8f21a2021674130d04cd1fc79a4829a23d2cdf50641b3d7a00ce31b98ead"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, - "db_connection": {:hex, :db_connection, "2.4.3", "3b9aac9f27347ec65b271847e6baeb4443d8474289bd18c1d6f4de655b70c94d", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c127c15b0fa6cfb32eed07465e05da6c815b032508d4ed7c116122871df73c12"}, + "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "faker": {:hex, :faker, "0.17.0", "671019d0652f63aefd8723b72167ecdb284baf7d47ad3a82a15e9b8a6df5d1fa", [:mix], [], "hexpm", "a7d4ad84a93fd25c5f5303510753789fc2433ff241bf3b4144d3f6f291658a6a"}, - "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, - "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, } diff --git a/lib/neo4ex/bolt_protocol.ex b/lib/neo4ex/bolt_protocol.ex index 9b32130..10c588a 100644 --- a/lib/neo4ex/bolt_protocol.ex +++ b/lib/neo4ex/bolt_protocol.ex @@ -28,8 +28,6 @@ defmodule Neo4ex.BoltProtocol do alias Neo4ex.BoltProtocol.Structure.Message.Summary.{Success, Failure} - @user_agent "Neo4ex/#{Application.spec(:neo4ex, :vsn)}" - @impl true def connect(opts) do hostname = Keyword.get(opts, :hostname) @@ -42,7 +40,11 @@ defmodule Neo4ex.BoltProtocol do :ok <- hello(socket, opts) do {:ok, socket} else - other -> other + {:ok, %Failure{metadata: %{"message" => failure}}} -> + {:error, failure} + + other -> + other end end @@ -290,12 +292,14 @@ defmodule Neo4ex.BoltProtocol do end if Version.match?(bolt_version, ">= 5.1.0") do - hello = %Hello{extra: %Extra.Hello{user_agent: @user_agent}} + hello = %Hello{extra: %Extra.Hello{}} logon = %Logon{ - scheme: scheme, - principal: principal, - credentials: credentials + auth: %Extra.Logon{ + scheme: scheme, + principal: principal, + credentials: credentials + } } with( @@ -311,7 +315,6 @@ defmodule Neo4ex.BoltProtocol do else message = %Hello{ extra: %Extra.Hello{ - user_agent: @user_agent, scheme: scheme, principal: principal, credentials: credentials diff --git a/lib/neo4ex/bolt_protocol/structure.ex b/lib/neo4ex/bolt_protocol/structure.ex index 79ff995..ad55571 100644 --- a/lib/neo4ex/bolt_protocol/structure.ex +++ b/lib/neo4ex/bolt_protocol/structure.ex @@ -82,10 +82,11 @@ defmodule Neo4ex.BoltProtocol.Structure do defp build_fields_list(block) do block |> Macro.prewalk([], fn - {:field, _, [name, opts]}, acc -> + {:field, _, [name | opts]}, acc -> opts = - Keyword.update( - opts, + opts + |> List.flatten() + |> Keyword.update( :version, quote(do: Version.parse_requirement!(">= 0.0.0")), fn requirement -> @@ -152,6 +153,11 @@ defmodule Neo4ex.BoltProtocol.Structure do # embedded structure must be a map but we should take advantage of field versioning defp embedded_encoder_protocol(fields_list) do + # When default values contain some data from module (like values from attributes) it won't be visible inside defimpl (different module) + # For protocol we need only version so this is completely fine + fields_list = + Enum.map(fields_list, fn {field, opts} -> {field, [version: opts[:version]]} end) + quote location: :keep do defimpl Neo4ex.BoltProtocol.Encoder do def encode(struct, bolt_version) do @@ -169,6 +175,11 @@ defmodule Neo4ex.BoltProtocol.Structure do end defp encoder_protocol(fields_list) do + # When default values contain some data from module (like values from attributes) it won't be visible inside defimpl (different module) + # For protocol we need only version so this is completely fine + fields_list = + Enum.map(fields_list, fn {field, opts} -> {field, [version: opts[:version]]} end) + quote location: :keep do defimpl Neo4ex.BoltProtocol.Encoder do alias Neo4ex.PackStream.{Markers, Exceptions} diff --git a/lib/neo4ex/bolt_protocol/structure/message/extra/hello.ex b/lib/neo4ex/bolt_protocol/structure/message/extra/hello.ex index 3891227..36fb87a 100644 --- a/lib/neo4ex/bolt_protocol/structure/message/extra/hello.ex +++ b/lib/neo4ex/bolt_protocol/structure/message/extra/hello.ex @@ -1,15 +1,26 @@ defmodule Neo4ex.BoltProtocol.Structure.Message.Extra.Hello do use Neo4ex.BoltProtocol.Structure + @version Mix.Project.config()[:version] + # can't be encoded directly, it's just helper for the Hello message embeded_structure do - field(:user_agent, default: "Neo4ex/0.1.0") + field(:user_agent, default: "Neo4ex/#{@version}") + + field(:bolt_agent, + default: %{ + product: "Neo4ex/#{@version}", + language: "Elixir/#{System.build_info()[:version]}" + }, + version: ">= 5.3.0" + ) + field(:patch_bolt, default: ["utc"], version: ">= 4.3.0 and <= 4.4.0") field(:routing, default: %{}, version: ">= 4.1.0") # prior to v5.1, authentication is handled inside HELLO message - field(:scheme, default: "", version: "< 5.1.0") - field(:principal, default: "", version: "< 5.1.0") - field(:credentials, default: "", version: "< 5.1.0") + field(:scheme, version: "< 5.1.0") + field(:principal, version: "< 5.1.0") + field(:credentials, version: "< 5.1.0") end end diff --git a/lib/neo4ex/bolt_protocol/structure/message/extra/logon.ex b/lib/neo4ex/bolt_protocol/structure/message/extra/logon.ex new file mode 100644 index 0000000..5ee5a13 --- /dev/null +++ b/lib/neo4ex/bolt_protocol/structure/message/extra/logon.ex @@ -0,0 +1,12 @@ +defmodule Neo4ex.BoltProtocol.Structure.Message.Extra.Logon do + use Neo4ex.BoltProtocol.Structure + + # TODO: implement validation + # @predefined_schemes ~w(none basic bearer kerberos) + + embeded_structure do + field(:scheme) + field(:principal) + field(:credentials) + end +end diff --git a/lib/neo4ex/bolt_protocol/structure/message/request/logon.ex b/lib/neo4ex/bolt_protocol/structure/message/request/logon.ex index 5c009c1..d2605ef 100644 --- a/lib/neo4ex/bolt_protocol/structure/message/request/logon.ex +++ b/lib/neo4ex/bolt_protocol/structure/message/request/logon.ex @@ -1,12 +1,9 @@ defmodule Neo4ex.BoltProtocol.Structure.Message.Request.Logon do use Neo4ex.BoltProtocol.Structure - # TODO: implement validation - # @predefined_schemes ~w(none basic bearer kerberos) + alias Neo4ex.BoltProtocol.Structure.Message.Extra structure 0x6A do - field(:scheme, default: "") - field(:principal, default: "") - field(:credentials, default: "") + field(:auth, default: %Extra.Logon{}) end end diff --git a/lib/neo4ex/connector.ex b/lib/neo4ex/connector.ex index 65c191e..938988b 100644 --- a/lib/neo4ex/connector.ex +++ b/lib/neo4ex/connector.ex @@ -15,7 +15,7 @@ defmodule Neo4ex.Connector do @noop <<0::size(@chunk_size)>> # since 4.3 there is support for version range during negotiation # so "4.4.1" actually means "4.4" plus one previous version "4.3" - @supported_versions ["4.4.1", "4.2.0", "4.1.0", "4.0.0"] + @supported_versions ["5.20.20", "4.4.3", "4.2.0", "4.0.0"] defmacro __using__(otp_app: app) do supported_versions = @supported_versions @@ -101,14 +101,17 @@ defmodule Neo4ex.Connector do end end - def supported_versions() do - Enum.flat_map(@supported_versions, fn version -> + defmacro supported_versions() do + @supported_versions + |> Enum.flat_map(fn version -> [major, minor, range] = version |> String.split(".") |> Enum.map(&String.to_integer/1) for i <- minor..(minor - range) do Version.parse!("#{major}.#{i}.0") end end) + |> Enum.uniq() + |> Macro.escape() end @doc false diff --git a/lib/neo4ex/utils.ex b/lib/neo4ex/utils.ex index b19812f..c4e2a13 100644 --- a/lib/neo4ex/utils.ex +++ b/lib/neo4ex/utils.ex @@ -1,6 +1,8 @@ defmodule Neo4ex.Utils do @moduledoc false + import Neo4ex.Connector, only: [supported_versions: 0] + alias Neo4ex.BoltProtocol alias Neo4ex.PackStream @@ -88,7 +90,7 @@ defmodule Neo4ex.Utils do end def list_valid_versions(requirement) do - Enum.filter(Neo4ex.Connector.supported_versions(), fn ver -> + Enum.filter(supported_versions(), fn ver -> Version.match?(ver, requirement) end) end diff --git a/mix.exs b/mix.exs index 43e9478..5c8e704 100644 --- a/mix.exs +++ b/mix.exs @@ -17,7 +17,7 @@ defmodule Neo4ex.MixProject do docs: docs(), test_coverage: [ ignore_modules: [ - Neo4ex.BoltProtocol.Structure, + ~r/^Neo4ex.BoltProtocol.Structure/, Neo4ex.PackStream.DecoderBuilder ], summary: [ @@ -40,14 +40,14 @@ defmodule Neo4ex.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:db_connection, "~> 2.4"}, + {:db_connection, "~> 2.6.0"}, # Tests {:mox, "~> 1.0", only: [:test]}, # Linting - {:credo, "~> 1.6.7", only: [:dev]}, - {:dialyxir, "~> 1.2.0", only: [:dev], runtime: false}, + {:credo, "~> 1.6", only: [:dev]}, + {:dialyxir, "~> 1.4", only: [:dev], runtime: false}, # Documentation # Run with: `mix docs` diff --git a/mix.lock b/mix.lock index 3c7eba9..aef5768 100644 --- a/mix.lock +++ b/mix.lock @@ -1,18 +1,19 @@ %{ - "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, - "credo": {:hex, :credo, "1.6.7", "323f5734350fd23a456f2688b9430e7d517afb313fbd38671b8a4449798a7854", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "41e110bfb007f7eda7f897c10bf019ceab9a0b269ce79f015d54b0dcf4fc7dd3"}, - "db_connection": {:hex, :db_connection, "2.4.3", "3b9aac9f27347ec65b271847e6baeb4443d8474289bd18c1d6f4de655b70c94d", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c127c15b0fa6cfb32eed07465e05da6c815b032508d4ed7c116122871df73c12"}, - "dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.31", "a93921cdc6b9b869f519213d5bc79d9e218ba768d7270d46fdcf1c01bacff9e2", [:mix], [], "hexpm", "317d367ee0335ef037a87e46c91a2269fef6306413f731e8ec11fc45a7efd059"}, - "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"}, - "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, - "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, - "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, - "mox": {:hex, :mox, "1.0.2", "dc2057289ac478b35760ba74165b4b3f402f68803dd5aecd3bfd19c183815d64", [:mix], [], "hexpm", "f9864921b3aaf763c8741b5b8e6f908f44566f1e427b2630e89e9a73b981fef2"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, - "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "credo": {:hex, :credo, "1.7.10", "6e64fe59be8da5e30a1b96273b247b5cf1cc9e336b5fd66302a64b25749ad44d", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "71fbc9a6b8be21d993deca85bf151df023a3097b01e09a2809d460348561d8cd"}, + "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, + "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, + "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, + "ex_doc": {:hex, :ex_doc, "0.35.1", "de804c590d3df2d9d5b8aec77d758b00c814b356119b3d4455e4b8a8687aecaf", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2121c6402c8d44b05622677b761371a759143b958c6c19f6558ff64d0aed40df"}, + "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, + "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, + "mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"}, + "nimble_ownership": {:hex, :nimble_ownership, "1.0.1", "f69fae0cdd451b1614364013544e66e4f5d25f36a2056a9698b793305c5aa3a6", [:mix], [], "hexpm", "3825e461025464f519f3f3e4a1f9b68c47dc151369611629ad08b636b73bb22d"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, } diff --git a/test/neo4ex/bolt_protocol/encoder_test.exs b/test/neo4ex/bolt_protocol/encoder_test.exs index f8d5809..f52b988 100644 --- a/test/neo4ex/bolt_protocol/encoder_test.exs +++ b/test/neo4ex/bolt_protocol/encoder_test.exs @@ -8,8 +8,11 @@ defmodule Neo4ex.BoltProtocol.EncoderTest do alias Neo4ex.BoltProtocol.Structure.Message.Extra alias Neo4ex.BoltProtocol.Encoder + alias Neo4ex.BoltProtocol.Decoder alias Neo4ex.PackStream.Exceptions + @version Mix.Project.config()[:version] + describe "encode/2" do test "returns valid binary representation of Lists" do assert <<0x90>> == Encoder.encode([], "4.0.0") @@ -25,8 +28,30 @@ defmodule Neo4ex.BoltProtocol.EncoderTest do end test "returns valid binary representation of Maps" do - assert <<0xA3, 0x81, "a", 1, 0x81, "b", 0x81, "a", 0x81, "c", 0xC1, 0x4::4, 0x0::60>> == - Encoder.encode(%{a: 1, b: "a", c: 2.0}, "4.0.0") + input = %{a: 1, b: "a", c: 2.0} + + # assert <<0xA3, 0x81, "a", 1, 0x81, "b", 0x81, "a", 0x81, "c", 0xC1, 0x4::4, 0x0::60>> == + # Encoder.encode(%{a: 1, b: "a", c: 2.0}, "4.0.0") + + # Keys in maps aren't sorted in newest OTP. We have to pattern match on each possible sorting (assuming the same kinds of values will be kept together, so the order is string,string,float or float,string,string) + case Encoder.encode(input, "4.0.0") do + <<0xA3, 0x81, "a", 1, 0x81, "b", 0x81, "a", 0x81, "c", 0xC1, 0x4::4, 0x0::60>> -> + :ok + + <<0xA3, 0x81, "b", 0x81, "a", 0x81, "a", 1, 0x81, "c", 0xC1, 0x4::4, 0x0::60>> -> + :ok + + <<0xA3, 0x81, "c", 0xC1, 0x4::4, 0x0::60, 0x81, "a", 1, 0x81, "b", 0x81, "a">> -> + :ok + + <<0xA3, 0x81, "c", 0xC1, 0x4::4, 0x0::60, 0x81, "b", 0x81, "a", 0x81, "a", 1>> -> + :ok + + other -> + flunk( + "Got invalid encoding for map: #{inspect(input)}, the result was: #{inspect(other)}" + ) + end end test "handles encoding of Node structures" do @@ -56,20 +81,39 @@ defmodule Neo4ex.BoltProtocol.EncoderTest do end test "handles encoding of Hello messages" do - user_agent_bytes = byte_size("user_agent") - ua_bytes = byte_size("Neo4ex/0.1.0") - scheme_bytes = byte_size("scheme") - none_bytes = byte_size("none") - principal_bytes = byte_size("principal") - credentials_bytes = byte_size("credentials") - - # even though Logon is a struct with prefdefined fields order, we're encoding it to the map so the keys will be sent alphabetically - assert <<0xB1, 0x01, 0xA4, 0x8::4, ^credentials_bytes::4, "credentials", 0x80, 0x8::4, - ^principal_bytes::4, "principal", 0x80, 0x8::4, ^scheme_bytes::4, "scheme", 0x8::4, - ^none_bytes::4, "none", 0x8::4, ^user_agent_bytes::4, "user_agent", 0x8::4, - ^ua_bytes::4, - "Neo4ex/0.1.0">> = - Encoder.encode(%Hello{extra: %Extra.Hello{scheme: "none"}}, "4.0.0") + bolt_version = "4.0.0" + # we can't match on every posible key order for generic maps (too many cases) + encoded = Encoder.encode(%Hello{extra: %Extra.Hello{scheme: "none"}}, bolt_version) + decoded = encoded |> Decoder.decode(bolt_version) |> Enum.take(1) |> hd() + + # Extra.Hello is embedded meaning it has no signature on the engine side + assert decoded == %Hello{ + extra: %{ + "user_agent" => "Neo4ex/#{@version}", + "scheme" => "none", + "credentials" => nil, + "principal" => nil + } + } + end + + test "handles encoding of Hello messages for >= 5.3" do + bolt_version = "5.3.0" + # we can't match on every posible key order for generic maps (too many cases) + encoded = Encoder.encode(%Hello{extra: %Extra.Hello{}}, bolt_version) + decoded = encoded |> Decoder.decode(bolt_version) |> Enum.take(1) |> hd() + + # Extra.Hello is embedded meaning it has no signature on the engine side + assert decoded == %Hello{ + extra: %{ + "user_agent" => "Neo4ex/#{@version}", + "bolt_agent" => %{ + "language" => "Elixir/#{System.build_info()[:version]}", + "product" => "Neo4ex/#{@version}" + }, + "routing" => %{} + } + } end end end diff --git a/test/neo4ex/bolt_protocol_test.exs b/test/neo4ex/bolt_protocol_test.exs index d8ac1ee..176ce36 100644 --- a/test/neo4ex/bolt_protocol_test.exs +++ b/test/neo4ex/bolt_protocol_test.exs @@ -4,6 +4,9 @@ defmodule Neo4ex.BoltProtocolTest do import Mox import Neo4ex.Neo4jConnection + alias Neo4ex.BoltProtocol.Structure.Message.Extra + alias Neo4ex.BoltProtocol.Structure.Message.Request.Logon + alias Neo4ex.BoltProtocol.Structure.Message.Request.Hello alias Neo4ex.BoltProtocol.Structure.Message.Request.Rollback alias Neo4ex.BoltProtocol.Structure.Message.Request.Commit alias Neo4ex.BoltProtocol.Structure.Message.Request.Begin @@ -24,6 +27,72 @@ defmodule Neo4ex.BoltProtocolTest do %{socket: %Socket{bolt_version: Version.parse!("4.3.0")}, query: query} end + describe "connect/1" do + test "properly negotiates version" do + success_message = %Success{} + encoded_success_message = Encoder.encode(success_message, "4.0.0") + + # two versions, 4.0.0 and 0.0.0 x 3 (client always sends 4 versions) + handshake = <<0x60, 0x60, 0xB0, 0x17, 0::8, 0::8, 0::8, 4::8, 0::96>> + + hello = generate_message_chunk(%Hello{extra: %Extra.Hello{scheme: "none"}}, "4.0.0") + + SocketMock + |> expect(:connect, fn ~c"noop", 7687, [:binary, {:active, false}] -> {:ok, nil} end) + |> expect(:send, fn _, ^handshake -> :ok end) + |> expect(:recv, fn _, 4 -> {:ok, <<0::16, 0::8, 4::8>>} end) + |> expect(:send, fn _, ^hello -> :ok end) + |> expect_message(encoded_success_message) + + assert {:ok, %Socket{bolt_version: %Version{major: 4, minor: 0, patch: 0}}} == + BoltProtocol.connect(hostname: "noop", versions: ["4.0.0"]) + + encoded_success_message = Encoder.encode(success_message, "5.3.0") + + # two versions, 5.3.0 and 0.0.0 x 3 (client always sends 4 versions) + handshake = <<0x60, 0x60, 0xB0, 0x17, 0::8, 0::8, 3::8, 5::8, 0::96>> + + hello = generate_message_chunk(%Hello{}, "5.3.0") + + logon = + generate_message_chunk( + %Logon{auth: %Extra.Logon{scheme: "bearer", credentials: "abc"}}, + "5.3.0" + ) + + SocketMock + |> expect(:connect, fn ~c"noop", 7687, [:binary, {:active, false}] -> {:ok, nil} end) + |> expect(:send, fn _, ^handshake -> :ok end) + |> expect(:recv, fn _, 4 -> {:ok, <<0::16, 3::8, 5::8>>} end) + |> expect(:send, fn _, ^hello -> :ok end) + |> expect_message(encoded_success_message) + |> expect(:send, fn _, ^logon -> :ok end) + |> expect_message(encoded_success_message) + + assert {:ok, %Socket{bolt_version: %Version{major: 5, minor: 3, patch: 0}}} == + BoltProtocol.connect(hostname: "noop", versions: ["5.3.0"], credentials: "abc") + end + + test "gracefully handles failures" do + message = %Failure{metadata: %{"message" => "failure"}} + encoded_failure_message = Encoder.encode(message, "5.3.0") + + # two versions, 5.3.0 and 0.0.0 x 3 (client always sends 4 versions) + handshake = <<0x60, 0x60, 0xB0, 0x17, 0::8, 0::8, 3::8, 5::8, 0::96>> + + hello = generate_message_chunk(%Hello{}, "5.3.0") + + SocketMock + |> expect(:connect, fn ~c"noop", 7687, [:binary, {:active, false}] -> {:ok, nil} end) + |> expect(:send, fn _, ^handshake -> :ok end) + |> expect(:recv, fn _, 4 -> {:ok, <<0::16, 3::8, 5::8>>} end) + |> expect(:send, fn _, ^hello -> :ok end) + |> expect_message(encoded_failure_message) + + assert {:error, "failure"} == BoltProtocol.connect(hostname: "noop", versions: ["5.3.0"]) + end + end + describe "disconnect/2" do test "sends Goodbye message", %{socket: socket} do chunk = generate_message_chunk(%Goodbye{}) diff --git a/test/neo4ex/connector_test.exs b/test/neo4ex/connector_test.exs index 847994d..97a1c36 100644 --- a/test/neo4ex/connector_test.exs +++ b/test/neo4ex/connector_test.exs @@ -3,6 +3,8 @@ defmodule Neo4ex.ConnectorTest do import Mox + require Neo4ex.Connector + alias Neo4ex.BoltProtocol.Structure.Message.Request.Run alias Neo4ex.BoltProtocol.Encoder @@ -71,4 +73,12 @@ defmodule Neo4ex.ConnectorTest do assert {:error, DBConnection.ConnectionError.exception(":closed")} == Connector.read(socket) end end + + describe "supported_versions/0" do + test "returns compile-time list of versions" do + assert Enum.map(20..0//-1, fn minor -> Version.parse!("5.#{minor}.0") end) ++ + Enum.map(4..0//-1, fn minor -> Version.parse!("4.#{minor}.0") end) == + Connector.supported_versions() + end + end end diff --git a/test/neo4ex/utils_test.exs b/test/neo4ex/utils_test.exs index 004162c..67bdd54 100644 --- a/test/neo4ex/utils_test.exs +++ b/test/neo4ex/utils_test.exs @@ -19,9 +19,11 @@ defmodule Neo4ex.UtilsTest do describe "list_valid_versions/1" do test "filters invalid versions" do - assert [] == Utils.list_valid_versions(">= 5.0.0") + assert Enum.map(20..0//-1, fn minor -> Version.parse!("5.#{minor}.0") end) == + Utils.list_valid_versions(">= 5.0.0") - assert [Version.parse!("4.4.0"), Version.parse!("4.3.0")] == + assert Enum.map(20..0//-1, fn minor -> Version.parse!("5.#{minor}.0") end) ++ + [Version.parse!("4.4.0"), Version.parse!("4.3.0")] == Utils.list_valid_versions(">= 4.3.0") end end diff --git a/test/neo4ex_test.exs b/test/neo4ex_test.exs index 0c40fa2..2c201bc 100644 --- a/test/neo4ex_test.exs +++ b/test/neo4ex_test.exs @@ -23,7 +23,7 @@ defmodule Neo4exTest do encoded_success_message = Encoder.encode(success_message, "4.0.0") SocketMock - |> expect(:connect, fn 'localhost', 7687, [:binary, {:active, false}] -> + |> expect(:connect, fn ~c"localhost", 7687, [:binary, {:active, false}] -> :gen_tcp.listen(0, [:binary]) end) # handshake diff --git a/test/support/neo4j_connection.ex b/test/support/neo4j_connection.ex index 5aaeb7c..57ee564 100644 --- a/test/support/neo4j_connection.ex +++ b/test/support/neo4j_connection.ex @@ -8,8 +8,8 @@ defmodule Neo4ex.Neo4jConnection do alias Neo4ex.BoltProtocol.Encoder - def generate_message_chunk(message) do - encoded_message = Encoder.encode(message, "4.0.0") + def generate_message_chunk(message, version \\ "4.0.0") do + encoded_message = Encoder.encode(message, version) message_size = byte_size(encoded_message) <> end