defmodule Cassandrax do @moduledoc ~S""" Cassandrax is a Cassandra ORM built on top of [Xandra](https://github.com/lexhide/xandra) and [Ecto](https://github.com/elixir-ecto/ecto). `Xandra` provides the driver for communication with the Database, and `Ecto` provides the data mapping functionality as well as changesets to control data mutations. Cassandrax is heavily inspired by [Triton](https://github.com/blitzstudios/triton) and [Ecto](https://github.com/elixir-ecto/ecto) projects, if you have used any of those projects, you'll get up to speed in no time to use Cassandrax. Cassandrax is split into 3 components following the design choices made on Ecto: * `Cassandrax.Keyspace` - keyspaces are the touchpoints with your Cassandra. Keyspaces provide the API for inserting, updating and deleting data from your tables, as well as querying the data already stored. Keyspaces need a `cluster` or a connection and a `name`. * `Cassandrax.Schema` - schemas are used to map data fetched from Cassandra into an Elixir struct. They use `Ecto.Schema` under the hood and should require a `@primary_key` to be defined before the table definition. * `Cassandrax.Query` - following `Ecto` design, they're written in Elixir syntax and are used to retrieve data from a Cassandra table. Queries are composable, even though they should remain as straight forward as possible. Unlike with relational databases, where you first model the data and their relationships and later you define how you should query that data, on Cassandra query design decisions are made when modeling the tables. For more information, please refer to [Cassandra Docs](https://cassandra.apache.org/doc/latest/data_modeling/index.html) Next we'll provide an overview of these components and how you'll use them to insert/change/fetch from/to Cassandra. Please check their corresponding module documentation for more in-depth description of the features available and options. ## Keyspaces `Cassandrax.Keyspace` is a wrapper around the keyspace. Each keyspace contains one or multiple tables and belongs to a cluster. A keyspace can be defined like so: defmodule SomeKeyspace do use Cassandrax.Keyspace, cluster: SomeCluster, name: "some_keyspace" end And the configuration for `SomeCluster` must be in your application environment, usually defined in your `config/config.exs`: config :cassandrax, clusters: [SomeCluster] config :cassandrax, SomeCluster, protocol_version: :v4, nodes: ["127.0.0.1:9042"] username: "cassandra", password: "cassandra", # cassandrax accepts all options you'd use in xandra Cassandrax automatically picks up these configs to start the pool of connections for each cluster. Keep in mind that all keyspaces that belong to the same cluster will share the same pool of connections. If you need your keyspace to have its own connection pool, please refer to the `Cassandrax.Keyspace` specific documentation. ## Schemas Schemas are used for table definition. Here's an example: defmodule UserByEmail do use Cassandrax.Schema # needs to be defined *before* defining the schema @primary_key [:email] # table name is users_by_email. Notice we don't need to set the keyspace here table "users_by_email" do field :email, :string field :id, :integer field :username, :string end end Cassandrax uses `Ecto.Schema` to define a struct with the schema fields: iex> user_by_email = %UserByEmail{email: "user@example.com"} iex> user_by_email.email "user@example.com" Just like with `Ecto`, this schema allows us to interact with keyspaces, like so: iex> user_by_email = %UserByEmail{email: "user@example.com", id: 123_456, username: "user"} iex> SomeKeyspace.insert!(user_by_email) %UserByEmail{...} Unlike relational databases which come with the autoincrement ID as default primary key, Cassandra requires you to define your own primary key. Therefore calling `Keyspace.insert/2` always returns the struct itself with updated metadata, but no changed fields. Also, bear in mind that Cassandra doesn't provice consistency guarantees the same way relational databases do, so the returned values of, for instance, deleting a record that doesn't exist anymore is the same as deleting an existing one: iex> user_by_email = %UserByEmail{email: "user@example.com", id: 123_456, username: "user"} # Store the result in a variable iex> result = SomeKeyspace.insert!(user_by_email) %UserByEmail{...} # Now delete the recently inserted record... iex> SomeKeyspace.delete!(result) %UserByEmail{...} # And if you try to delete a record that doesn't exist, no error is returned iex> SomeKeyspace.delete!(result) %UserByEmail{...} ## Queries Cassandrax provides you with a DSL so you can write queries in Elixir, lowering the chances of writing invalid CQL statements. In some occasions, `Cassandrax.Query` will validate your query at compile time and fail as soon as possible if your query is invalid: import Ecto.Query query = UserByEmail |> where(:email == "user@example.com") # Returns a List of %UserByEmail{} structs matching the query result = SomeKeyspace.all(query) Specific examples and detailed documentation for all available keywords are available in `Cassandrax.Query` module docs, but the supported keywords are: * `:allow_filtering` * `:distinct` * `:group_by` * `:limit` * `:order_by` * `:per_partition_limit` * `:select` * `:where` `Cassandrax.Keyspace` provides the same API as Ecto: You have `Keyspace.all/1` which returns all records matching a query, `Keyspace.one/1` which returns a single entry or raises and `Keyspace.get/2` which fetches an entry by its primary key. """ use Application def start(_type, _args) do Application.get_env(:cassandrax, :clusters, []) |> Enum.map(fn cluster -> config = Application.get_env(:cassandrax, cluster) |> ensure_cluster_config!(cluster) Cassandrax.Supervisor.child_spec(cluster, config) end) |> start_link() end def ensure_cluster_config!(empty, cluster) when is_nil(empty) or empty == [] do raise( Cassandrax.ClusterConfigError, "Expected to find keyword configs for #{inspect(cluster)}, found #{inspect(empty)}" ) end def ensure_cluster_config!(config, _cluster), do: config def start_link(children) do Supervisor.start_link(children, strategy: :one_for_one, name: Cassandrax.Supervisor) end def cql(conn, statement, values \\ [], opts \\ []) do case Cassandrax.Connection.prepare(conn, statement) do {:ok, prepared} -> Cassandrax.Connection.execute(conn, prepared, values, opts) {:error, error} -> {:error, error} end end end