All notable changes to this project will be documented in this file. See Conventional Commits for commit guidelines.
v0.10.0 (2026-06-21)
This release opened on functionality and pivoted to industrialisation and integrity: the write path is now atomic and constraint-backed, the read path gains a graph-native traversal expression, and the whole data layer returns typed errors rather than raising. The headline is graph traversal as a first-class Ash expression — a multi-hop path that pushes down to Cypher and composes with the rest of your filter, which a relational data layer structurally cannot offer.
Breaking Changes
AshNeo4j.Types.*→AshNeo4j.Type.*(#323) — the type namespace is renamed to singular, matchingAsh.Type. UpdateAshNeo4j.Types.Vector→AshNeo4j.Type.Vector(and any otherAshNeo4j.Types.*reference) in your attribute declarations. No storage or behaviour change.
Features
Graph traversal as an Ash expression (#321) —
traverse(^hop_chain, projection)expresses a multi-hop, direction-and-type-selected path inside anAsh.Expr, and the data layer pushes it down to a Cypher path pattern instead of an imperative load-time Elixir walk.hop_chainis a list of{:forward | :reverse, edge_selector}hops;edge_selectoris an Ash relationship name or an explicit{:edge, label}/{:edge, label, dest}. The reached node composes as a value in afilter:- reached-node field comparison —
filter(traverse(^chain, :status) == "active") - spatial composition (#330, #332) —
filter(st_dwithin(traverse(^chain, :location), ^point, 5_000))("services whose site is within 5 km of a point") in one query - membership / cardinality (#334) —
traverse(^chain, :exists) == true,traverse(^chain, :count) > 0 field aggregates (#338) —
traverse(^chain, {:min | :max | :avg | :sum, :field}) <op> value- reverse-terminal node typing (#336)
This is radical for Ash: relational data layers model relationships as joins and have no notion of a path as an expression value — so this isn't parity work, it's a graph-native differentiator. The filter context ships now;
sort(#335),calculate/policy, and variable-length are tracked on the open epic #321. Seeusage-rules/traverse.md.- reached-node field comparison —
Read-time polymorphic projection —
AshNeo4j.Calculations.ProjectedTraversal+AshNeo4j.Unknown(#329) — a calculation that follows a hop chain and returns the reached node, late-binding its concrete type at read time. IntroducesAshNeo4j.Unknown, a first-class value complementary toAsh.NotLoaded:NotLoadedmeans "not fetched yet";Unknownmeans "reached, but couldn't be determined in the current view of the graph". Never collapse it intonil.Atomic & bulk writes (#361) — atomic updates render
changeset.atomicsstraight to a CypherSET(numeric, stringconcat/trim, and enum/atom forms); bulk update and destroy run as a singleupdate_query/4/destroy_query/4viaAsh.bulk_update/Ash.bulk_destroywithstrategy: :atomic; a single filtered (optimistic-lock) update or destroy whose guard no longer holds returnsAsh.Error.Changes.StaleRecordrather than a silent no-op.Atomic upsert (#379) — create-or-update keyed on an identity renders an atomic Cypher
MERGE, so concurrent upserts converge on one node instead of racing to duplicates.Identities & primary keys as Neo4j uniqueness constraints (#20, #32) —
AshNeo4j.Constraint.create_constraints/1buildsCREATE CONSTRAINT … IS UNIQUEfor every enforceable identity and for the primary key (single and composite, Community Edition). A conflict surfaces as Ash's ownAsh.Error.Changes.InvalidAttribute("has already been taken"), sopre_check?and its race window are no longer needed. Identities Neo4j can't enforce (nils_distinct?: false, filteredwhere:) are refused rather than silently unenforced. Like indexes, AshNeo4j runs no migrations on boot — you invoke the helper. Seeusage-rules/identities.md.Typed tensor attribute —
AshNeo4j.Type.NxTensor(#309) — a shape-and-element-typed tensor backed byNx.Tensor, rank 1 to 3 (vector / matrix / 3-tensor), stored row-major as a native propertyLIST(:property, default) or a base64 binary blob (:packed); neither type nor shape is stored — both are declared constraints recovered on read. Foundation slice of the hybrid tensor/compute epic (#308); structural ops areNx's own (the value is anNx.Tensor).Dynamic node labels (#339) — a capability + pattern-position render primitive letting a label be supplied at query time (Cypher 5 ≥ 5.26), groundwork for polymorphic-label reads.
Cypher fragment filter escape hatch (#33) —
cypher_fragment(...)drops a raw, parameterised Cypher predicate into afilterfor the rare case the expression surface can't reach, without abandoning the data layer. A caveated last resort, not a default. Seeusage-rules/cypher-fragments.md.Query results as Mermaid flowcharts (#60) —
AshNeo4j.Mermaidrenders a graph-level query result as a Mermaid diagram for docs and Livebooks.APOC availability healthcheck (#386) — detects whether APOC procedures are installed on the connected server, so APOC-dependent paths can degrade explicitly.
Nested arrays (#317) —
{:array, {:array, _}}round-trips via an outer nativeLISTwith inner JSON.
Improvements
The data layer returns typed errors, never raises (#342, #350, #358, #372) — every read/write-path failure is a returned
{:error, Splode}with a class, not a raised string. New errors:UnresolvableTraversal(a traverse filter that can't be formed — never a fabricated edge),GeoDimensionMismatch,Unsupported3DGeometry,RequiresCypher25. Neo4j server errors are surfaced and classified rather than flattened. Bare-string errors throughout the data layer are now typed (#372).Many-to-many modelled as back-to-back
has_manyfails fast (#127) — with a clear error pointing at a joiner resource node, instead of silently mis-relating.Guarded relationship attach/detach honours
changeset.filter(#368) — aStaleRecordon miss, consistent with the guarded update/destroy path.Type-check gate (#347) — CI compiles
--warnings-as-errorsand runs Dialyzer on a clean baseline; the test suite compiles warning-free.Logging unified (#373, #374) — one data-layer log format; "nothing deleted" demoted from error to debug.
CYPHER 5 sunset tripwire (#363) and
bolty0.2.0 (#362); toolchain bumped to Elixir 1.20 / OTP 29 and Neo4j 5 to 5.26.27 (#318, #320).
v0.9.0 (2026-06-09)
Breaking Changes
- Minimum Neo4j / Bolt raised (#292, #293) — bumps to
bolty0.1.0, which speaks Bolt 5.6–6.0, so Neo4j 4.x (Bolt 4.x) is no longer supported. Flagged as breaking for completeness, but it should break no one in practice: Neo4j 4.x is end-of-life and not considered secure, so no realistic ash_neo4j deployment targets it.
Features
Vector embeddings & similarity search (#74) — a new
AshNeo4j.Types.Vectorattribute type, stored as a Neo4jLIST<FLOAT>, withvector_similarity(cosine) andvector_cosine_distanceAsh query functions, k-NN ordering pushed down to Cypher, andAshNeo4j.Vectorindex-lifecycle helpers. Vector predicates and ordering route to a Cypher-25-capable pool. Because the values persist asLIST<FLOAT>(the nativeVECTORtype can't be a node property), the feature is gated on Cypher 25.WGS-84-3D points (#270, Phase 1) —
%Geo.PointZ{}(srid 4979) stores as a native 3D Neo4jPOINTat<attr>.point, withpoint.distancepushdown in 3D and an in-memory haversine (mean-height-scaled arc + Δh) that matches Neo4j to ~0.1 m. A strict dimension policy stops 2D and 3D mixing silently — a mismatch raisesAshNeo4j.Error.GeoDimensionMismatch, andAshNeo4j.Geo.force_2d/1does an explicit downward projection. 3D areal/linear geometry (PolygonZ, …) raisesAshNeo4j.Error.Unsupported3DGeometry, deferred to Phase 2.CYPHER 25 language selector (#292, #293) — on Neo4j ≥ 2025.06, AshNeo4j auto-prepends
CYPHER 25to generated queries, opting into the versioned Cypher 25 language; older servers stay on Cypher 5. The selector is derived from the server version and cached per pool, and is distinct from the Bolt protocol version.
Bug Fixes
Spatial POINT index now effective at scale (#311) — two issues left geospatial queries unindexed despite a POINT index existing. A
:point_zattribute built indexes on the.bbSW/.bbNEcompanions a%Geo.PointZ{}never writes (it now indexes the.pointit stores), and thewithin_bbox/within_bbox_boxcontainment form put the indexed properties in the box position, forcing aNodeByLabelScan. The containment predicates are reformulated into range scans on the indexed corners (NodeIndexSeekByRange). Benchmarked at N=10k: indexedst_dwithin~6–7×; point-in-polygon containment is index-servable but caps near ~1.3× (single-corner seek).existsover an empty result isfalse, notnil(#301).CYPHER 25 selector emitted once (#299) — inside a
CALL {…}combination block the selector was prepended per branch and again on the outer query; it is now emitted only once, on the outer query.Root-node aggregates (#291) — an aggregate with no relationship path now aggregates over the root node, not an unbound relationship variable.
Domain-fragment label resolution (#295) — uses
Code.ensure_compiled/1so a domain-fragment label resolves under any compilation order.Test-suite stability (#304) — caps ExUnit
max_casesto the Bolt pool size, removing intermittentDBConnection:queue_timeoutfailures under parallel tests (test-only; no runtime effect).
v0.8.1 (2026-05-31)
Bug Fixes:
drop stale geo companions when a geo value changes shape (#287) by Matt Beanland
scope node-read pagination to nodes, not edge rows (#285) by Matt Beanland
clear geo companions when a geo attribute is cleared to nil (#283) by Matt Beanland
v0.8.0 (2026-05-28)
Breaking Changes
Spatial storage rearchitecture (#274) — the spatial surface introduced in 0.7.0 is replaced. The
AshNeo4j.Type.PointandAshNeo4j.Type.Boxmodules are removed; spatial attributes now useash_geotypes and carry%Geo.*{}structs.Bolty.Types.Pointno longer appears at the Ash boundary (it was a driver-layer type leaking through). Migration:attribute :loc, AshNeo4j.Type.Point→attribute :loc, AshGeo.GeoJson, constraints: [geo_types: [:point], force_srid: 4326]attribute :b, AshNeo4j.Type.Box→attribute :b, AshGeo.GeoJson, constraints: [geo_types: [:polygon], force_srid: 4326](Box was always proto-Polygon; axis-aligned validation is now an application-layer concern)- values:
Bolty.Types.Point.create(:wgs_84, lng, lat)→%Geo.Point{coordinates: {lng, lat}, srid: 4326};%AshNeo4j.Type.Box{sw, ne}→%Geo.Polygon{coordinates: [ring], srid: 4326} - on-disk shape changed: Point's native Point moves from
<attr>to<attr>.pointand gains a<attr>.jsoncanonical; Box's 4-Point array becomes<attr>.json+<attr>.bbSW/<attr>.bbNE. Existing 0.7.0 spatial nodes need re-creation or a one-shot migration cypher (AshNeo4j ships no migrations by design).
Adds
ash_geo ~> 0.3as a runtime dependency (clean —jason/geo/ash; the PostGIS-flavoured deps are test-only in ash_geo).
Features
Full GeoJSON geometry surface (#274) —
AshGeo.GeoJson/AshGeo.GeoAnyattributes support all RFC 7946 geometry types:Point,LineString,Polygon,MultiPoint,MultiLineString,MultiPolygon. The data layer detects geometry values (classification:geoinTypeClassifier) and stores them as a canonical RFC 7946 GeoJSONSTRINGat<attr>.jsonplus indexable scalar Point companions — native<attr>.pointfor Point (preservingpoint.distance/point.withinBBoxpushdown),<attr>.bbSW/<attr>.bbNEbounding-box corners for everything else. On-disk GeoJSON is strict RFC 7946 (nocrsmember,bboxmember included) so any GIS tool can ingest it directly.Recursive geo-promotion (#274) — a geometry nested inside another attribute (an
Ash.TypedStructfield, embedded resource, map) has its indexable companion promoted to a node-level property at the dotted path (<attr>.<field>.pointetc.), even though the parent value stores as a single JSON blob. A location buried inside a characteristic is indexable viapoint.distance(n.characteristic.location.point, …). The data layer walks the value tree on write (geo_walk/2) and round-trips the nested geometry on read.st_closest_point(#274) — newAsh.Query.Functionreturning the nearest vertex (%Geo.Point{}) from aLineStringorMultiPointto a target point. In-memory.AshNeo4j.Spatialindex helpers (#275) —create_index/3/drop_index/2build and run the POINT index Cypher backing spatial pushdown from a resource module + attribute name, resolving the Neo4j label, the attribute→property translation, and the companion suffix convention (.pointfor a Point; both.bbSW/.bbNEfor any other geometry, in one call). Nested geometries take a[attribute, field…]path (create_index(Place, [:pet, :home])) and resolve the dotted property by walking theAsh.TypedStructfields.create_index/3is idempotent (CREATE … IF NOT EXISTS), takesrecreate: true(DROP + CREATE) for storage-shape changes, andname:to override the derived index name.index_statements/3returns the exact Cypher without touching the database, for review or a dry run. Consistent with the "no migrations, index lifecycle is the operator's concern" stance (#45) — an ergonomic tool you call, not automatic behaviour.AshNeo4j.worlds/1— N-world projection (#273, exploratory) — a Neo4j node carries labels for every(Domain, Resource)world it participates in, but an Ash read returns only the queried world's struct.worlds/1projects a read record's labels (already on__metadata__.labels) back to the loadable resource modules —[{domain, resource}, …]ordered outermost-first — so a consumer can recover the outer type(s) of a polymorphic node for cross-domain late binding (diffo#172) without dropping to Cypher. An outer world contains the inner worlds and adds detail, so it carries more labels (more labels = more nuanced = more outer). Resolution is dynamic against loaded modules (no registry): a candidate is a loadedAshNeo4j.DataLayerresource whose own labels are a subset of the node's, the outermost (most-nuanced) is kept per domain, and the loaded-resource index is cached in:persistent_term. Labels that don't resolve to a loaded module are left unknown — omitted, never coerced. Returns[]for a non-AshNeo4j record. Pre-1.0 and may change — shipped to learn its shape from real downstream use.
Improvements
In-memory distance matches Neo4j's
point.distance(#274) —st_distance/st_dwithinpush down to Neo4j's nativepoint.distanceinside comparison filters but evaluate in Elixir elsewhere (order_by,calculate, LineString/MultiPoint). Both now use the same model — a spherical haversine on the WGS-84 equatorial radius (6 378 137 m), the radius Neo4j uses, not the mean Earth radius (6 371 000 m) — so the two paths agree to within ~1 m over 700 km rather than diverging by ~0.11 % (≈800 m). Single source of truthAshNeo4j.Geo.haversine_meters/2, shared byst_distanceandst_closest_point; a sandbox test asserts the paths stay in step.st_*expression functions extended (#274) —st_distance/st_dwithin/st_intersects/st_contains/st_withinnow operate on%Geo.*{}argument shapes across the full geometry surface. Pushdown gating reads the attribute'sgeo_typesconstraint rather than the (now-removed) type-module identity.Exact, hole-aware polygon predicates (#267) —
st_containsandst_intersectsrefine viatopoon the actual%Geo.*{}rings, replacing the bbox approximation. A point in the bounding box but outside the ring is correctly excluded; a point in an interior ring (hole) is not contained; a LineString that crosses a Polygon without a vertex inside it correctly intersects. InsideAsh.Query.filter,st_containskeeps the indexedpoint.withinBBoxbbox prefilter in Cypher and runs the exacttopotest in-memory over the candidates (a true match always lies within the bbox, so the prefilter never drops one). Addstopo ~> 1.0runtime dep.Exact geometry-to-Point distance (#279) —
st_distance(andst_dwithin, which delegates to it) now measures any geometry to a Point exactly: LineString/MultiLineString use the true closest-point-on-segment instead of closest-vertex (the old approximation could overstate a mid-edge proximity by tens of kilometres — e.g. 78.7 km reported where the perpendicular distance is 55.7 km), and Polygon/MultiPolygon return0when the point is inside (hole-aware viatopo) or the nearest-boundary distance otherwise.st_closest_pointlikewise returns the closest point on the nearest segment of a LineString (an interior edge point, not just a vertex). NewAshNeo4j.Geo.point_segment_meters/3/closest_point_on_segment/3/min_segment_meters/2primitives (local equirectangular projection to find the closest point, haversine for the distance). Distance between two non-Point geometries is still deferred. Also confirms MultiLineString — the sixth RFC 7946 geometry — round-trips through storage and works across the predicates, and extendsst_containsto accept LineString / MultiLineString containees.AshNeo4j.GeoJson(#274) — RFC 7946 encoder/decoder wrappinggeo; strips the obsoletecrsmember (whichgeoemits whensridis set — see felt/geo#250), injects thebboxmember, key-sorts viaAshNeo4j.Util.json_encode.Util.to_json_safe/json_decodegained symmetric Geo handling so geometries survive nesting inside JSON-stored types. Local workarounds for ash_geo#13 (bare-atomgeo_typesformatter crash) and ash_geo#14 (cast_storedmap handling) are in place pending those upstream fixes.
v0.7.0 (2026-05-25)
Features
Spatial types and
st_*expressions (#45) — first-class WGS-84 2D spatial support. New attribute typesAshNeo4j.Type.Point(native Neo4j Point) andAshNeo4j.Type.Box(axis-aligned bounding box, 4-vertex straight-sided polygon on disk). SixAsh.Query.Functionmodules matching ash_geo / PostGIS signatures:st_contains(box-point, box-box),st_within,st_intersects,st_distance(point-point, with comparison pushdown),st_distance_in_meters(alias),st_dwithin(point-point). Predicates push down to native Cypher (point.distance,point.withinBBox) wherever possible; in-memoryevaluate/1is the correctness fallback. Box's on-disk storage uses a 4-Point vertex array plus 4 scalar bbox-corner companion properties (<prop>.bbSW/.bbSE/.bbNE/.bbNW) written by a genericcompanions/1callback on the Type module — the same shape future Polygon support (#267) will use, so no data migration when Polygon lands. The bbox companions are scalar Point properties specifically to be indexable via Neo4j's POINT index — storage is indexable, not yet indexed (operators runCREATE POINT INDEXthemselves; lifecycle management is future work). Documentation inusage-rules/spatial.md. Requiresbolty >= 0.0.13for native Point property serialisation (bolty#32).Combination queries (#10) — support for all five
Ash.Query.Combinationtypes (:base,:union,:union_all,:intersect,:except). Combinations of only:unionor only:union_allpush down to a single CypherCALL { … UNION/UNION ALL … } WITH s OPTIONAL MATCH (s)-[r]-(d) RETURN s, r, dblock, with per-branch parameter prefixing to avoid name collisions. Combinations involving:intersect,:except, or mixed union types take an in-memory orchestration path — each branch runs returning just node ids (id(s) AS sid), the set operation is computed in Elixir overMapSets, then a finalMATCH WHERE id(s) IN $idsfetches the keep-set with the standard OPTIONAL MATCH enrichment. Cypher has no nativeINTERSECT/EXCEPT; the in-memory implementation is the honest answer. Documentation inusage-rules/combination-queries.md. NewAshNeo4j.Cypher.Querybuilders:branch_node_read/3,branch_node_read_ids/3,combination_block/2,node_read_by_ids/2; newparam_prefix:opt onnode_read_filtered/3andbuild_conditions/3. NewAshNeo4j.Cypher.Callclause type.
v0.6.0 (2026-05-19)
Breaking Changes
Introspection API renamed (#105) —
AshNeo4j.DataLayer.InfoandAshNeo4j.DataLayer.Domain.Infoare now generated bySpark.InfoGenerator. AshNeo4j now declares a directspark >= 2.7.0dependency to guarantee availability. All functions follow the InfoGenerator convention:neo4j_label/1returns{:ok, value} | :error;neo4j_label!/1returns the value or raises; list options (relate,guard,skip) always return the list via the!variant. Previous hand-rolled helpers (label/1,relate/1,guard/1,skip/1) are removed.
Features
- Domain fragment label (#261) — domains can declare a cross-domain graph label via
AshNeo4j.DataLayer.Domain(use Ash.Domain, extensions: [AshNeo4j.DataLayer.Domain]withneo4j do label :MyLabel end). The fragment label is written as an additional Neo4j node label on CREATE, enabling polymorphic graph traversals across domains. Exposed viaResourceInfo.domain_fragment_label/1and included inResourceInfo.all_labels/1andResourceInfo.mapping/1.
Bug Fixes
belongs_tosource attribute always nil after read (#258) —belongs_tosource attributes (e.g.specification_id) were correctly populated on create but lost on any subsequent read. The enrichment step now correctly extracts the relationship attribute from the destination node returned by the OPTIONAL MATCH traversal when the source resource uses a fragment-inherited relationship whose destination lives in a different domain.Domain fragment label dropped on Ash 3.25+ —
ResourceInfo.all_labels/1was returning the compile-time persisted label list, which is baked before the domain extension compiles under Ash 3.25's updated compilation order, causing the domain fragment label to be silently omitted.all_labels/1now always computes dynamically from the individual label accessors, consistent with howmapping/1already worked.
Improvements
- Scalar filter pushdown for aggregates (#253) — filtered aggregates whose filter consists entirely of scalar
==equality predicates on non-embedded destination attributes now push aWHERE d.prop = $valclause directly into Cypher, avoiding full destination record loading in Elixir. Complex filters (OR, embedded fields, non-equality operators) continue to use the Elixir-side path introduced in #252.
v0.5.1 (2026-05-10)
Improvements
- Documentation (#249) — ex_doc configuration overhauled: extras reorganised with titled entries, module groups defined for AshNeo4j, Introspection, Cypher, Utilities and Internals, Livebook added to How To, CHANGELOG included in About AshNeo4j, maintainer contact updated.
Bug Fixes
Aggregate filters honoured (#252) — filters declared via
filter expr(...)on aggregate definitions were silently dropped. Filtered aggregates now load full destination records in Elixir and applyAsh.Filter.Runtime.filter_matches/3per source group before reducing. The fast Cypher push-down path is preserved for unfiltered aggregates.Aggregate names with
?suffix (#251) — aggregate names following the Elixir predicate convention (e.g.exists :cvc_defined?, :characteristics) caused Neo4j to reject the generated Cypher with an invalid identifier error. Column aliases are now backtick-quoted, allowing any valid Elixir atom as an aggregate name.
v0.5.0 (2026-05-08)
Features
- Aggregates — full support for
:count,:exists,:sum,:avg,:min,:max,:first,:listaggregate kinds, declared in the standard Ashaggregatesblock. Aggregates are executed as CypherOPTIONAL MATCHtraversals; single-hop and multi-hop relationship paths are both supported. - Aggregates on embedded/JSON-type fields — when
field:points to an attribute stored as JSON (Ash.TypedStruct,Ash.Type.NewType, embedded resources,Ash.Type.Map, etc.) AshNeo4j collects raw JSON from Neo4j and deserializes in Elixir.:listand:firstreturn fully-typed structs;:sum/:avg/:min/:maxwork on directly comparable values. - Expression aggregates (
expr:) — programmatic aggregate API (Ash.aggregate/3) acceptsexpr:to aggregate over a sub-field of an embedded struct or any Ash expression, without needing to elevate the field. Fetches full destination records and evaluates expressions in Elixir. - Expression calculations —
calculate :name, :type, expr(...)declarations are now evaluated in Elixir after records are loaded. Supports load (Ash.load!), filter (Ash.Query.filter), and sort (Ash.Query.sort). Embedded struct fields work directly viaget_path— no elevation needed.
Improvements
- Cypher query struct family extended;
Neo4jHelperrefactored to use it - Calculation-based filter predicates are excluded from Cypher WHERE and evaluated in-memory via
Ash.Filter.Runtime - Calculation-based sort terms are applied in Elixir after records are loaded
v0.4.1 (2026-05-06)
What's Changed
- fix in_transaction? by @matt-beanland in https://github.com/diffo-dev/ash_neo4j/pull/226
- fixed sandbox and non-sandbox paths by @matt-beanland in https://github.com/diffo-dev/ash_neo4j/pull/227
- fix unhandled throws by @matt-beanland in https://github.com/diffo-dev/ash_neo4j/pull/228
v0.4.0 (2026-05-01)
Features:
- real Neo4j transactions via
Bolty.transaction—can?(_, :transact)now advertised, rollback genuinely aborts the database transaction AshNeo4j.Sandbox— test isolation adapter analogous toEcto.Adapters.SQL.Sandbox, enabling safe parallel test execution withasync: true
Improvements:
- silenced spurious runtime
Logger.warningcalls that fired on normal OPTIONAL MATCH traversal - full test suite parallelised with
async: true
v0.3.1 (2026-04-23)
This release changes the storage type for Ash.Type.DateTime, Ash.Type.UtcDateTime and Ash.Type.UtcDateTimeUsec
What's Changed
- use native neo4j 5.x datetime by @matt-beanland
v0.3.0 (2026-04-18)
This release changes the storage type for most types. Ash.Type dump_to_native/cast_stored are used where possible.T String.Chars is no longer required and JSON blobs/Base64 are employed. Native Neo4j types are used except for datetime, instead we use ISO8601 strings to work around Neo4j 5.x incompatibility. There is no data migration supported.
What's Changed
- 196 remove need for structs to implement stringchars by @matt-beanland in https://github.com/diffo-dev/ash_neo4j/pull/197
- reduced advertised capability, fixed calculations by @matt-beanland in https://github.com/diffo-dev/ash_neo4j/pull/198
- refactored transformers as persisters, split DataLayer and Resource Info by @matt-beanland in https://github.com/diffo-dev/ash_neo4j/pull/201
- updated deps and reinstated keyword tests by @matt-beanland in https://github.com/diffo-dev/ash_neo4j/pull/204
- fixed persister and improved verifier to verify all labels by @matt-beanland in https://github.com/diffo-dev/ash_neo4j/pull/205
- added encoding test and fixed json_encode for map by @matt-beanland in https://github.com/diffo-dev/ash_neo4j/pull/207
- added defensive casting, returning error tuple by @matt-beanland in https://github.com/diffo-dev/ash_neo4j/pull/209
- expression calculations in memory by @matt-beanland in https://github.com/diffo-dev/ash_neo4j/pull/210
v0.2.15 (2026-03-19)
Fixes
- fix domain label incorrect
v0.2.14 (2026-03-19)
Fixes
- fix relationship enrichment inconsistent across neo4j versions
v0.2.13 (2026-03-12)
Features
- translate using attribute source (translate DSL removed)
- nodes are also labelled with domain label
Fixes
- fixed dates and times not native
Maintenance
- uses bolty at https://github.com/diffo-dev/bolty, a reluctant fork of boltx
- updated deps and tool versions
- improved info documenation
v0.2.12 (2025-11-18)
Features
- 173 relationship source attribute filtering by @matt-beanland in #174
Maintenance
- added deep wiki badge by @matt-beanland in #171
v0.2.11 (2025-10-13)
Features
- REUSE compliant
Fixes
- updated ash dependency for CVE-2025-48043 fix
v0.2.10 (2025-09-09)
Maintenance
- fixed update on_lookup relate on has_many exclusivity
v0.2.9 (2025-08-16)
Maintenance
- fixed Ash.Error.Unknown when reading structs embedded in structs
v0.2.8 (2025-08-14)
Features
- relate destination node label
- independent relationships
- simplified dsl
Maintenance
- fixed unexpected empty query result
- fixed has_many enrichment incorrect cypher
- fixed create with multiple relationships doesn't relate nodes
v0.2.7 (2025-08-03)
Features
- relates node cypher avoids cartesian product warning
Maintenance
- fixed Ash.Error.Unknown no result to unrelate nodes
- fixed create or update belongs_to on same resoruce adds rather than replaces
- fixed Ash.Error.Unknown no case clause matching on update
- fixed guard edge label regex
- fixed sorting not working
- fixed nested calculations with references are nil
v0.2.6 (2025-07-25)
Maintenance
- fixed nested calculations with references are nil
- fixed cypher error when filtering on atom type
- fixed Ash.Error.Unknown when a delete is guarded
- fixed Ash.Error.Unknown invalid filter statement provided
v0.2.5 (2025-07-21)
Features:
- guard against destroy
- improved has_one and belongs_to enrichment
- improved logging
Maintenance
- fixed destroy should fail when destination has allow_nil?: false
v0.2.4 (2025-07-16)
Features:
- support AshStateMachine
- improved enrichment
- query on relationship attribute
- create with multiple relationships
Maintenance
- fixed Ash.Error.Unknown no function matching clause in AshNeo4j.Cypher.expression/4
v0.2.3 (2025-07-10)
Features:
- expression calculations
- unloaded attributes are Ash.NotLoaded
- improved metadata
- improved relate error messages
- improved relate verification
v0.2.2 (2025-06-26)
Maintenance:
- refactored tests
- fixed Ash.Error.Unknown when filtering using contains
- fixed Ash.Error.Unknown in datalayer when relate not defined
v0.2.1 (2025-06-17)
Features:
- many to many relationship (back to back has_many)
- has one relationship
v0.2.0 (2025-06-05)
Features:
- improved BoltxHelper
- create relate
- livebook
v0.1.6 (2025-06-02)
Features:
- embedded resources
- nil attributes
- nil relationship attributes
v0.1.5 (2025-05-31)
Features:
- logger
- upsert nodes
- optional label
v0.1.4 (2025-05-28)
Features:
- spark improvements
v0.1.3 (2025-05-24)
Features:
- sort, offset, limit
v0.1.2 (2025-05-23)
Features:
- property types, duration, relate, destroy
v0.1.1 (2025-05-05)
Features:
- create
Bug Fixes:
- read arbitrary resource
v0.1.0 (2025-04-30)
Features:
- initial version, read only