lib/ex4j.ex

defmodule Ex4j do
  @moduledoc """
  An Ecto-inspired Cypher DSL and Neo4j driver for Elixir.

  Ex4j provides a macro-based query builder that compiles Elixir expressions
  into parameterized Cypher queries, preventing injection and enabling
  Neo4j query plan caching.

  ## Setup

  Add the dependency:

      def deps do
        [{:ex4j, "~> 2.0"}]
      end

  Configure the connection:

      config :ex4j, Boltx,
        url: "bolt://localhost:7687",
        basic_auth: [username: "neo4j", password: "password"],
        pool_size: 10

      # For Neo4j Aura:
      config :ex4j, Boltx,
        url: "neo4j+s://your-instance.databases.neo4j.io",
        basic_auth: [username: "your_username", password: "your_password"],
        database: "your-database-name",
        pool_size: 5

  Define a Repo:

      defmodule MyApp.Repo do
        use Ex4j.Repo, otp_app: :my_app
      end

  ## Defining Schemas

      defmodule MyApp.User do
        use Ex4j.Schema

        node "User" do
          field :name, :string
          field :age, :integer
          field :email, :string
        end

        def changeset(user, attrs) do
          user
          |> cast(attrs, [:name, :age, :email])
          |> validate_required([:name, :email])
        end
      end

      defmodule MyApp.Comment do
        use Ex4j.Schema

        node "Comment" do
          field :content, :string
        end
      end

      defmodule MyApp.HasComment do
        use Ex4j.Schema

        relationship "HAS_COMMENT" do
          from MyApp.User
          to MyApp.Comment
          field :created_at, :utc_datetime
        end
      end

  ## Building Queries

      import Ex4j.Query.API

      # Read with macro-based WHERE (parameterized, safe)
      User
      |> match(as: :u)
      |> where([u], u.age > 18 and u.name == "Tiago")
      |> return([:u])
      |> limit(10)
      |> MyApp.Repo.all()

      # Runtime values with pin operator
      name = "Tiago"
      User
      |> match(as: :u)
      |> where([u], u.name == ^name)
      |> return([u], [:name, :age])
      |> MyApp.Repo.all()

      # Relationship traversal
      User
      |> match(as: :u)
      |> edge(HasComment, as: :r, from: :u, to: :c, direction: :out)
      |> match(Comment, as: :c)
      |> where([c], c.content =~ "Article")
      |> return([:u, :c])
      |> MyApp.Repo.all()

      # CREATE
      query()
      |> create(User, as: :u, set: %{name: "Alice", age: 30, email: "alice@example.com"})
      |> return([:u])
      |> MyApp.Repo.run()

      # MERGE + SET
      query()
      |> merge(User, as: :u, match: %{email: "alice@example.com"})
      |> set(:u, :name, "Alice Updated")
      |> return([:u])
      |> MyApp.Repo.run()

      # DELETE
      User
      |> match(as: :u)
      |> where([u], u.name == "Alice")
      |> delete(:u, detach: true)
      |> MyApp.Repo.run()

      # Dynamic queries for runtime conditions
      dynamic = Enum.reduce(filters, dynamic([u], true), fn
        {:name, name}, dyn -> dynamic([u], ^dyn and u.name == ^name)
        {:min_age, age}, dyn -> dynamic([u], ^dyn and u.age >= ^age)
      end)

      User |> match(as: :u) |> where(^dynamic) |> return([:u]) |> MyApp.Repo.all()

      # Fragment for raw Cypher
      User
      |> match(as: :u)
      |> where([u], fragment("u.score > duration(?)", "P1Y"))
      |> return([:u])
      |> MyApp.Repo.all()

      # Raw Cypher query (full escape hatch)
      MyApp.Repo.query("MATCH (n:User) RETURN n LIMIT 25")

      # Transactions
      MyApp.Repo.transaction(fn ->
        MyApp.Repo.run(create_query)
        MyApp.Repo.run(relationship_query)
      end)

  ## Cypher 25 Support

  Ex4j supports the latest Cypher 25 features including:
  - Walk semantics (REPEATABLE ELEMENTS)
  - Vector operations (vector(), vector_distance(), etc.)
  - Full aggregation function set
  - Subqueries with CALL {}
  - UNION / UNION ALL
  - Variable-length relationship patterns
  """
end