AshNeo4j.Spatial (AshNeo4j v0.8.1)

Copy Markdown View Source

Convenience helpers for creating the Neo4j POINT indexes that back AshNeo4j's spatial pushdown (#275).

After #274, the indexable form of a geometry is a scalar Neo4j POINT companion that the data layer writes automatically alongside the canonical RFC 7946 GeoJSON:

  • a %Geo.Point{} gets one companion at <property>.point;
  • any other geometry gets two, <property>.bbSW / <property>.bbNE (the bounding-box corners point.withinBBox reads).

Hand-writing the index Cypher means knowing the resource's Neo4j label, the attribute → property translation, the companion suffix convention, and — for bbox geometries — that there are two companions. This module applies that internal knowledge for you, resolving everything from the resource module and attribute name.

# top-level Point attribute → one `.point` index
AshNeo4j.Spatial.create_index(Place, :location)

# non-Point geometry → both bbSW and bbNE indexes
AshNeo4j.Spatial.create_index(Place, :bounds)

# nested geometry — [attribute, field...] path into a TypedStruct
AshNeo4j.Spatial.create_index(Place, [:pet, :home])

# rebuild (DROP IF EXISTS + CREATE) after a storage-shape change
AshNeo4j.Spatial.create_index(Place, :location, recreate: true)

Consistent with AshNeo4j's "no migrations, index lifecycle is the operator's concern" stance (#45) — this is an ergonomic tool you choose to call (e.g. from a start-up task), not automatic behaviour. create_index/3 uses IF NOT EXISTS, so it's safe to run repeatedly. Indexes are schema objects independent of data: clearing nodes does not drop them, and they re-populate as nodes are written.

Naming

Each companion gets its own index named <base>_<suffix>, where suffix is point / bbSW / bbNE and base defaults to <label>_<dotted_property> (label lower-cased, dots as underscores) — e.g. place_location_point, place_bounds_bbSW. Pass name: to override the base.

Dry run

index_statements/3 returns the exact CREATE Cypher without touching the database — useful for review, a migration file, or testing.

Summary

Types

An attribute name, or a [attribute, field, ...] path into a nested geometry.

Functions

Creates the POINT index(es) backing spatial pushdown for attr_or_path.

Drops the POINT index(es) for attr_or_path (both corners for a bbox geometry). Uses IF EXISTS, so it's a no-op when absent.

Returns {:ok, statements} — the CREATE POINT INDEX Cypher that create_index/3 would run — without touching the database, or {:error, reason}. Honours :name; ignores :recreate.

Types

attr_or_path()

@type attr_or_path() :: atom() | [atom()]

An attribute name, or a [attribute, field, ...] path into a nested geometry.

Functions

create_index(resource, attr_or_path, opts \\ [])

@spec create_index(Ash.Resource.t(), attr_or_path(), keyword()) ::
  {:ok, [Bolty.Response.t()]} | {:error, term()}

Creates the POINT index(es) backing spatial pushdown for attr_or_path.

Returns {:ok, responses} — a list with one %Bolty.Response{} per index created (one for a Point, two for a bbox geometry) — or {:error, reason} on the first failure or if the attribute can't be resolved to a geometry.

Options

  • :recreate — when true, DROP INDEX ... IF EXISTS precedes each CREATE. Needed only when the index definition changes; data churn never requires it. Defaults to false.
  • :name — override the auto-derived base index name. The companion suffix (_point / _bbSW / _bbNE) is still appended.

drop_index(resource, attr_or_path, opts \\ [])

@spec drop_index(Ash.Resource.t(), attr_or_path(), keyword()) ::
  {:ok, [Bolty.Response.t()]} | {:error, term()}

Drops the POINT index(es) for attr_or_path (both corners for a bbox geometry). Uses IF EXISTS, so it's a no-op when absent.

Returns {:ok, responses} or {:error, reason}. Pass :name to match a base name overridden at create time.

index_statements(resource, attr_or_path, opts \\ [])

@spec index_statements(Ash.Resource.t(), attr_or_path(), keyword()) ::
  {:ok, [String.t()]} | {:error, term()}

Returns {:ok, statements} — the CREATE POINT INDEX Cypher that create_index/3 would run — without touching the database, or {:error, reason}. Honours :name; ignores :recreate.

AshNeo4j.Spatial.index_statements(Place, :location)
#=> {:ok, ["CREATE POINT INDEX place_location_point IF NOT EXISTS FOR (n:Place) ON (n.`location.point`)"]}