# Changelog

All notable changes to ActiveMemory are documented here. Versions follow
[Semantic Versioning](https://semver.org); while the package is pre-1.0 a minor
bump may carry a behavior change, and those are called out below.

## 0.8.2

### Fixed

- `use ActiveMemory.Store` and `use ActiveMemory.ActiveRepo` no longer swallow
  the using module's own `handle_info` clauses. The injected unknown-message
  catch-all is now added via `@before_compile` — after user-defined clauses —
  instead of at the `use` site above them, where it made every custom clause
  unreachable: timer ticks and monitor messages sent to a store were silently
  discarded, and Elixir 1.19's type checker flags the dead clauses as
  redundant. Library-owned messages (`:sweep`, `:"ETS-TRANSFER"`) keep
  priority; unknown messages still no-op.

## 0.8.1

### Added

- `mix active_memory.candidates` — reads table statistics through the
  application's own Ecto repo (PostgreSQL via `pg_stat_user_tables`,
  MySQL/MariaDB via `performance_schema`) and reports each table's read/write
  ratio, row count and size, flagging the high-read, low-write tables that are
  candidates for an ActiveMemory table. Thresholds are tunable with
  `--min-ratio`, `--max-rows` and `--min-reads`, and `--timeout` covers schemas
  whose statistics views are slow to answer.

  The task does **not** start your application: only its configuration is
  loaded, and the one repo you name is started with a two connection pool, so
  it is safe to point at a production database with read-only credentials. It
  runs only read-only queries against statistics views, never application
  tables. Known infrastructure tables (job queues, migration bookkeeping) are
  reported as such rather than as candidates, and tables with too little
  recorded traffic are not judged at all.

  Validated against PostgreSQL and against a production Aurora MySQL database.

### Changed

- ActiveMemory now requires Elixir 1.15 or later, matching the versions CI
  tests against. Earlier Elixirs may continue to work but are no longer
  resolved for or verified.

## 0.8.0

Ecto compatibility. A `Table` can now be typed or defined as an Ecto schema, and
the read API covers what `Ecto.Repo` offers.

### Added

- `field/3` in an `attributes` block takes an optional Ecto type —
  `field :name, :string` — defaulting to `:any`. Types are not enforced on write;
  they power `Ecto.Changeset` casting and validation. Available as
  `__attributes__(:types)`.
- A `Table` can skip `attributes` entirely and define an Ecto `embedded_schema`.
  All table metadata is derived from the schema, so the module is a real Ecto
  schema and every changeset function works on it.
- Autogenerated schema fields are honored on write: an `:id`/`:binary_id` primary
  key, a custom key such as `@primary_key {:uuid, Ecto.UUID, autogenerate: true}`,
  and `timestamps()`.
- `write/1` accepts an `Ecto.Changeset` as well as a struct, like
  `c:Ecto.Repo.insert/2`. An invalid changeset returns `{:error, changeset}` with its
  `action` set to `:insert`, which is what a Phoenix form needs to render errors.
- `get/1`, `get!/1`, `get_by/1`, `get_by!/1`, `one!/1`, `reload/1` and `reload!/1`.
  The bang variants raise `ActiveMemory.NotFoundError`.
- `count/1`, which asks the table for its size rather than copying records out, so
  it is O(1). On a `ttl` table it takes `sweep: true` to exclude expired but
  unswept records.
- `exists?/2`.
- `:order_by`, `:limit` and `:offset` on `all/1` and `select/2`. Sorting happens
  after reading, and uses a value's own `compare/2` when it has one, so `Decimal`
  and the calendar types order correctly rather than by Erlang term order.
- `ActiveMemory.NotFoundError` and `ActiveMemory.MultipleResultsError`.
- `__attributes__(:primary_key)`, the table's first field — what ETS and Mnesia
  key a record on.

### Changed

- **`one/1` now raises `ActiveMemory.MultipleResultsError`** when a query matches
  more than one record, instead of returning `{:error, :more_than_one_result}`.
  This matches `c:Ecto.Repo.one/2`. `withdraw/1` raises there too, since deleting an
  arbitrary one of several matches would be wrong. Callers matching on
  `{:error, :more_than_one_result}` need updating.
- An Ecto schema table that declares a primary key somewhere other than its first
  field, or a composite primary key, raises when the table is created. An in memory
  table keys a record on its first field, so neither can be honored, and the
  previous behavior would have read the wrong field.

### Fixed

- An Ecto schema table with an autogenerating primary key of a custom type
  (`@primary_key {:uuid, Ecto.UUID, autogenerate: true}`) generated no key, so
  every record was written under a `nil` key and collapsed onto one entry.
- Reconciling a recovered Mnesia table's options could crash the store's startup:
  an option change Mnesia refused raised out of `init/1`, and a table naming more
  than one copy type (`ram_copies` plus `disc_copies`, for instance) crashed the
  reconciliation even when the configuration was valid. A refused change is now
  logged as a warning and skipped, so the store starts and the table keeps its
  current setting. A copy configuration that is wrong in the code itself — the same
  node under two copy types — raises `ArgumentError`, since it would fail on every
  boot.
- Compile warnings from the test dependency `local_cluster` (deprecated `:slave`
  usage, charlist syntax) are gone with the upgrade to 2.1.

### Documentation

- A "Deleting a record" section explaining that `delete/1` matches a record in
  full, that a stale struct therefore removes nothing while still returning `:ok`,
  and that `withdraw/1` is the query based alternative.
- A `ttl` on an Ecto schema table needs an explicit `field :expires_at, :integer`;
  the `attributes` DSL still adds it for you.
- `Store` and `ActiveRepo` documentation now describes the same behavior for the
  operations they share.
- A "Testing" section covering which test modules can run `async: true` — a module
  owning its own table and store runs concurrently today — and which need
  `async: false`, namely modules sharing the application's store, since a table's
  module name is its physical table name.
- The `majority` option, Mnesia's quorum writes, is documented as the mitigation for
  partition behavior rather than one line in a list of options.

## 0.7.4

### Fixed

- `use ActiveMemory.Store` and `use ActiveMemory.ActiveRepo` read the table's `ttl`
  while the caller's module compiled, making every table a compile time dependency
  of its store. Tooling that compiles a store's file without the table module
  loaded — a language server, for one — raised `UndefinedFunctionError` on code
  that compiles and runs correctly. The lookup happens at runtime now.

## 0.7.3

### Fixed

- Removed an unreachable `ttl` clause that newer Elixir type checkers flagged in
  any project defining a store on a table without a `ttl`.
- Removed unreachable clauses in the ETS adapter's `select/2`.

## 0.7.2

### Changed

- `delete_all/0` returns `:ok` on success. The ETS adapter had returned the raw
  `true` from `:ets.delete_all_objects/1` and the Mnesia adapter `{:atomic, :ok}`,
  neither matching the documented `:ok | {:error, any()}` spec, so
  `:ok = Store.delete_all()` raised a `MatchError`.

## 0.7.1 and earlier

See the [commit history](https://github.com/SullysMustyRuby/active_memory/commits/main)
for releases before this changelog was kept. Notable additions were record expiry
(`ttl`), `ActiveMemory.ActiveRepo` for managing several tables from one process,
`ActiveMemory.TableHeir` so an ETS table survives its store crashing, and making
`withdraw/1` a single atomic operation.
