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.
Summary
Functions
Areas one relation hop above any of these children, as a query.
The areas of a release, as a query.
Areas carrying any of these values under one code type, as a query.
Areas one relation hop below any of these parents, as a query.
Codes held by the areas of a release, as a query.
Names held by the areas of a release, as a query.
The schema prefix these schemas read, as validated at compile time.
Relation edges among the areas of a release, as a query.
Functions
@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.
@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.
@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.
@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.
@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.
@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.
@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.
@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.