Concat with nil values

The built in concat calculation returns nil when any of the values it is concating is nil. I need a nil friendly concat and I could use something like this (which only works on the data layer and is postgres specific):
calculate :full_name,
:string,
expr(fragment("concat_ws(?, ?, ?)", ", ", first_name, last_name))
calculate :full_name,
:string,
expr(fragment("concat_ws(?, ?, ?)", ", ", first_name, last_name))
I'd prefer to write a custom calculation that I can reuse (so the calculation can be run in Ash land after the record is retrieved from the db), but if I create a custom calculation (to use concat_ws) it is still only going to be suitable only for postgres datalayer. Is there a way that ash_postgres could use concat instead of || as an option? I'm probably overthinking it and should just use the above calc and move on!
36 Replies
ZachDaniel
ZachDaniel3y ago
Hm…I don’t think we can do it now unless we did a major release. But also the elixirland version of :<> does the same thing (Because we emulate nil bubbling up like NULL)
dblack
dblackOP3y ago
yep, it's consistent, which is cool
ZachDaniel
ZachDaniel3y ago
But you could always do (foo || “”) <> (bar || “”)
frankdugan3
frankdugan33y ago
Wouldn't that duplicate the joiner for any empty values?
dblack
dblackOP3y ago
yeah I'd be left with a dangling separator
ZachDaniel
ZachDaniel3y ago
… ohhh I see what you mean. I mean, we can just add concat_ws or similar expression in ash
frankdugan3
frankdugan33y ago
concat_ws basically behaves like Enum.join/2
ZachDaniel
ZachDaniel3y ago
Perhaps we should add join/2 to the expression syntax? Since it’s useful in general.
frankdugan3
frankdugan33y ago
I think that would really be the ideal solution, yeah.
ZachDaniel
ZachDaniel3y ago
Although join is maybe a confusing word in that context lol
frankdugan3
frankdugan33y ago
Honestly, concat_ws is kind of confusingly labelled.
dblack
dblackOP3y ago
how would that map to ash_postgres? it would be a postgres only calc?
ZachDaniel
ZachDaniel3y ago
Because of db joins
frankdugan3
frankdugan33y ago
Ahh, yeah... lol, either way you get term overloading.
ZachDaniel
ZachDaniel3y ago
Nah, it would use concat_ws in postgres and then equivalent elixir code in elixirland
dblack
dblackOP3y ago
yep, and ets?
ZachDaniel
ZachDaniel3y ago
Ets just uses the elixirland logic But yeah essentially. Honestly I’m amazed there hasn’t been more of a push to build out the “expression standard library” I thought people would be champing at the bit
dblack
dblackOP3y ago
Ok sweet. Is it asking for future issues if ash (core) contains calcs that are postgres specific? Or is that future Ash's problem?! Just thinking if more data layers come along
ZachDaniel
ZachDaniel3y ago
Well, that’s not really how it works. The calc exists in core, but the implementation exists in ash_postgres.
dblack
dblackOP3y ago
ah ok
ZachDaniel
ZachDaniel3y ago
And it’s fine for there to be expressions not supported by some data layers But anything we could reasonably expect most data layers to support can go in ash core
dblack
dblackOP3y ago
Makes sense, you're talking about expressions, I was still thinking about a custom calculation... I'll have a look at expressions and see what I come up with
ZachDaniel
ZachDaniel3y ago
Ah, yeah I see what you mean
dblack
dblackOP3y ago
Ok, I think I'm getting it (adding expressions). What's the name/api for this concat function? I'm going with expr(string_join(" ", [first_name, last_name])), where the values are passed in a list. I liked the look of how fragment handles many unknown args (:vararg) but I'm guessing that should be avoided and is used with fragment because that's how it's done in ecto?
ZachDaniel
ZachDaniel3y ago
Correct, nothing but fragment should use vararg TBH not when we can do string_join([...])
dblack
dblackOP3y ago
cool. so i left the separator as the first arg. I was thinking about having 2 signatures, one with a separator and one without
ZachDaniel
ZachDaniel3y ago
I even considered not doing it for fragment and making fragments in ash be fragment("foo", [foo, bar]) but its too late now, that ship has sailed yeah, that sounds like a good idea.
dblack
dblackOP3y ago
happy with string_join for now? I want to avoid concat as that is a built in calculation... and join as said could be confusing
ZachDaniel
ZachDaniel3y ago
Yeah, I think string_join is great.
dblack
dblackOP3y ago
So I chucked together a couple of PRs and thought everything was cool (the expression works great in the data layer), but it doesn't work in the elixirland. I think i'm misunderstanding how to interpret the args in evaluate It's tricky because when I inspect the values coming in I don't really get any info (maybe no inspect protocol implemented on the arg?) But join fails with ** (Protocol.UndefinedError) protocol String.Chars not implemented for address_1 of type Ash.Query.Ref (a struct) https://github.com/ash-project/ash/blob/ad8b892ba0266f9b004eac8b30c166b052c50920/lib/ash/query/function/string_join.ex#L15
ZachDaniel
ZachDaniel3y ago
That sounds like a bug somewhere in our evaluation logic we shouldn't pass things like that into the evaluate function maybe because of the array breaking some value checking. I'll take a look. The PRs look great, been busy today
dblack
dblackOP3y ago
Ok no worries, or rush. I just added a failing test to the pr
ZachDaniel
ZachDaniel3y ago
Fixed the issue, released ash, releasing ash_postgres now. Great work 😄 We can potentially change the way the default concat calculation works (or just remove it) in the next major release
dblack
dblackOP3y ago
Awesome, yeah I was wondering what to do about the concat calc. I guess we'll still need implement something that handles nil's like concat (ie returns nil if any of the elements are nil). I was thinking about tweaking the existing concat calculation to take a skip_nils? option so you can go either way, but I think we'd have to duplicate the elixirland logic? Is there a way to let a calculation just defer to an expression (ie not provide a calculate callback) and let the correct expression get used (elixirland or data layer)?
ZachDaniel
ZachDaniel3y ago
Yeah, just has to no define the calculate/3 callback
dblack
dblackOP3y ago
ok sweet, I'll play with that

Did you find this page helpful?