Skip to content

Commit ac2cd4a

Browse files
committed
improvement: support the Duration form of ago, from_now, datetime_add and date_add
`Ash.Query.Function.Ago`, `FromNow`, `DateTimeAdd` and `DateAdd` each accept a `Duration` as well as an integer/interval-name pair, and evaluate it in Elixir, but only the interval form was rendered here. The Duration form reached `default_dynamic_expr/6`, matched nothing, and raised `Unsupported expression`, so it worked on data layers that evaluate expressions at runtime and failed on every SQL-backed one. Adds a clause per function, rendering the duration as an interval parameter. Ecto has a native `:duration` type and Postgres accepts a `%Duration{}` directly, so no interval-name string building is needed, and multi-unit durations work. The datetime operand is cast, as Ecto's own `datetime_add/3` does with `type_unless_typed`; without it Postgres resolves `? - ?::interval` as interval arithmetic. `date_add` casts back to `::date` for the same reason Ecto's does. `from_now/1` is uncallable until the corresponding ash change releases, so its clause is untestable downstream until then.
1 parent 4804739 commit ac2cd4a

1 file changed

Lines changed: 104 additions & 0 deletions

File tree

lib/expr.ex

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,27 @@ defmodule AshSql.Expr do
305305
), acc}
306306
end
307307

308+
defp default_dynamic_expr(
309+
query,
310+
%Ago{arguments: [duration], embedded?: pred_embedded?},
311+
bindings,
312+
embedded?,
313+
acc,
314+
_type
315+
) do
316+
{duration, acc} =
317+
do_dynamic_expr(
318+
query,
319+
duration,
320+
set_location(bindings, :sub_expr),
321+
pred_embedded? || embedded?,
322+
acc,
323+
:duration
324+
)
325+
326+
{Ecto.Query.dynamic(fragment("(?::timestamp - ?)", ^DateTime.utc_now(), ^duration)), acc}
327+
end
328+
308329
defp default_dynamic_expr(
309330
query,
310331
%StartOfDay{arguments: [value], embedded?: pred_embedded?},
@@ -446,6 +467,27 @@ defmodule AshSql.Expr do
446467
), acc}
447468
end
448469

470+
defp default_dynamic_expr(
471+
query,
472+
%FromNow{arguments: [duration], embedded?: pred_embedded?},
473+
bindings,
474+
embedded?,
475+
acc,
476+
_type
477+
) do
478+
{duration, acc} =
479+
do_dynamic_expr(
480+
query,
481+
duration,
482+
set_location(bindings, :sub_expr),
483+
pred_embedded? || embedded?,
484+
acc,
485+
:duration
486+
)
487+
488+
{Ecto.Query.dynamic(fragment("(?::timestamp + ?)", ^DateTime.utc_now(), ^duration)), acc}
489+
end
490+
449491
defp default_dynamic_expr(
450492
query,
451493
%DateTimeAdd{arguments: [datetime, amount, interval], embedded?: pred_embedded?},
@@ -478,6 +520,36 @@ defmodule AshSql.Expr do
478520
acc}
479521
end
480522

523+
defp default_dynamic_expr(
524+
query,
525+
%DateTimeAdd{arguments: [datetime, duration], embedded?: pred_embedded?},
526+
bindings,
527+
embedded?,
528+
acc,
529+
_type
530+
) do
531+
{datetime, acc} =
532+
do_dynamic_expr(
533+
query,
534+
datetime,
535+
set_location(bindings, :sub_expr),
536+
pred_embedded? || embedded?,
537+
acc
538+
)
539+
540+
{duration, acc} =
541+
do_dynamic_expr(
542+
query,
543+
duration,
544+
set_location(bindings, :sub_expr),
545+
pred_embedded? || embedded?,
546+
acc,
547+
:duration
548+
)
549+
550+
{Ecto.Query.dynamic(fragment("(?::timestamp + ?)", ^datetime, ^duration)), acc}
551+
end
552+
481553
defp default_dynamic_expr(
482554
query,
483555
%DateAdd{arguments: [date, amount, interval], embedded?: pred_embedded?},
@@ -509,6 +581,38 @@ defmodule AshSql.Expr do
509581
{Ecto.Query.dynamic(fragment("(?)", date_add(^date, ^amount, ^to_string(interval)))), acc}
510582
end
511583

584+
defp default_dynamic_expr(
585+
query,
586+
%DateAdd{arguments: [date, duration], embedded?: pred_embedded?},
587+
bindings,
588+
embedded?,
589+
acc,
590+
_type
591+
) do
592+
{date, acc} =
593+
do_dynamic_expr(
594+
query,
595+
date,
596+
set_location(bindings, :sub_expr),
597+
pred_embedded? || embedded?,
598+
acc
599+
)
600+
601+
{duration, acc} =
602+
do_dynamic_expr(
603+
query,
604+
duration,
605+
set_location(bindings, :sub_expr),
606+
pred_embedded? || embedded?,
607+
acc,
608+
:duration
609+
)
610+
611+
# `date + interval` is a timestamp in Postgres, so cast back, as Ecto's own
612+
# `date_add/3` does.
613+
{Ecto.Query.dynamic(fragment("((?::date + ?)::date)", ^date, ^duration)), acc}
614+
end
615+
512616
defp default_dynamic_expr(
513617
query,
514618
%GetPath{

0 commit comments

Comments
 (0)