# SPDX-FileCopyrightText: 2023 ash_sqlite contributors # # SPDX-License-Identifier: MIT defmodule AshSqlite.SqlImplementation do @moduledoc false use AshSql.Implementation require Ecto.Query require Ash.Expr @impl true def manual_relationship_function, do: :ash_sqlite_join @impl true def manual_relationship_subquery_function, do: :ash_sqlite_subquery @impl true def strpos_function, do: "instr" @impl true def ilike?, do: false @impl true def expr( query, %like{arguments: [arg1, arg2], embedded?: pred_embedded?}, bindings, embedded?, acc, type ) when like in [AshSqlite.Functions.Like, AshSqlite.Functions.ILike] do {arg1, acc} = AshSql.Expr.dynamic_expr(query, arg1, bindings, pred_embedded? || embedded?, :string, acc) {arg2, acc} = AshSql.Expr.dynamic_expr(query, arg2, bindings, pred_embedded? || embedded?, :string, acc) inner_dyn = if like == AshSqlite.Functions.Like do Ecto.Query.dynamic(like(^arg1, ^arg2)) else Ecto.Query.dynamic(like(fragment("LOWER(?)", ^arg1), fragment("LOWER(?)", ^arg2))) end if type != Ash.Type.Boolean do {:ok, inner_dyn, acc} else {:ok, Ecto.Query.dynamic(type(^inner_dyn, ^type)), acc} end end def expr( query, %Ash.Query.Operator.In{ right: %Ash.Query.Function.Type{arguments: [right | _]} = type } = op, bindings, embedded?, acc, type ) when is_list(right) or is_struct(right, MapSet) do expr(query, %{op | right: right}, bindings, embedded?, acc, type) end def expr( query, %Ash.Query.Operator.In{left: left, right: right, embedded?: pred_embedded?}, bindings, embedded?, acc, type ) when is_list(right) or is_struct(right, MapSet) do right |> Enum.reduce(nil, fn val, acc -> if is_nil(acc) do %Ash.Query.Operator.Eq{left: left, right: val} else %Ash.Query.BooleanExpression{ op: :or, left: acc, right: %Ash.Query.Operator.Eq{left: left, right: val} } end end) |> then(fn expr -> {expr, acc} = AshSql.Expr.dynamic_expr(query, expr, bindings, pred_embedded? || embedded?, type, acc) {:ok, expr, acc} end) end def expr( query, %Ash.Query.Function.GetPath{ arguments: [%Ash.Query.Ref{attribute: %{type: type}}, right] } = get_path, bindings, embedded?, acc, nil ) when is_atom(type) and is_list(right) do if Ash.Type.embedded_type?(type) do type = determine_type_at_path(type, right) do_get_path(query, get_path, bindings, embedded?, acc, type) else do_get_path(query, get_path, bindings, embedded?, acc) end end def expr( query, %Ash.Query.Function.GetPath{ arguments: [%Ash.Query.Ref{attribute: %{type: {:array, type}}}, right] } = get_path, bindings, embedded?, acc, nil ) when is_atom(type) and is_list(right) do if Ash.Type.embedded_type?(type) do type = determine_type_at_path(type, right) do_get_path(query, get_path, bindings, embedded?, acc, type) else do_get_path(query, get_path, bindings, embedded?, acc) end end def expr( query, %Ash.Query.Function.GetPath{} = get_path, bindings, embedded?, acc, type ) do do_get_path(query, get_path, bindings, embedded?, acc, type) end def expr( query, %Ash.Query.Function.StringTrim{arguments: [value], embedded?: pred_embedded?}, bindings, embedded?, acc, type ) do {expr, acc} = AshSql.Expr.dynamic_expr( query, %Ash.Query.Function.Fragment{ embedded?: pred_embedded?, arguments: [ raw: "TRIM(", expr: value, raw: ")" ] }, bindings, embedded?, type, acc ) {:ok, expr, acc} end def expr( query, %Ash.Query.Function.StringLength{arguments: [value], embedded?: pred_embedded?}, bindings, embedded?, acc, type ) do {expr, acc} = AshSql.Expr.dynamic_expr( query, %Ash.Query.Function.Fragment{ embedded?: pred_embedded?, arguments: [ raw: "LENGTH(", expr: value, raw: ")" ] }, bindings, embedded?, type, acc ) {:ok, expr, acc} end # Handle comparisons involving map values - SQLite can't directly compare maps, # so we convert both sides to JSON strings for comparison def expr( query, %Ash.Query.Operator.NotEq{left: left, right: right, embedded?: pred_embedded?}, bindings, embedded?, acc, type ) when is_non_struct_map(left) or is_non_struct_map(right) do handle_map_comparison(query, :!=, left, right, pred_embedded?, bindings, embedded?, acc, type) end def expr( query, %Ash.Query.Operator.Eq{left: left, right: right, embedded?: pred_embedded?}, bindings, embedded?, acc, type ) when is_non_struct_map(left) or is_non_struct_map(right) do handle_map_comparison(query, :==, left, right, pred_embedded?, bindings, embedded?, acc, type) end @impl true def expr( _query, _expr, _bindings, _embedded?, _acc, _type ) do :error end defp handle_map_comparison( query, operator, left, right, pred_embedded?, bindings, embedded?, acc, type ) do {left_expr, acc} = as_json(query, left, pred_embedded?, bindings, embedded?, acc, type) {right_expr, acc} = as_json(query, right, pred_embedded?, bindings, embedded?, acc, type) result = case operator do :== -> Ecto.Query.dynamic(^left_expr == ^right_expr) :!= -> Ecto.Query.dynamic(^left_expr != ^right_expr) end {:ok, result, acc} end defp as_json(query, value, pred_embedded?, bindings, embedded?, acc, type) do if plain_map?(value) do AshSql.Expr.dynamic_expr( query, Jason.encode!(value), bindings, pred_embedded? || embedded?, :string, acc ) else AshSql.Expr.dynamic_expr( query, %Ash.Query.Function.Fragment{ embedded?: pred_embedded?, arguments: [raw: "json(", expr: value, raw: ")"] }, bindings, embedded?, type, acc ) end end defp plain_map?(value) when is_map(value) and not is_struct(value), do: true defp plain_map?(_), do: false @impl true def type_expr(expr, nil), do: expr def type_expr(expr, type) when is_atom(type) do type = Ash.Type.get_type(type) cond do !Ash.Type.ash_type?(type) -> Ecto.Query.dynamic(type(^expr, ^type)) Ash.Type.storage_type(type, []) == :ci_string -> Ecto.Query.dynamic(fragment("(? COLLATE NOCASE)", ^expr)) true -> Ecto.Query.dynamic(type(^expr, ^Ash.Type.storage_type(type, []))) end end def type_expr(expr, type) do case type do {:parameterized, {inner_type, constraints}} -> if inner_type.type(constraints) == :ci_string do Ecto.Query.dynamic(fragment("(? COLLATE NOCASE)", ^expr)) else Ecto.Query.dynamic(type(^expr, ^type)) end nil -> expr type -> Ecto.Query.dynamic(type(^expr, ^type)) end end @impl true def table(resource) do AshSqlite.DataLayer.Info.table(resource) end @impl true def schema(_resource) do nil end @impl true def repo(resource, kind) do AshSqlite.DataLayer.Info.repo(resource, kind) end @impl true def multicolumn_distinct?, do: false @impl true def parameterized_type({:parameterized, _} = type, _) do type end def parameterized_type({:parameterized, _, _} = type, _) do type end def parameterized_type({:in, type}, constraints) do parameterized_type({:array, type}, constraints) end def parameterized_type({:array, type}, constraints) do case parameterized_type(type, constraints[:items] || []) do nil -> nil type -> {:array, type} end end def parameterized_type({type, constraints}, []) do parameterized_type(type, constraints) end def parameterized_type(type, _constraints) when type in [Ash.Type.Map, Ash.Type.Map.EctoType], do: nil def parameterized_type(type, constraints) do if Ash.Type.ash_type?(type) do cast_in_query? = if function_exported?(Ash.Type, :cast_in_query?, 2) do Ash.Type.cast_in_query?(type, constraints) else Ash.Type.cast_in_query?(type) end if cast_in_query? do parameterized_type(Ash.Type.ecto_type(type), constraints) else nil end else if is_atom(type) && :erlang.function_exported(type, :type, 1) do Ecto.ParameterizedType.init(type, constraints) else type end end end @impl true def determine_types(mod, args, returns \\ nil) do returns = case returns do {:parameterized, _} -> nil {:array, {:parameterized, _}} -> nil {:array, {type, constraints}} when type != :array -> {type, [items: constraints]} {:array, _} -> nil {type, constraints} -> {type, constraints} other -> other end {types, new_returns} = Ash.Expr.determine_types(mod, args, returns) {types, new_returns || returns} end defp do_get_path( query, %Ash.Query.Function.GetPath{arguments: [left, right], embedded?: pred_embedded?}, bindings, embedded?, acc, type \\ nil ) do path = "$." <> Enum.join(right, ".") {expr, acc} = AshSql.Expr.dynamic_expr( query, %Ash.Query.Function.Fragment{ embedded?: pred_embedded?, arguments: [ raw: "json_extract(", expr: left, raw: ", ", expr: path, raw: ")" ] }, bindings, embedded?, type, acc ) if type do {expr, acc} = AshSql.Expr.dynamic_expr( query, %Ash.Query.Function.Type{arguments: [expr, type, []]}, bindings, embedded?, type, acc ) {:ok, expr, acc} else {:ok, expr, acc} end end defp determine_type_at_path(type, path) do path |> Enum.reject(&is_integer/1) |> do_determine_type_at_path(type) end defp do_determine_type_at_path([], _), do: nil defp do_determine_type_at_path([item], type) do case Ash.Resource.Info.attribute(type, item) do nil -> nil %{type: {:array, type}, constraints: constraints} -> constraints = constraints[:items] || [] {type, constraints} %{type: type, constraints: constraints} -> {type, constraints} end end defp do_determine_type_at_path([item | rest], type) do case Ash.Resource.Info.attribute(type, item) do nil -> nil %{type: {:array, type}} -> if Ash.Type.embedded_type?(type) do type else nil end %{type: type} -> if Ash.Type.embedded_type?(type) do type else nil end end |> case do nil -> nil type -> do_determine_type_at_path(rest, type) end end end