Handle is_distinct_from / is_not_distinct_from in the map-comparison path - #220
Merged
zachdaniel merged 1 commit intoAug 16, 2026
Conversation
…path
Ash emits is_distinct_from in place of != whenever either side of the
comparison can be nil, which is exactly what update_timestamp's
only-bump-if-changed condition builds for any nullable :map attribute.
The v0.2.15 JSON-encoded map comparison only matched Eq/NotEq, so the
nullable case fell through to the generic renderer, bound the raw
Elixir map as a driver parameter, and Exqlite rejected it:
(Exqlite.Error) unsupported type: %{...}.
Mirror the Eq/NotEq clauses for IsDistinctFrom/IsNotDistinctFrom,
routing to handle_map_comparison/9, and render IS DISTINCT FROM /
IS NOT DISTINCT FROM (supported by SQLite since 3.39). The
is_non_struct_map guard is unchanged, so no comparison not involving a
plain map changes behaviour. json(NULL) is NULL, so a NULL column
remains correctly distinct from any map, and writing the same map
still does not bump updated_at.
Contributor
|
PR welcome to resolve this 🙇 |
Contributor
|
The stated fix sounds good 👍 |
Contributor
|
lol, this is a PR 😆 |
zachdaniel
approved these changes
Aug 16, 2026
Contributor
|
🚀 Thank you for your contribution! 🚀 |
Contributor
Author
|
I appreciate the fast turnaround on the review... do you ever sleep? On the out-of-scope part: it's filed as ash_sql #246 and I have the callback implementation in progress, so a PR for that is coming shortly. |
This was referenced Aug 16, 2026
zachdaniel
pushed a commit
that referenced
this pull request
Aug 16, 2026
…y(...) (#221) SQLite has no array constructor, so AshSql's ARRAY[...] rendering of list literals fails to parse ((Exqlite.Error) near "[?]": syntax error) for any {:array, :map} attribute. Implement the AshSql.Implementation.list_expr/6 callback (ash-project/ash_sql#247) to render json_array(...) with each element bound as its JSON encoding, matching what Ecto's SQLite adapter dumps for the same column so UPDATE-written and CREATE-written values are byte-identical and update_timestamp's only-bump-if-changed comparison stays honest. Depends on ash-project/ash_sql#247; stacked on #220. * Require ash_sql >= 0.6.9 (Implementation.list_expr/6)
zachdaniel
pushed a commit
that referenced
this pull request
Aug 17, 2026
…222) A map anywhere in an expression other than as a comparison operand (v0.2.15/#220) or a list element (#221) was bound as a bare Elixir term and rejected by exqlite (unsupported type). One expr/6 clause for a plain map, just before the catch-all, delegating to the existing as_json/7 so a map has one behaviour in this data layer. Maps that contain expressions and the :select location stay with AshSql, so nothing that works today changes. Also bumps mix.lock's ash_sql to the 0.6.9 that mix.exs has required since #221, so a fresh clone can run the suite again. Closes #219.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
An update that Ash runs atomically, on a resource with
update_timestamp(:updated_at), writing amap-typed attribute that is
allow_nil? true, fails on SQLite:The same update against an otherwise identical attribute that is
allow_nil? falsesucceeds.Why
Ash.Changeset.atomic_default_condition/4builds the "only bump the timestamp if somethingactually changed" condition. If the changed attribute can be nil it emits
is_distinct_from(^new_value, ^ref(key)); if it cannot it emits^new_value != ^ref(key).v0.2.15 added JSON-encoded map comparison ("handle map comparisons via json encoding") to
AshSqlite.SqlImplementation.expr/6, but only forAsh.Query.Operator.EqandAsh.Query.Operator.NotEq.Ash.Query.Function.IsDistinctFromhas no clause, so the expressionfalls through to the generic
AshSql.Exprrenderer. There,AshSqlite.SqlImplementation.parameterized_type/2deliberately returnsnilforAsh.Type.Map,so
maybe_type_expr/6produces no cast and the raw Elixir map is bound as a driver parameter.Exqlite cannot encode a bare map and rejects it.
So
allow_nil?is the entire discriminator, on one resource, one action, two attributes of thesame type:
allow_nil?valuefalse!=metadatatrueis_distinct_from(Exqlite.Error) unsupported typeMinimal repro
Remove
update_timestamp :updated_atand both succeed, because Ash never builds the condition.The fix
Two
expr/6clauses forIsDistinctFrom/IsNotDistinctFromthat mirror the existingEq/NotEqones exactly, routing tohandle_map_comparison/9, plus two cases in that functionrendering
IS DISTINCT FROM/IS NOT DISTINCT FROM. SQLite has supported those operators since3.39.
The guard is the same
is_non_struct_map(left) or is_non_struct_map(right)used by the clausesabove, so no comparison that does not involve a plain map changes behaviour.
IS DISTINCT FROMremains correct against the JSON encoding: the literal side becomes a JSONstring and the ref side becomes
json(col), andjson(NULL)isNULL, so a NULL column iscorrectly "distinct from" any map.
Testing
Verified against a real application on ash 3.31.3 / ash_sqlite 0.2.17 / exqlite 0.36.0
(SQLite 3.51.3):
(resource, update action, nullable map attribute)triples across ~28 resources went fromraising to executing. Each is exercised against a real database rather than verified by
reading the code.
compared equal.
allow_nil? falsecontrol still passes, so the pre-existing!=branch is untouched.nilstill clears the column, so nil and%{}remain distinguishable.updated_at, which is the behaviour the condition existsto provide. This is the assertion that would catch a fix that simply forced the comparison true.
Out of scope (separate defect, reported for completeness)
{:array, :map}attributes are also broken on SQLite, but for a different reason and with adifferent signature:
AshSql.Expr.encode_list/6renders every list literal as PostgresARRAY[...](orarray_to_json(ARRAY[...])) with no adapter seam. That is inash_sql, affects the plainSETclause as well, and is unrelated tois_distinct_from, so it is not addressed here. Happyto open a separate issue on
ash_sqlif useful; it likely needs a newAshSql.Implementationcallback so adapters can render list literals themselves.