Packages
The typed, attribute-queryable in-memory store for ETS and Mnesia, with Ecto changeset support, record expiry (TTL), crash resilience, and atomic take-once reads.
Current section
Files
Jump to
Current section
Files
active_memory
CHANGELOG.md
CHANGELOG.md
# 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.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.