Skip to main content

CHANGELOG.md

# Changelog

## 0.3.0 — 2026-06-30

### Added

- **Per-shape prepared-statement cache names.** `insert_all/4` now derives the
  Postgrex `:cache_statement` name from the table **and the unnest arity** (the
  number of array columns), e.g. `ecto_unnest_all_events_2`. Ecto's default keys
  `insert_all` on the table alone (`ecto_insert_all_events`), but `unnest` renders
  different SQL per column shape, so distinct shapes for one table would otherwise
  collide on one cache slot and force a re-prepare on every shape change. The row
  count never enters the name (the SQL is constant across it). Pass
  `cache_statement: "name"` to override, or `cache_statement: nil` to restore
  Ecto's default. (Arity does not separate same-arity/different-column shapes — give
  those an explicit `:cache_statement`.)
- **`:require_all_fields` completeness check.** With `require_all_fields: true`
  every schema field must appear in the columns map or in `:placeholders`, else the
  call raises listing what is missing — catching a forgotten column that would
  otherwise silently take its DB default. The autogenerated primary key and
  `timestamps()` fields are exempt. Schema sources only (a binary source has no
  field list and raises). Defaults to `config :ecto_unnest, :require_all_fields`
  (so it can be a test-only safety net), else `false`; an explicit per-call option
  always wins.

## 0.2.0 — 2026-06-24

### Added

- **Per-row JSON columns (`jsonb` / `{:array, :map}`).** A column whose per-row
  value is itself JSON is now shipped as a 1-D `text[]` of pre-encoded JSON with a
  `::jsonb` cast in the `SELECT` projection (instead of a `::jsonb[]` param, which
  `unnest` flattened and Postgrex double-encoded). Schema sources auto-detect
  `:map` / `{:map, _}` / `{:array, :map}`; binary sources opt in with
  `types: %{col: :jsonb}` or the new `:json` option. Raw terms are encoded with the
  configured `:postgrex` `:json_library`; already-encoded strings pass through.
- **Full `Ecto.Query` as `:on_conflict`.** Enables a conditional
  `ON CONFLICT ... DO UPDATE SET ... WHERE <predicate>` (and `ORDER BY`), matching
  `Ecto.Repo.insert_all/3`.
- **`:types` accepts atoms** as well as strings.
- **`config :ecto_unnest, :allowed_types`** — an explicit allow-list of custom PG
  types permitted in a `:types` override.

### Security

- **`:types` overrides are now fail-closed.** The value is rendered into the SQL
  cast verbatim, so it is validated: Ecto's default PG types (`bigint`, `text`,
  `jsonb`, `timestamptz`, …) are always allowed, but any other spelling (a domain,
  an alias like `int4`, a modifier) must be listed in
  `config :ecto_unnest, :allowed_types` or the call raises. Apps using `:types`
  with non-default types (including binary sources, which require `:types`) must add
  this config.

### Fixed

- **Placeholders no longer require a `pg_type`.** `resolve_pg_type!` is skipped for
  placeholder (`:scalar`) columns, since their cast comes from `type(^value, type)`
  in the projection, not the `unnest` param. Integer-backed `Ecto.Enum`, date and
  uuid placeholders now work on schema sources with no `:types` entry.
- **Binary-source non-string placeholders** can be cast via `:types` (e.g.
  `types: %{created_at: :timestamptz}`), including custom PG types such as a domain
  `:kafka_topic_name`, rendered as a raw `::type` cast on the parameter.

### Notes

- Binary-source column alignment was confirmed correct on execution: both the
  executed `Repo.insert_all/3` and `to_sql/3` derive the `INSERT` column list from
  the same name-ordered `SELECT`, and the projection references `unnest` columns by
  name — so values land in their named column regardless of physical column order.
  A regression test guards this.