README.md

# 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 (including Postgres schema) is inferred from your existing module.

```elixir
    defmodule YourApp.Voltage do
      use Ecto.Schema

      # Add this block to the schema you want to partition
      use PartitionedSchema,
        repo: MyApp.Repo,
        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
```

## Tests

To run the tests, make sure to create the database first via
`MIX_ENV=test mix ecto.create`

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>.