Adding macros to `expr/1`

I'd like to add the st_* macros from GeoPostGIS to expr/1, possibly more in future. These macros do generate exactly the fragment syntax that is currently supported by expr, which is pretty nice, but they don't work as-is. Is doesn't look like it's possible currently to extend Ash.Expr, and I'm quite interested in changing that, or at least making it possible to mix in external macros in this way using the existing supporting constructs like fragment. Has there been any interest/thought up to now on this topic? https://github.com/bryanjos/geo_postgis/blob/master/lib/geo_postgis.ex
GitHub
geo_postgis/geo_postgis.ex at master · bryanjos/geo_postgis
Postrex Extension for PostGIS. Contribute to bryanjos/geo_postgis development by creating an account on GitHub.
38 Replies
\ ឵឵឵
\ ឵឵឵OP3y ago
I'm doing a lot of queries with these things and it would be fantastic to be able to write stuff like
filter expr(st_within(^arg(:latlng), :geometry))
filter expr(st_within(^arg(:latlng), :geometry))
ZachDaniel
ZachDaniel3y ago
GitHub
Custom predicates & operators · Issue #374 · ash-project/ash
These would be exposed to the expression syntax in code, and can be made available over api extensions as well. I'm thinking something like this: support a custom configured list of functions t...
ZachDaniel
ZachDaniel3y ago
This is the tracking issue for that feature in the meantime, there are some things you can do. 1. add calculations to the resources you want to use this with
calculate :st_within, :the_type, expr(fragment("...", ^arg(:latlng), :geometry)) do
argument :ltlng, :type, allow_nil?: false
end
calculate :st_within, :the_type, expr(fragment("...", ^arg(:latlng), :geometry)) do
argument :ltlng, :type, allow_nil?: false
end
2. write macros and use them in your exprs. This exact strategy may or may not work but I'm pretty sure we can make something similar to it work at least.
# in YourModule
defmacro st_within(latlng, type) do
quote do
st_within(^arg(:latlng), :geometry)
end
end

# in your resources
import YourModule
expr(^st_within(latlng, type))
# in YourModule
defmacro st_within(latlng, type) do
quote do
st_within(^arg(:latlng), :geometry)
end
end

# in your resources
import YourModule
expr(^st_within(latlng, type))
\ ឵឵឵
\ ឵឵឵OP3y ago
Great that it's on the list Tried the second example, getting
undefined function fragment/3 (there is no such import)
undefined function fragment/3 (there is no such import)
On the plus side, if we can get that working, then I can make them understand the arg(_) -> {:_arg, _} and friends stuff, so I can write wrappers for these that can be mixed with the usual Ash stuff to some degree.
ZachDaniel
ZachDaniel3y ago
Actually, try this
defmacro st_within(latlng, type) do
quote do
import Ash.Expr
import Ash.Filter.TemplateHelpers

expr(fragment("..", ...))
end
end
defmacro st_within(latlng, type) do
quote do
import Ash.Expr
import Ash.Filter.TemplateHelpers

expr(fragment("..", ...))
end
end
And if that doesn't work try it without the wrappers
\ ឵឵឵
\ ឵឵឵OP3y ago
Works:
filter expr(fragment("ST_Within(?,?)", ^arg(:latlng), :geometry))
filter expr(fragment("ST_Within(?,?)", ^arg(:latlng), :geometry))
Not happy:
defmodule AshGeoPostgis do
defmacro st_within(arg, attr) do
quote do
import Ash.Expr
import Ash.Filter.TemplateHelpers

fragment("ST_Within(?,?)", ^arg(unquote(arg)), unquote(attr))
end
end
end
defmodule AshGeoPostgis do
defmacro st_within(arg, attr) do
quote do
import Ash.Expr
import Ash.Filter.TemplateHelpers

fragment("ST_Within(?,?)", ^arg(unquote(arg)), unquote(attr))
end
end
end
filter expr(^st_within(:latlng, :geometry))
filter expr(^st_within(:latlng, :geometry))
ZachDaniel
ZachDaniel3y ago
whats the error?
\ ឵឵឵
\ ឵឵឵OP3y ago
Same
ZachDaniel
ZachDaniel3y ago
defmodule AshGeoPostgis do
defmacro st_within(arg, attr) do
quote do
import Ash.Expr
import Ash.Filter.TemplateHelpers

expr(fragment("ST_Within(?,?)", ^arg(unquote(arg)), unquote(attr)))
end
end
end
defmodule AshGeoPostgis do
defmacro st_within(arg, attr) do
quote do
import Ash.Expr
import Ash.Filter.TemplateHelpers

expr(fragment("ST_Within(?,?)", ^arg(unquote(arg)), unquote(attr)))
end
end
end
Need to wrap it in expr/1 Also if you do it that way it should just "pass through" arg stuff
defmodule AshGeoPostgis do
defmacro st_within(a, b) do
quote do
import Ash.Expr
import Ash.Filter.TemplateHelpers

expr(fragment("ST_Within(?,?)", unquote(a), unquote(b)))
end
end
end
defmodule AshGeoPostgis do
defmacro st_within(a, b) do
quote do
import Ash.Expr
import Ash.Filter.TemplateHelpers

expr(fragment("ST_Within(?,?)", unquote(a), unquote(b)))
end
end
end
Then you can st_within(^arg(:foo), :bar)
\ ឵឵឵
\ ឵឵឵OP3y ago
Aye, that looks right. Will give it a shot when I'm back at my desk. Some progress:
defmodule AshGeoPostgis do
defmacro st_within(contained, container) do
quote do
expr(fragment("ST_Within(ST_PointFromText(?),?)", unquote(contained), unquote(container)))
end
end
end
defmodule AshGeoPostgis do
defmacro st_within(contained, container) do
quote do
expr(fragment("ST_Within(ST_PointFromText(?),?)", unquote(contained), unquote(container)))
end
end
end
filter expr(^st_within(^arg(:latlng), geometry))
filter expr(^st_within(^arg(:latlng), geometry))
ZachDaniel
ZachDaniel3y ago
Nice! So that works?
\ ឵឵឵
\ ឵឵឵OP3y ago
Yep, got input types working as well 🙂 Nested fragments working, seems good. Got a bunch of stuff to write with this over the next days for a stress test. If all goes well, I'll find some time next week to roll ash_geo_postgis. One odd thing: cast_input is not getting called for the argument type in a create with set_attribute, but is getting called when doing a read with filter expr(...).
defmodule Thing do
use Ash.Resource, data_layer: AshPostgres.DataLayer

attributes do
attribute :area, :geometry
end

actions do
default_actions [:read, :destroy]

create :create do
primary? true
argument :area, :geo_json
change set_attribute(:area, arg(:area))
end

read :containing do
argument :point, :geo_json
filter expr(^st_within(^arg(:point), area))
end
end
end
defmodule Thing do
use Ash.Resource, data_layer: AshPostgres.DataLayer

attributes do
attribute :area, :geometry
end

actions do
default_actions [:read, :destroy]

create :create do
primary? true
argument :area, :geo_json
change set_attribute(:area, arg(:area))
end

read :containing do
argument :point, :geo_json
filter expr(^st_within(^arg(:point), area))
end
end
end
Call flow for read/filter expr(...) goes: 1. :geo_json.cast_input 2. :geometry.cast_stored Call flow for create/set_attribute on the other hand: 1. :geometry.cast_input 2. :geometry.dump_to_native Because there are multiple input formats that should all be parsed into a geometry and I'd like to let them be specified explicitly, this is not ideal, but I can try to work with the current behaviour, just wanted to check if this is as intended, or is an Ash fix. (ideal would be that cast_input for the input type always gets called for an argument)
ZachDaniel
ZachDaniel3y ago
🤔 if you could make a test showing an example of not calling cast_input when providing input for pretty much any argument anywhere, then it is almost certainly a bug 🙂 There are some cases where we don't call apply_constraints/2 (specifically when building queries)
\ ឵឵឵
\ ឵឵឵OP3y ago
In this case, cast_input is getting called, but only on the underlying type (:geometry), not the input type (:geo_json), which seems wrong to me.
ZachDaniel
ZachDaniel3y ago
So do you have :geo_json set up as a type alias somewhere I assume? like w/ the custom short names config?
\ ឵឵឵
\ ឵឵឵OP3y ago
Correct
ZachDaniel
ZachDaniel3y ago
gotcha. In that case that sounds like a bug 🙂
\ ឵឵឵
\ ឵឵឵OP3y ago
Yeah changeset.ex has the cast to underlying in force_change_attribute but didn't see the input cast anywhere there or in the set_sttribute built-in.
ZachDaniel
ZachDaniel3y ago
well the set_attribute builtin calls force_change_attribute under the hood the problem is in the action argument casting I imagine
\ ឵឵឵
\ ឵឵឵OP3y ago
Yeah I know, but it's only casting to the underlying type, not from the input type
ZachDaniel
ZachDaniel3y ago
Yeah, that makes sense arguments get casted up-front
\ ឵឵឵
\ ឵឵឵OP3y ago
Right, so that's what I was thinking, maybe this is supposed to be happening further up the request chain. Where is it supposed to be getting casted?
ZachDaniel
ZachDaniel3y ago
it happens in set_argument/3 its interesting...I think I might see the problem maybe. Can I see how you're calling the action?
\ ឵឵឵
\ ឵឵឵OP3y ago
code_interface do
define :create
define :containing, args: [:point]
end
code_interface do
define :create
define :containing, args: [:point]
end
ZachDaniel
ZachDaniel3y ago
okay yeah nvm them um....how sure are you that its not calling the function you want it to call?
\ ឵឵឵
\ ឵឵឵OP3y ago
create one is bare
ZachDaniel
ZachDaniel3y ago
just want to check our bases are you like inspecting something in the cast_input function of your types?
\ ឵឵឵
\ ឵឵឵OP3y ago
Reasonably sure... I have dbg breaks on all my cast_inputs for the type wrappers.
ZachDaniel
ZachDaniel3y ago
and you definitely have your short names configured properly?
\ ឵឵឵
\ ឵឵឵OP3y ago
Same short names and same breaks are being used to validate the flow for the read action where it's working. I don't have a break on the :error catch-all but if it's getting an error out of cast and passing it through anyways, that's a bug as well.
ZachDaniel
ZachDaniel3y ago
true, but lets check it just to be sure 😄
\ ឵឵឵
\ ឵឵឵OP3y ago
Back at desk in 5
ZachDaniel
ZachDaniel3y ago
Also try out just this Resource |> Ash.Changeset.for_action(:create, %{...input}) it should cast the arguments there w/o even calling the action
\ ឵឵឵
\ ឵឵឵OP3y ago
Yep, Thing |> Ash.Changeset.for_action(:create, %{area: %{}}) triggers :geojson.cast_input correctly. And... So does the code interface call now.
ZachDaniel
ZachDaniel2y ago
🤔
\ ឵឵឵
\ ឵឵឵OP2y ago
:thinkies:
abeeshake456
abeeshake4562y ago
When using ash_geo library, I get
lib/companies_reg_ex/resources/recruiters.ex:37: function expr/1 imported from both Ash.Filter.TemplateHelpers and Ash.Expr, call is ambiguous
(spark 1.1.22) expanding macro: Spark.Dsl.Extension.set_entity_opt/4
lib/companies_reg_ex/resources/recruiters.ex:37: CompaniesRegEx.Resources.Recruiters (module)
(ash 2.13.2) expanding macro: Ash.Resource.Dsl.Actions.Read.Options.filter/1
lib/companies_reg_ex/resources/recruiters.ex:37: function expr/1 imported from both Ash.Filter.TemplateHelpers and Ash.Expr, call is ambiguous
(spark 1.1.22) expanding macro: Spark.Dsl.Extension.set_entity_opt/4
lib/companies_reg_ex/resources/recruiters.ex:37: CompaniesRegEx.Resources.Recruiters (module)
(ash 2.13.2) expanding macro: Ash.Resource.Dsl.Actions.Read.Options.filter/1
the docs for ash_geo and ash are sparse around expr. @\ ឵឵឵
\ ឵឵឵
\ ឵឵឵OP2y ago
Fixes to outdated example in the readme and updates to test suite for latest ash and ash_postgres are up now on Hex ash_geo v0.1.3.

Did you find this page helpful?