# `AshNeo4j.Spatial`
[🔗](https://github.com/diffo-dev/ash_neo4j/blob/v0.10.1/lib/spatial.ex#L5)

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.

# `attr_or_path`

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

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

# `create_index`

```elixir
@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`

```elixir
@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`

```elixir
@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`)"]}

---

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