README.md

# Cassandrax

[![CI](https://github.com/loopsocial/cassandrax/actions/workflows/ci.yml/badge.svg)](https://github.com/loopsocial/cassandrax/actions/workflows/ci.yml)
[![Hex.pm](https://img.shields.io/hexpm/v/cassandrax.svg)](https://hex.pm/packages/cassandrax)

Cassandrax is a Cassandra data mapping toolkit built on top of
[Ecto](https://github.com/elixir-ecto/ecto)
and query builder and runner on top of [Xandra](https://github.com/lexhide/xandra).

Cassandrax is heavily inspired by the [Triton](https://github.com/blitzstudios/triton) and
[Ecto](https://github.com/elixir-ecto/ecto) projects. It allows you to build
and run CQL statements as well as map results to Elixir structs.

The docs can be found at [https://hexdocs.pm/cassandrax](https://hexdocs.pm/cassandrax).

## Installation

```elixir
def deps do
  [
    {:cassandrax, "~> 0.0.5"}
  ]
end
```

## Setup

```elixir
test_conn_attrs = [
  nodes: ["127.0.0.1:9043"],
  username: "cassandra",
  password: "cassandra"
]

# MyApp.MyCluster is just an atom
child = Cassandrax.Supervisor.child_spec(MyApp.MyCluster, test_conn_attrs)
Cassandrax.start_link([child])
```

Alternatively, if you're using CassandraDB on a Phoenix app, you can edit your
`config/config.exs` file to add Cassandrax to your supervision tree:

```elixir
# In your config/config.exs, you can add as many clusters as you like

config :cassandrax, clusters: [MyApp.MyCluster]

config :cassandrax, MyApp.MyCluster,
  protocol_version: :v4,
  nodes: ["127.0.0.1:9042"],
  pool_size: System.get_env("CASSANDRADB_POOL_SIZE") || 10,
  username: System.get_env("CASSANDRADB_USER") || "cassandra",
  password: System.get_env("CASSANDRADB_PASSWORD") || "cassandra",
  # Default write/read options
  write_options: [consistency: :local_quorum],
  read_options: [consistency: :one]
```

## Usage

You can easily define a Keyspace module that will act as a wrapper for
read/write operations:

```elixir
defmodule MyKeyspace do
  use Cassandrax.Keyspace, cluster: MyApp.MyCluster, name: "my_keyspace"
end
```

To define your schema, use the `Cassandrax.Schema` module, which provides the
`table` macro:

```elixir
defmodule UserById do
  use Cassandrax.Schema

  # Defines :id as partition key and :age as clustering key
  @primary_key [:id, :age]

  table "user_by_id" do
    field :id, :integer
    field :age, :integer
    field :user_name, :string
    field :nicknames, MapSetType
  end
end
```

While we work to support an actual migration DSL, you can run plain CQL statements to
migrate the database schema, like so:

```elixir
iex(1)> statement = """
   CREATE KEYSPACE IF NOT EXISTS my_keyspace
   WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1}
 """

# Creating the Keyspace
iex(2)> Cassandrax.cql(MyApp.MyCluster, statement)
{:ok,
 %Xandra.SchemaChange{
   effect: "CREATED",
   options: %{keyspace: "my_keyspace"},
   target: "KEYSPACE",
   tracing_id: nil
 }}

iex(3)> statement = """
   CREATE TABLE IF NOT EXISTS my_keyspace.user_by_id(
   id int,
   age int,
   user_name varchar,
   nicknames set<varchar>,
   PRIMARY KEY (id, age))
"""

# Creating the Table
iex(4)> Cassandrax.cql(MyApp.MyCluster, statement)
{:ok,
 %Xandra.SchemaChange{
   effect: "CREATED",
   options: %{keyspace: "my_keyspace", subject: "user_by_id"},
   target: "TABLE",
   tracing_id: nil
 }}
```

Keep in mind that in order to use your current Ecto Repo migrations to run the above
commands (always defining `up` and `down` functions separately), first you need to make
sure `:cassandrax` is started before migrations are ran. To do that, edit your
`config/config.exs` like so:

```elixir
config :my_app, MyApp.Repo, start_apps_before_migration: [:cassandrax]
```

Mutating data is as easy as it is with a regular Ecto schema. You can work
straight with structs, or with changesets:

#### Insert
```elixir
iex(5)> user =  %UserById{id: 1, user_name: "alice"}
%UserById{id: 1, user_name: "alice"}

iex(6)> MyKeyspace.insert(user) 
{:ok, %UserById{__meta__: %Ecto.Schema.Metadata{:loaded, "user_by_id"}, id: 1, user_name: "alice"}}

iex(7)> MyKeyspace.insert!(user)
%UserById{__meta__: %Ecto.Schema.Metadata{:loaded, "user_by_id"}, id: 1, user_name: "alice"}
```

#### Update
```elixir
iex(8)> changeset = Changeset.change(user, user_name: "bob")
#Ecto.Changeset<changes: %{user_name: "bob"}, ...>

iex(9)> MyKeyspace.update(changeset)
{:ok, %UserById{__meta__: %Ecto.Schema.Metadata{:loaded, "user_by_id"}, id: 1, user_name: "bob"}}

iex(10)> MyKeyspace.update!(changeset)
%UserById{__meta__: %Ecto.Schema.Metadata{:loaded, "user_by_id"}, id: 1, user_name: "bob"}
```

#### Delete
```elixir
iex(11)> MyKeyspace.delete(user)
{:ok, %UserById{__meta__: %Ecto.Schema.Metadata{:deleted, "user_by_id"}, id: 1, user_name: "bob"}}

iex(12)> MyKeyspace.delete!(user)
%UserById{__meta__: %Ecto.Schema.Metadata{:deleted, "user_by_id"}, id: 1, user_name: "bob"}
```

#### Batch operations

You can issue many operation at once with a `BATCH` operation. For more
information on how Batches work in Cassandra DB, please refer to [CassandraDB
Batches](https://cassandra.apache.org/doc/latest/cql/dml.html#batch).

```elixir
iex(13)> user = %UserById{id: 1, user_name: "alice"}
%UserById{id: 1, user_name: "alice"}

iex(14)> changeset = MyKeyspace.get(UserById, id: 2) |> Changeset.change(user_name: "eve")
#Ecto.Changeset<changes: %{user_name: "eve", ...}>

iex(15)> MyKeyspace.batch(fn batch ->
  batch
  |> MyKeyspace.batch_insert(user)
  |> MyKeyspace.batch_update(changeset)
 end)
:ok
```

#### Querying

##### Disclaimer
> One thing to keep in mind when it comes to querying is the API is still under
development and, therefore, can still change in version prior to `0.1.0`.
>
> If you use it in production, be cautious when updating `cassandrax`, and make
sure all your queries work correctly after installing the new version.

`Cassandrax` queries are very similar to `Ecto`'s, you can use the `all/2`, `get/2`
and `one/2` functions directly from your Keyspace module.

```elixir
iex(16)> MyKeyspace.get(UserById, [id: 1, age: 20])
%UserById{__meta__: %Ecto.Schema.Metadata{:loaded, "user_by_id"}, id: 1, age: 20, user_name: "alice"}

iex(17)> MyKeyspace.all(UserById)
[
  %UserById{__meta__: %Ecto.Schema.Metadata{:loaded, "user_by_id"}, id: 1, user_name: "alice"},
  %UserById{__meta__: %Ecto.Schema.Metadata{:loaded, "user_by_id"}, id: 2, user_name: "eve"},
  ...
]
```

Also, you can use `Cassandrax.Query` macros to build your own queries.

```elixir
iex(18)> import Cassandrax.Query
true

iex(19)> UserById |> where(id: 1, age: 20) |> MyKeyspace.one()
%UserById{__meta__: %Ecto.Schema.Metadata{:loaded, "user_by_id"}, id: 1, age: 20, user_name: "alice"}

# Remember when filtering data by non-primary key fields, you should use ALLOW FILTERING:
iex(20)> UserById
  |> where(id: 3)
  |> where(:user_name == "adam")
  |> where(:age >= 30)
  |> allow_filtering()
  |> MyKeyspace.all()
[%UserById{__meta__: %Ecto.Schema.Metadata{:loaded, "user_by_id"}, id: 3, user_name: "adam", age: 31}}]
```