From 35b174a76591bec4b5c957cc1bdfc1f1e6434505 Mon Sep 17 00:00:00 2001 From: Michal Szewczyk Date: Mon, 16 Dec 2024 16:47:21 +0100 Subject: [PATCH] Improvements for Neo4j v5 engines --- .github/workflows/elixir.yml | 4 +- .tool-versions | 4 +- README.md | 8 ++- docker-compose.yml | 5 +- example_app/config/config.exs | 11 ++-- example_app/lib/example_app/application.ex | 2 +- ...t_sips_benchmark.ex => boltx_benchmark.ex} | 12 ++-- example_app/mix.exs | 2 +- example_app/mix.lock | 9 +-- lib/neo4ex/bolt_protocol.ex | 19 +++--- .../structure/message/extra/hello.ex | 14 +++- .../structure/message/extra/logon.ex | 12 ++++ .../structure/message/request/logon.ex | 7 +- lib/neo4ex/connector.ex | 9 ++- lib/neo4ex/utils.ex | 4 +- mix.exs | 2 +- mix.lock | 23 +++---- test/neo4ex/bolt_protocol/encoder_test.exs | 65 ++++++++++++++++--- test/neo4ex/utils_test.exs | 6 +- test/neo4ex_test.exs | 2 +- 20 files changed, 152 insertions(+), 68 deletions(-) rename example_app/lib/mix/tasks/{bolt_sips_benchmark.ex => boltx_benchmark.ex} (69%) create mode 100644 lib/neo4ex/bolt_protocol/structure/message/extra/logon.ex diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 8b13252..3848727 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -16,8 +16,8 @@ jobs: strategy: matrix: - iex: [1.14.3] - otp: [24.3.4, 25.2] + iex: [1.14.5, 1.15.8, 1.16.3, 1.17.3] + otp: [24.3.4, 25.3, 26.2, 27.2] steps: - uses: actions/checkout@v3 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 69% rename from example_app/lib/mix/tasks/bolt_sips_benchmark.ex rename to example_app/lib/mix/tasks/boltx_benchmark.ex index 60d6f3a..419f11e 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 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/message/extra/hello.ex b/lib/neo4ex/bolt_protocol/structure/message/extra/hello.ex index 3891227..7f340a7 100644 --- a/lib/neo4ex/bolt_protocol/structure/message/extra/hello.ex +++ b/lib/neo4ex/bolt_protocol/structure/message/extra/hello.ex @@ -1,9 +1,21 @@ defmodule Neo4ex.BoltProtocol.Structure.Message.Extra.Hello do use Neo4ex.BoltProtocol.Structure + @version Mix.Project.config()[:version] + @system_info System.build_info()[: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_info}" + }, + version: ">= 5.3.0" + ) + field(:patch_bolt, default: ["utc"], version: ">= 4.3.0 and <= 4.4.0") field(:routing, default: %{}, version: ">= 4.1.0") 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..0b65a08 --- /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, default: "") + field(:principal, default: "") + field(:credentials, default: "") + 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..dd986b8 100644 --- a/mix.exs +++ b/mix.exs @@ -40,7 +40,7 @@ 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]}, diff --git a/mix.lock b/mix.lock index 3c7eba9..3529aa3 100644 --- a/mix.lock +++ b/mix.lock @@ -2,17 +2,18 @@ "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, "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"}, + "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.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"}, + "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, "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"}, + "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..8569c16 100644 --- a/test/neo4ex/bolt_protocol/encoder_test.exs +++ b/test/neo4ex/bolt_protocol/encoder_test.exs @@ -10,6 +10,8 @@ defmodule Neo4ex.BoltProtocol.EncoderTest do alias Neo4ex.BoltProtocol.Encoder 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 +27,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 +80,43 @@ defmodule Neo4ex.BoltProtocol.EncoderTest do end test "handles encoding of Hello messages" do + app_version = "Neo4ex/#{@version}" user_agent_bytes = byte_size("user_agent") - ua_bytes = byte_size("Neo4ex/0.1.0") + ua_bytes = byte_size(app_version) 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">> = + # even though Logon is a struct with prefdefined fields order, prior to 5.1 we're encoding it to the map so the keys will be sent according to map keys rules (compiler-defined, should be the same but can be random) + assert <<0xB1, 0x01, 0xA4, 0x8::4, ^scheme_bytes::4, "scheme", 0x8::4, ^none_bytes::4, + "none", 0x8::4, ^credentials_bytes::4, "credentials", 0x80, 0x8::4, + ^user_agent_bytes::4, "user_agent", 0x8::4, ^ua_bytes::4, ^app_version::binary, + 0x8::4, ^principal_bytes::4, "principal", + 0x80>> = Encoder.encode(%Hello{extra: %Extra.Hello{scheme: "none"}}, "4.0.0") end + + test "handles encoding of Hello messages for >= 5.3" do + app_version = "Neo4ex/#{@version}" + elixir_version = "Elixir/#{System.build_info()[:version]}" + user_agent_bytes = byte_size("user_agent") + ua_bytes = byte_size(app_version) + routing_bytes = byte_size("routing") + bolt_agent_bytes = byte_size("bolt_agent") + bolt_agent_product_bytes = byte_size("product") + bolt_agent_language_bytes = byte_size("language") + bolt_agent_language_value_bytes = byte_size(elixir_version) + + # even though Logon is a struct with prefdefined fields order, prior to 5.1 we're encoding it to the map so the keys will be sent according to map keys rules (compiler-defined, should be the same but can be random) + assert <<0xB1, 0x01, 0xA3, 0x8::4, ^routing_bytes::4, "routing", 0xA0, 0x8::4, + ^user_agent_bytes::4, "user_agent", 0x8::4, ^ua_bytes::4, ^app_version::binary, + 0x8::4, ^bolt_agent_bytes::4, "bolt_agent", 0xA2, 0x8::4, + ^bolt_agent_product_bytes::4, "product", 0x8::4, ^ua_bytes::4, + ^app_version::binary, 0x8::4, ^bolt_agent_language_bytes::4, "language", 0x8::4, + ^bolt_agent_language_value_bytes::4, + ^elixir_version::binary>> = + Encoder.encode(%Hello{extra: %Extra.Hello{}}, "5.3.0") + 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