improvement: clean ups, remove operators that don't make sense for sqlite

This commit is contained in:
Zach Daniel 2023-10-05 16:24:39 -04:00
parent ea9c3faccd
commit 9caf99844b
8 changed files with 234 additions and 385 deletions

View file

@ -322,7 +322,7 @@ defmodule AshSqlite.DataLayer do
Mix.Task.run("ash_sqlite.drop", args)
end
import Ecto.Query, only: [from: 2, subquery: 1]
import Ecto.Query, only: [from: 2]
@impl true
def can?(_, :async_engine), do: false

View file

@ -7,17 +7,13 @@ defmodule AshSqlite.Expr do
alias Ash.Query.Function.{
Ago,
At,
Contains,
DateAdd,
DateTimeAdd,
FromNow,
GetPath,
If,
Length,
Now,
StringJoin,
StringSplit,
Today,
Type
}
@ -232,29 +228,6 @@ defmodule AshSqlite.Expr do
)
end
defp do_dynamic_expr(
query,
%Length{arguments: [list], embedded?: pred_embedded?},
bindings,
embedded?,
type
) do
do_dynamic_expr(
query,
%Fragment{
embedded?: pred_embedded?,
arguments: [
raw: "array_length((",
expr: list,
raw: "), 1)"
]
},
bindings,
embedded?,
type
)
end
defp do_dynamic_expr(
query,
%If{arguments: [condition, when_true, when_false], embedded?: pred_embedded?},
@ -319,82 +292,101 @@ defmodule AshSqlite.Expr do
)
end
defp do_dynamic_expr(
query,
%StringJoin{arguments: [values, joiner], embedded?: pred_embedded?},
bindings,
embedded?,
type
) do
do_dynamic_expr(
query,
%Fragment{
embedded?: pred_embedded?,
arguments:
Enum.reduce(values, [raw: "(concat_ws(", expr: joiner], fn value, acc ->
acc ++ [raw: ", ", expr: value]
end) ++ [raw: "))"]
},
bindings,
embedded?,
type
)
end
# Wow, even this doesn't work, because of course it doesn't.
# Doing string joining properly requires a recursive "if not empty" check
# that honestly I don't have the energy to do right now.
# There are commented out tests for this in the calculation tests, make sure those pass, whoever feels like fixing this.
# defp do_dynamic_expr(
# _query,
# %StringJoin{arguments: [values | _], embedded?: _pred_embedded?} = string_join,
# _bindings,
# _embedded?,
# _type
# )
# when not is_list(values) do
# raise "SQLite can only join literal lists, not dynamic values. i.e `string_join([foo, bar])`, but not `string_join(something)`. Got #{inspect(string_join)}"
# end
defp do_dynamic_expr(
query,
%StringSplit{arguments: [string, delimiter, options], embedded?: pred_embedded?},
bindings,
embedded?,
type
) do
if options[:trim?] do
raise "trim?: true not supported by AshSqlite"
else
do_dynamic_expr(
query,
%Fragment{
embedded?: pred_embedded?,
arguments: [
raw: "string_to_array(",
expr: string,
raw: ", NULLIF(",
expr: delimiter,
raw: ", ''))"
]
},
bindings,
embedded?,
type
)
end
end
# defp do_dynamic_expr(
# query,
# %StringJoin{arguments: [values, joiner], embedded?: pred_embedded?},
# bindings,
# embedded?,
# type
# ) do
# # Not optimal, but it works
# last_value = :lists.last(values)
defp do_dynamic_expr(
query,
%StringJoin{arguments: [values], embedded?: pred_embedded?},
bindings,
embedded?,
type
) do
do_dynamic_expr(
query,
%Fragment{
embedded?: pred_embedded?,
arguments:
[raw: "(concat("] ++
(values
|> Enum.reduce([], fn value, acc ->
acc ++ [expr: value]
end)
|> Enum.intersperse({:raw, ", "})) ++
[raw: "))"]
},
bindings,
embedded?,
type
)
end
# values =
# values
# |> :lists.droplast()
# |> Enum.map(&{:not_last, &1})
# |> Enum.concat([{:last, last_value}])
# do_dynamic_expr(
# query,
# %Fragment{
# embedded?: pred_embedded?,
# arguments:
# Enum.reduce(values, [raw: "("], fn
# {:last, value}, acc ->
# acc ++
# [
# raw: "COALESCE(",
# expr: value,
# raw: ", '')"
# ]
# {:not_last, value}, acc ->
# acc ++
# [
# raw: "(CASE ",
# expr: value,
# raw: " WHEN NULL THEN '' ELSE ",
# expr: value,
# raw: " || ",
# expr: joiner,
# raw: " END) || "
# ]
# end)
# |> Enum.concat(raw: ")")
# },
# bindings,
# embedded?,
# type
# )
# end
# defp do_dynamic_expr(
# query,
# %StringJoin{arguments: [values], embedded?: pred_embedded?},
# bindings,
# embedded?,
# type
# ) do
# do_dynamic_expr(
# query,
# %Fragment{
# embedded?: pred_embedded?,
# arguments:
# Enum.reduce(values, {[raw: "("], true}, fn value, {acc, first?} ->
# add =
# if first? do
# [expr: value]
# else
# [raw: " || COALESCE(", expr: value, raw: ", '')"]
# end
# {acc ++ add, false}
# end)
# |> elem(0)
# |> Enum.concat(raw: ")")
# },
# bindings,
# embedded?,
# type
# )
# end
# Sorry :(
# This is bad to do, but is the only reasonable way I could find.
@ -642,23 +634,21 @@ defmodule AshSqlite.Expr do
defp do_dynamic_expr(
query,
%Ash.CiString{string: string} = expression,
%Ash.CiString{string: string},
bindings,
embedded?,
type
) do
string = do_dynamic_expr(query, string, bindings, embedded?)
require_extension!(query, "citext", expression)
do_dynamic_expr(
query,
%Fragment{
embedded?: embedded?,
arguments: [
raw: "",
raw: "(",
casted_expr: string,
raw: "::citext"
raw: "collate nocase)"
]
},
bindings,
@ -842,6 +832,10 @@ defmodule AshSqlite.Expr do
embedded?,
type
) do
if !bindings[:parent_bindings] do
raise "Used `parent/1` without parent context. AshSqlite is not capable of supporting `parent/1` in relationship where clauses yet."
end
parent? = Map.get(bindings.parent_bindings, :parent_is_parent_as?, true)
do_dynamic_expr(
@ -1071,7 +1065,6 @@ defmodule AshSqlite.Expr do
if is_list(other) do
list_expr(query, other, bindings, true, type)
else
IO.inspect(other, structs: false)
raise "Unsupported expression in AshSqlite query: #{inspect(other)}"
end
else
@ -1223,7 +1216,7 @@ defmodule AshSqlite.Expr do
embedded?: pred_embedded?,
arguments:
[
raw: "jsonb_extract_path(",
raw: "json_extract(",
expr: left,
raw: ","
] ++ path_frags
@ -1241,17 +1234,6 @@ defmodule AshSqlite.Expr do
end
end
defp require_extension!(query, extension, context) do
repo = AshSqlite.DataLayer.Info.repo(query.__ash_bindings__.resource)
unless extension in repo.installed_extensions() do
raise Ash.Error.Query.InvalidExpression,
expression: context,
message:
"The #{extension} extension needs to be installed before #{inspect(context)} can be used. Please add \"#{extension}\" to the list of installed_extensions in #{inspect(repo)}."
end
end
defp determine_type_at_path(type, path) do
path
|> Enum.reject(&is_integer/1)

View file

@ -425,7 +425,6 @@ defmodule AshSqlite.Join do
# Ash.Resource.Info.primary_key(joined_query.__ash_bindings__.resource)
# )
# )
# |> IO.inspect()
# else
joined_query
# end

View file

@ -3,7 +3,6 @@ defmodule AshSqlite.CalculationTest do
alias AshSqlite.Test.{Account, Api, Author, Comment, Post, User}
require Ash.Query
import Ash.Expr
test "calculations can refer to embedded attributes" do
author =
@ -185,105 +184,50 @@ defmodule AshSqlite.CalculationTest do
assert account.active
end
describe "string join expression" do
test "no nil values" do
author =
Author
|> Ash.Changeset.for_create(:create, %{
first_name: "Bill",
last_name: "Jones",
bio: %{title: "Mr.", bio: "Bones"}
})
|> Api.create!()
# describe "string join expression" do
# test "no nil values" do
# author =
# Author
# |> Ash.Changeset.for_create(:create, %{
# first_name: "Bill",
# last_name: "Jones",
# bio: %{title: "Mr.", bio: "Bones"}
# })
# |> Api.create!()
assert %{
full_name_with_nils: "Bill Jones",
full_name_with_nils_no_joiner: "BillJones"
} =
Author
|> Ash.Query.filter(id == ^author.id)
|> Ash.Query.load(:full_name_with_nils)
|> Ash.Query.load(:full_name_with_nils_no_joiner)
|> Api.read_one!()
end
# assert %{
# full_name_with_nils: "Bill Jones",
# full_name_with_nils_no_joiner: "BillJones"
# } =
# Author
# |> Ash.Query.filter(id == ^author.id)
# |> Ash.Query.load(:full_name_with_nils)
# |> Ash.Query.load(:full_name_with_nils_no_joiner)
# |> Api.read_one!()
# end
test "with nil value" do
author =
Author
|> Ash.Changeset.for_create(:create, %{
first_name: "Bill",
bio: %{title: "Mr.", bio: "Bones"}
})
|> Api.create!()
# test "with nil value" do
# author =
# Author
# |> Ash.Changeset.for_create(:create, %{
# first_name: "Bill",
# bio: %{title: "Mr.", bio: "Bones"}
# })
# |> Api.create!()
assert %{
full_name_with_nils: "Bill",
full_name_with_nils_no_joiner: "Bill"
} =
Author
|> Ash.Query.filter(id == ^author.id)
|> Ash.Query.load(:full_name_with_nils)
|> Ash.Query.load(:full_name_with_nils_no_joiner)
|> Api.read_one!()
end
end
# Logger.configure(level: :debug)
test "arguments with cast_in_query?: false are not cast" do
Post
|> Ash.Changeset.new(%{title: "match", score: 42})
|> Api.create!()
Post
|> Ash.Changeset.new(%{title: "not", score: 42})
|> Api.create!()
assert [post] =
Post
|> Ash.Query.filter(similarity(search: expr(query(search: "match"))))
|> Api.read!()
assert post.title == "match"
end
describe "string split expression" do
test "with the default delimiter" do
author =
Author
|> Ash.Changeset.for_create(:create, %{
first_name: "Bill",
last_name: "Jones",
bio: %{title: "Mr.", bio: "Bones"}
})
|> Api.create!()
assert %{
split_full_name: ["Bill", "Jones"]
} =
Author
|> Ash.Query.filter(id == ^author.id)
|> Ash.Query.load(:split_full_name)
|> Api.read_one!()
end
test "trimming whitespace" do
author =
Author
|> Ash.Changeset.for_create(:create, %{
first_name: "Bill ",
last_name: "Jones ",
bio: %{title: "Mr.", bio: "Bones"}
})
|> Api.create!()
assert %{
split_full_name: ["Bill", "Jones"]
} =
Author
|> Ash.Query.filter(id == ^author.id)
|> Ash.Query.load([:split_full_name])
|> Api.read_one!()
end
end
# assert %{
# full_name_with_nils: "Bill",
# full_name_with_nils_no_joiner: "Bill"
# } =
# Author
# |> Ash.Query.filter(id == ^author.id)
# |> Ash.Query.load(:full_name_with_nils)
# |> Ash.Query.load(:full_name_with_nils_no_joiner)
# |> Api.read_one!()
# end
# end
describe "-/1" do
test "makes numbers negative" do

View file

@ -474,79 +474,6 @@ defmodule AshSqlite.FilterTest do
end
end
describe "length/1" do
test "it works with a list attribute" do
author1 =
Author
|> Ash.Changeset.new(%{badges: [:author_of_the_year]})
|> Api.create!()
_author2 =
Author
|> Ash.Changeset.new(%{badges: []})
|> Api.create!()
author1_id = author1.id
assert [%{id: ^author1_id}] =
Author
|> Ash.Query.filter(length(badges) > 0)
|> Api.read!()
end
test "it works with nil" do
author1 =
Author
|> Ash.Changeset.new(%{badges: [:author_of_the_year]})
|> Api.create!()
_author2 =
Author
|> Ash.Changeset.new()
|> Api.create!()
author1_id = author1.id
assert [%{id: ^author1_id}] =
Author
|> Ash.Query.filter(length(badges || []) > 0)
|> Api.read!()
end
test "it works with a list" do
author1 =
Author
|> Ash.Changeset.new()
|> Api.create!()
author1_id = author1.id
explicit_list = [:foo]
assert [%{id: ^author1_id}] =
Author
|> Ash.Query.filter(length(^explicit_list) > 0)
|> Api.read!()
assert [] =
Author
|> Ash.Query.filter(length(^explicit_list) > 1)
|> Api.read!()
end
test "it raises with bad values" do
Author
|> Ash.Changeset.new()
|> Api.create!()
assert_raise(Ash.Error.Unknown, fn ->
Author
|> Ash.Query.filter(length(first_name) > 0)
|> Api.read!()
end)
end
end
describe "exists/2" do
test "it works with single relationships" do
post =

View file

@ -106,115 +106,117 @@ defmodule AshSqlite.Test.LoadTest do
end
describe "lateral join loads" do
test "parent references are resolved" do
post1 =
Post
|> Ash.Changeset.new(%{title: "title"})
|> Api.create!()
# uncomment when lateral join is supported
# it does not necessarily have to be implemented *exactly* as lateral join
# test "parent references are resolved" do
# post1 =
# Post
# |> Ash.Changeset.new(%{title: "title"})
# |> Api.create!()
post2 =
Post
|> Ash.Changeset.new(%{title: "title"})
|> Api.create!()
# post2 =
# Post
# |> Ash.Changeset.new(%{title: "title"})
# |> Api.create!()
post2_id = post2.id
# post2_id = post2.id
post3 =
Post
|> Ash.Changeset.new(%{title: "no match"})
|> Api.create!()
# post3 =
# Post
# |> Ash.Changeset.new(%{title: "no match"})
# |> Api.create!()
assert [%{posts_with_matching_title: [%{id: ^post2_id}]}] =
Post
|> Ash.Query.load(:posts_with_matching_title)
|> Ash.Query.filter(id == ^post1.id)
|> Api.read!()
# assert [%{posts_with_matching_title: [%{id: ^post2_id}]}] =
# Post
# |> Ash.Query.load(:posts_with_matching_title)
# |> Ash.Query.filter(id == ^post1.id)
# |> Api.read!()
assert [%{posts_with_matching_title: []}] =
Post
|> Ash.Query.load(:posts_with_matching_title)
|> Ash.Query.filter(id == ^post3.id)
|> Api.read!()
end
# assert [%{posts_with_matching_title: []}] =
# Post
# |> Ash.Query.load(:posts_with_matching_title)
# |> Ash.Query.filter(id == ^post3.id)
# |> Api.read!()
# end
test "parent references work when joining for filters" do
%{id: post1_id} =
Post
|> Ash.Changeset.new(%{title: "title"})
|> Api.create!()
# test "parent references work when joining for filters" do
# %{id: post1_id} =
# Post
# |> Ash.Changeset.new(%{title: "title"})
# |> Api.create!()
post2 =
Post
|> Ash.Changeset.new(%{title: "title"})
|> Api.create!()
# post2 =
# Post
# |> Ash.Changeset.new(%{title: "title"})
# |> Api.create!()
Post
|> Ash.Changeset.new(%{title: "no match"})
|> Api.create!()
# Post
# |> Ash.Changeset.new(%{title: "no match"})
# |> Api.create!()
Post
|> Ash.Changeset.new(%{title: "no match"})
|> Api.create!()
# Post
# |> Ash.Changeset.new(%{title: "no match"})
# |> Api.create!()
assert [%{id: ^post1_id}] =
Post
|> Ash.Query.filter(posts_with_matching_title.id == ^post2.id)
|> Api.read!()
end
# assert [%{id: ^post1_id}] =
# Post
# |> Ash.Query.filter(posts_with_matching_title.id == ^post2.id)
# |> Api.read!()
# end
test "lateral join loads (loads with limits or offsets) are supported" do
assert %Post{comments: %Ash.NotLoaded{type: :relationship}} =
post =
Post
|> Ash.Changeset.new(%{title: "title"})
|> Api.create!()
# test "lateral join loads (loads with limits or offsets) are supported" do
# assert %Post{comments: %Ash.NotLoaded{type: :relationship}} =
# post =
# Post
# |> Ash.Changeset.new(%{title: "title"})
# |> Api.create!()
Comment
|> Ash.Changeset.new(%{title: "abc"})
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|> Api.create!()
# Comment
# |> Ash.Changeset.new(%{title: "abc"})
# |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
# |> Api.create!()
Comment
|> Ash.Changeset.new(%{title: "def"})
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|> Api.create!()
# Comment
# |> Ash.Changeset.new(%{title: "def"})
# |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
# |> Api.create!()
comments_query =
Comment
|> Ash.Query.limit(1)
|> Ash.Query.sort(:title)
# comments_query =
# Comment
# |> Ash.Query.limit(1)
# |> Ash.Query.sort(:title)
results =
Post
|> Ash.Query.load(comments: comments_query)
|> Api.read!()
# results =
# Post
# |> Ash.Query.load(comments: comments_query)
# |> Api.read!()
assert [%Post{comments: [%{title: "abc"}]}] = results
# assert [%Post{comments: [%{title: "abc"}]}] = results
comments_query =
Comment
|> Ash.Query.limit(1)
|> Ash.Query.sort(title: :desc)
# comments_query =
# Comment
# |> Ash.Query.limit(1)
# |> Ash.Query.sort(title: :desc)
results =
Post
|> Ash.Query.load(comments: comments_query)
|> Api.read!()
# results =
# Post
# |> Ash.Query.load(comments: comments_query)
# |> Api.read!()
assert [%Post{comments: [%{title: "def"}]}] = results
# assert [%Post{comments: [%{title: "def"}]}] = results
comments_query =
Comment
|> Ash.Query.limit(2)
|> Ash.Query.sort(title: :desc)
# comments_query =
# Comment
# |> Ash.Query.limit(2)
# |> Ash.Query.sort(title: :desc)
results =
Post
|> Ash.Query.load(comments: comments_query)
|> Api.read!()
# results =
# Post
# |> Ash.Query.load(comments: comments_query)
# |> Api.read!()
assert [%Post{comments: [%{title: "def"}, %{title: "abc"}]}] = results
end
# assert [%Post{comments: [%{title: "def"}, %{title: "abc"}]}] = results
# end
test "loading many to many relationships on records works without loading its join relationship when using code interface" do
source_post =

View file

@ -28,9 +28,9 @@ defmodule AshSqlite.Test.Author do
calculations do
calculate(:title, :string, expr(bio[:title]))
calculate(:full_name, :string, expr(first_name <> " " <> last_name))
calculate(:full_name_with_nils, :string, expr(string_join([first_name, last_name], " ")))
calculate(:full_name_with_nils_no_joiner, :string, expr(string_join([first_name, last_name])))
calculate(:split_full_name, {:array, :string}, expr(string_split(full_name)))
# calculate(:full_name_with_nils, :string, expr(string_join([first_name, last_name], " ")))
# calculate(:full_name_with_nils_no_joiner, :string, expr(string_join([first_name, last_name])))
# calculate(:split_full_name, {:array, :string}, expr(string_split(full_name)))
calculate(:first_name_or_bob, :string, expr(first_name || "bob"))
calculate(:first_name_and_bob, :string, expr(first_name && "bob"))

View file

@ -98,11 +98,6 @@ defmodule AshSqlite.Test.Post do
belongs_to(:author, AshSqlite.Test.Author)
has_many :posts_with_matching_title, __MODULE__ do
no_attributes?(true)
filter(expr(parent(title) == title and parent(id) != id))
end
has_many(:comments, AshSqlite.Test.Comment, destination_attribute: :post_id)
has_many :comments_matching_post_title, AshSqlite.Test.Comment do