# PartitionedSchema
Adds partition maintenance helpers to an Ecto schema module.
## Usage
Add the `PartitionedSchema` definition to an Ecto.Schema you want to partition.
The table name accepts a schema prefix in case your table does not live in public
eg: `table.name: "customschema.voltages"`.
```elixir
defmodule YourApp.Voltage do
use Ecto.Schema
# Add this block to the schema you want to partition
use PartitionedSchema,
repo: MyApp.Repo,
table_name: "voltages",
partition_column: :ts,
partition_column_type: :timestamptz, # :date | :timestamptz | :naive_datetime
partition_type: :weekly, # :daily | :weekly | :monthly
retention: 30, # days, or {:days, n}, {:weeks, n}, {:months, n}, or nil
timezone: "Etc/UTC" # used for DateTime boundaries
schema "voltages" do
field :device_id, :id
field :ts, :utc_datetime_usec
field :voltage, :float
end
end
```
Note that if you set the `:retention` to `nil` no automatic cleanup will occur.
New partitions will be added and old partitions remain untouched.
Then add the `PartitionMaintainer` to your applications supervisor as in the
following, abbreviated example. The `:interval` option specifies how often the
maintainer process should check for whether partitions need to be rotated.
The `partitions` option is a list of modules that use `PartitionedSchema`.
If you run in a clustered environment (ie Erlang distribution), the maintainer
process will run on each node but for rotating the partitions a Postgres
`pg_try_advisory_xact_lock` [Postgres Docs](https://www.postgresql.org/docs/18/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS)
is aqcuired so it can only happen once at a time evenif multiple
PartitionMaintainer processes attempt it at the same exact time.
```elixir
defmodule YourApp.Application do
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
…
{PartitionedSchema.PartitionMaintainer, repo: YourApp.Repo, partitions: [YourApp.Voltage], interval: :timer.minutes(1)},
…
] ++ maybe_include_push_processes()
opts = [strategy: :one_for_one, name: YourApp.Supervisor]
Supervisor.start_link(children, opts)
end
```
Your parent table must already exist and be partitioned. Here is an example
migration:
```elixir
defmodule YourApp.Repo.Migrations.AddPartitionedTable do
use Ecto.Migration
def up do
execute """
CREATE TABLE voltages (
device_id bigint NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
ts timestamptz NOT NULL,
voltage int NOT NULL,
PRIMARY KEY (device_id, ts)
) PARTITION BY RANGE (ts);
"""
end
def down do
execute "DROP TABLE voltages;"
end
end
```
This module creates partitions with:
CREATE TABLE IF NOT EXISTS <child> PARTITION OF <parent>
FOR VALUES FROM ('...') TO ('...');
Notes:
- Partition ranges are inclusive on start and exclusive on end (Postgres semantics).
- By default, weekly boundaries are ISO weeks (week starts Monday) when using dates.
- For timestamptz partitions, boundaries are computed in `timezone` and then stored as UTC instants.
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `partitioned_schema` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:partitioned_schema, "~> 0.9.1"}
]
end
```
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/partitioned_schema>.