# `GeoGenius.Published`
[🔗](https://github.com/agoodway/geo_genius/blob/main/lib/geo_genius/published.ex#L1)

Composable Ecto queries over the catalog's read views.

`GeoGenius.Query` builds queries over the catalog's `SETOF` plpgsql
functions. PostgreSQL cannot push a predicate, a join qualifier, or a `LIMIT`
inside one, so the whole set materialises before a host's own `WHERE` runs,
and the four functions project five of `area_match`'s sixteen columns. This
module reads the views those functions read, as ordinary Ecto schemas: the
planner gets a relation it can optimise, and the caller gets every column the
view carries, `release_id` among them.

Reach for this module for joins, aggregates, set-keyed reads, and anything
keyed on `release_id` -- a host projection table keyed
`(release_id, area_key)` is joinable only from here. Reach for `GeoGenius`
and `GeoGenius.Query` for spatial resolution, name search, and multi-level
traversal; those have no view-backed equivalent. `guides/sql_api.md` lays the
two side by side.

## Composing

Every function returns an `Ecto.Query` selecting whole schema structs, not a
list, and every source carries a named binding: `:area` for
`GeoGenius.Published.Area`, `:relation` for
`GeoGenius.Published.AreaRelation`, `:code` for
`GeoGenius.Published.AreaCode`, `:name` for `GeoGenius.Published.AreaName`.
Counting host records per child area, the case `GeoGenius.Query` serves
worst:

    from([area: area] in GeoGenius.Published.children_of("us:state:pa", types: ["city"]),
      left_join: record in MyApp.Record,
      on: record.area_key == area.area_key and record.release_id == area.release_id,
      group_by: area.area_key,
      select: {area.area_key, count(record.id)})

## Set-keyed by default

Where the function-backed API takes one `area_key`, these take a list, so
resolving the parents of three thousand areas is one query rather than three
thousand. A single binary is accepted as a one-element set. `children_of/2`
and `ancestors_of/2` return one row per relation edge, so an area reachable
from two of the parents given appears twice; the `:relation` binding says
which parent each row came from, and `Ecto.Query.distinct/3` collapses them
when the edge does not matter.

## Release and retirement scoping

Each `published_*` view is a release-scoped base view joined to the
publication pointer, so with no `:release_id` these queries read the
currently published release of every collection. Giving `:release_id` swaps
every source in the query onto its base -- `release_areas`,
`release_relations`, `release_area_codes`, `release_area_names` -- and reads
that release whether or not its collection publishes it, which is what a host
verifying a release or filling a projection ahead of go-live needs. The bases
project exactly the columns their published counterparts do, so nothing else
about a query changes.

The swap is all-or-nothing across the sources in one query, and every join
between two release-carrying sources equates their `release_id` as well as
the key it joins on. Both together are what keep a read on one release:
reading areas from a base and relations from a published view, or joining
them on `area_key` alone, would pair an edge of one release with an area of
another.

Retired areas are excluded unless `include_retired: true` is given, matching
every `SETOF` read function, on the published and unpublished paths alike.
`codes/1`, `names/1` and `relations/1` cannot honour it and do not take it:
their views carry no `retired_at`, because retirement is a property of an
area rather than of a code, a name, or an edge between two areas. So all
three return rows for retired areas too. Moving from `children_of/2` to
`relations/1` to reach an edge's measurement columns therefore widens the
result; join back through `areas/1` on `area_key` and `release_id` to narrow
it again.

## The compile-time prefix

`@schema_prefix` is read at compile time through `Application.compile_env/3`
and validated exactly as `GeoGenius.Query` validates its own. `:prefix`
therefore belongs in `config/config.exs` and must never be set in
`config/runtime.exs`; `GeoGenius.Query`'s moduledoc explains what breaks if
it is, and how to recompile after changing it. There is no per-call
`:prefix`, for the same reason there is none there: a host serving several
catalogs from one VM uses the struct-returning API in `GeoGenius`, which
resolves `:prefix` through `GeoGenius.Context` on every call.

# `ancestors_of`

```elixir
@spec ancestors_of([String.t()] | String.t(), keyword()) :: Ecto.Query.t()
```

Areas one relation hop above any of these children, as a query.

Same options as `children_of/2`.

# `areas`

```elixir
@spec areas(keyword()) :: Ecto.Query.t()
```

The areas of a release, as a query.

Accepts `:collections`, `:types`, `:area_keys`, `:release_id` and
`:include_retired`. Each list-valued option keeps the rows matching any of
its members and is skipped entirely when absent. `:release_id` accepts either
the hyphenated string every read projects or the sixteen raw bytes Postgrex
binds, and reads that release whether or not it is published.

# `areas_by_code`

```elixir
@spec areas_by_code(String.t(), [String.t()] | String.t(), keyword()) ::
  Ecto.Query.t()
```

Areas carrying any of these values under one code type, as a query.

Takes every option `areas/1` takes. The matched code is available through the
`:code` binding, which is what tells apart the areas a set of values matched.
A code is unique only within a parent, so scope a slug lookup by composing
this with `children_of/2`'s parent set rather than trusting a bare value.

# `children_of`

```elixir
@spec children_of([String.t()] | String.t(), keyword()) :: Ecto.Query.t()
```

Areas one relation hop below any of these parents, as a query.

Takes every option `areas/1` takes, plus `:classifications` to keep only
edges of given relation types. A walk deeper than one hop is
`GeoGenius.Query.children_of/2`'s `:max_depth`; a recursive walk has no
view-backed form here.

# `codes`

```elixir
@spec codes(keyword()) :: Ecto.Query.t()
```

Codes held by the areas of a release, as a query.

Accepts `:collections`, `:area_keys`, `:code_types` and `:release_id`. One
row per code an area of the release holds, where `GeoGenius.AreaMatch`
carries them collapsed into a JSON object. A code is a fact about the area,
so an area carried by several releases yields one row per release, each
stamped with its own `release_id`.

# `names`

```elixir
@spec names(keyword()) :: Ecto.Query.t()
```

Names held by the areas of a release, as a query.

Accepts `:collections`, `:area_keys`, `:kinds`, `:locales` and `:release_id`.
Every name of every kind, where `GeoGenius.Published.Area`'s `name` is the
single official one a trigger keeps current. Like `codes/1`, a name is a fact
about the area, so an area carried by several releases yields one row per
release.

# `prefix`

```elixir
@spec prefix() :: String.t()
```

The schema prefix these schemas read, as validated at compile time.

A host building SQL of its own against the same catalog reads it from here
rather than assuming the default.

# `relations`

```elixir
@spec relations(keyword()) :: Ecto.Query.t()
```

Relation edges among the areas of a release, as a query.

Accepts `:collections`, `:parent_area_keys`, `:child_area_keys`,
`:classifications` and `:release_id`. Use this to read an edge's measurement
columns, which no `area_match` read projects; use `children_of/2` or
`ancestors_of/2` to read the areas at the far end of one.

There is no `:include_retired`, and edges touching a retired area are always
returned: the relation views carry no `retired_at` to filter on, because
retirement is a property of an area rather than of an edge between two. A
caller coming from `children_of/2`, which excludes them by default, narrows
the result by joining `areas/1` back on `area_key` and `release_id`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
