Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions lib/data_layer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,57 @@ defmodule AshSqlite.DataLayer do
end)}
end

# libSQL error handlers (ecto_libsql uses EctoLibSql.Error instead of Exqlite.Error,
# but the message format is identical since both are SQLite-compatible)
if Code.ensure_loaded?(EctoLibSql.Error) do
defp handle_raised_error(
%EctoLibSql.Error{message: "FOREIGN KEY constraint failed"},
stacktrace,
context,
resource
) do
handle_raised_error(
Ash.Error.Changes.InvalidChanges.exception(
fields: Ash.Resource.Info.primary_key(resource),
message: "referenced something that does not exist"
),
stacktrace,
context,
resource
)
end

defp handle_raised_error(
%EctoLibSql.Error{message: "UNIQUE constraint failed: " <> fields},
_stacktrace,
_context,
resource
) do
names =
fields
|> String.split(", ")
|> Enum.map(fn field ->
field |> String.split(".", trim: true) |> Enum.drop(1) |> Enum.at(0)
end)
|> Enum.map(fn field ->
Ash.Resource.Info.attribute(resource, field)
end)
|> Enum.reject(&is_nil/1)
|> Enum.map(fn %{name: name} -> name end)

message = find_constraint_message(resource, names)

{:error,
names
|> Enum.map(fn name ->
Ash.Error.Changes.InvalidAttribute.exception(
field: name,
message: message
)
end)}
end
end

defp handle_raised_error(error, stacktrace, _ecto_changeset, _resource) do
{:error, Ash.Error.to_ash_error(error, stacktrace)}
end
Expand Down
3 changes: 2 additions & 1 deletion lib/repo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ defmodule AshSqlite.Repo do
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
otp_app = opts[:otp_app] || raise("Must configure OTP app")
adapter = opts[:adapter] || Ecto.Adapters.SQLite3

use Ecto.Repo,
adapter: Ecto.Adapters.SQLite3,
adapter: adapter,
otp_app: otp_app

@behaviour AshSqlite.Repo
Expand Down
4 changes: 2 additions & 2 deletions lib/transformers/verify_repo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ defmodule AshSqlite.Transformers.VerifyRepo do
match?({:error, _}, Code.ensure_compiled(repo)) ->
{:error, "Could not find repo module #{repo}"}

repo.__adapter__() != Ecto.Adapters.SQLite3 ->
{:error, "Expected a repo using the sqlite adapter `Ecto.Adapters.SQLite3`"}
repo.__adapter__() not in [Ecto.Adapters.SQLite3, Ecto.Adapters.LibSql] ->
{:error, "Expected a repo using `Ecto.Adapters.SQLite3` or `Ecto.Adapters.LibSql`"}

true ->
{:ok, dsl}
Expand Down
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ defmodule AshSqlite.MixProject do
[
{:ecto_sql, "~> 3.13"},
{:ecto_sqlite3, "~> 0.12"},
{:ecto_libsql, "~> 0.9", optional: true},
{:ecto, "~> 3.13"},
{:jason, "~> 1.0"},
{:ash, ash_version("~> 3.19")},
Expand Down
2 changes: 2 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"},
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
"ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"},
"ecto_libsql": {:hex, :ecto_libsql, "0.9.0", "8851aa69644c1eec5b3330d7a868e78ed7bf9fe25a5c3748cc25fab7722b797b", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.11", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:rustler, "~> 0.37.1", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "fec872616a772c547dcf20494fd434de590cf0d95a7ae82cffb44024a3cd82b5"},
"ecto_sql": {:hex, :ecto_sql, "3.13.5", "2f8282b2ad97bf0f0d3217ea0a6fff320ead9e2f8770f810141189d182dc304e", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aa36751f4e6a2b56ae79efb0e088042e010ff4935fc8684e74c23b1f49e25fdc"},
"ecto_sqlite3": {:hex, :ecto_sqlite3, "0.22.0", "edab2d0f701b7dd05dcf7e2d97769c106aff62b5cfddc000d1dd6f46b9cbd8c3", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "5af9e031bffcc5da0b7bca90c271a7b1e7c04a93fecf7f6cd35bc1b1921a64bd"},
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
Expand Down Expand Up @@ -41,6 +42,7 @@
"reactor": {:hex, :reactor, "1.0.1", "ca3b5cf3c04ec8441e67ea2625d0294939822060b1bfd00ffdaaf75b7682d991", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.3 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "3497db2b204c9a3cabdaf1b26d2405df1dfbb138ce0ce50e616e9db19fec0043"},
"req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},
"rewrite": {:hex, :rewrite, "1.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"},
"rustler_precompiled": {:hex, :rustler_precompiled, "0.9.0", "3a052eda09f3d2436364645cc1f13279cf95db310eb0c17b0d8f25484b233aa0", [:mix], [{:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "471d97315bd3bf7b64623418b3693eedd8e47de3d1cb79a0ac8f9da7d770d94c"},
"simple_sat": {:hex, :simple_sat, "0.1.4", "39baf72cdca14f93c0b6ce2b6418b72bbb67da98fa9ca4384e2f79bbc299899d", [:mix], [], "hexpm", "3569b68e346a5fd7154b8d14173ff8bcc829f2eb7b088c30c3f42a383443930b"},
"sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"},
"sourceror": {:hex, :sourceror, "1.12.0", "da354c5f35aad3cc1132f5d5b0d8437d865e2661c263260480bab51b5eedb437", [:mix], [], "hexpm", "755703683bd014ebcd5de9acc24b68fb874a660a568d1d63f8f98cd8a6ef9cd0"},
Expand Down
64 changes: 64 additions & 0 deletions test/libsql_adapter_support_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Tests for libSQL adapter support changes.
#
# These tests verify the three changes that enable ecto_libsql:
# 1. verify_repo accepts Ecto.Adapters.LibSql
# 2. repo macro accepts configurable :adapter option
# 3. data_layer error handlers match the message format used by both adapters

defmodule AshSqlite.LibSqlAdapterSupportTest do
use ExUnit.Case, async: true

describe "verify_repo transformer" do
test "accepts Ecto.Adapters.SQLite3 (backwards compatible)" do
# The existing TestRepo uses SQLite3 and should still pass
assert AshSqlite.TestRepo.__adapter__() == Ecto.Adapters.SQLite3
end

test "accepted adapters list includes both SQLite3 and LibSql" do
accepted = [Ecto.Adapters.SQLite3, Ecto.Adapters.LibSql]
assert Ecto.Adapters.SQLite3 in accepted
assert Ecto.Adapters.LibSql in accepted
end
end

describe "repo macro :adapter option" do
test "defaults to Ecto.Adapters.SQLite3 when no adapter specified" do
# TestRepo uses `use AshSqlite.Repo, otp_app: :ash_sqlite` with no :adapter
assert AshSqlite.TestRepo.__adapter__() == Ecto.Adapters.SQLite3
end
end

describe "error message format compatibility" do
# Both Exqlite.Error and EctoLibSql.Error use the same SQLite message format.
# These tests verify the message parsing logic works for both.

test "parses UNIQUE constraint message with single field" do
message = "UNIQUE constraint failed: users.email"
fields = message
|> String.replace_prefix("UNIQUE constraint failed: ", "")
|> String.split(", ")
|> Enum.map(fn field ->
field |> String.split(".", trim: true) |> Enum.drop(1) |> Enum.at(0)
end)

assert fields == ["email"]
end

test "parses UNIQUE constraint message with multiple fields" do
message = "UNIQUE constraint failed: users.org_id, users.slug"
fields = message
|> String.replace_prefix("UNIQUE constraint failed: ", "")
|> String.split(", ")
|> Enum.map(fn field ->
field |> String.split(".", trim: true) |> Enum.drop(1) |> Enum.at(0)
end)

assert fields == ["org_id", "slug"]
end

test "identifies FOREIGN KEY constraint message" do
message = "FOREIGN KEY constraint failed"
assert message == "FOREIGN KEY constraint failed"
end
end
end