Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
011a05a
fix: Add some more RETURNING clause tests, and fix JSON encoding and …
ocean Jan 14, 2026
f4f0265
feat: Add ecto_sqlite3 compatibility test suite
ocean Jan 14, 2026
47125bf
docs: Add comprehensive ecto_sqlite3 compatibility testing documentation
ocean Jan 14, 2026
ab0c6f3
fix: Switch ecto_sqlite3 compat tests to manual table creation
ocean Jan 14, 2026
4a2ddf9
docs: Update compatibility testing status and findings
ocean Jan 14, 2026
71cd4eb
docs: Add comprehensive session summary
ocean Jan 14, 2026
89402b5
Fix ecto_libsql compatibility tests - Timestamps, JSON, and BLOB tests
ocean Jan 14, 2026
36795a0
Add comprehensive session summary for compatibility test fixes
ocean Jan 14, 2026
44c813f
Apply formatter fixes to all modified files
ocean Jan 14, 2026
79c5443
Add datetime roundtrip assertions in type_compatibility_test
ocean Jan 14, 2026
4dd6aac
fix: Add per-test cleanup and timestamp columns to CRUD compatibility…
ocean Jan 14, 2026
51ff6be
docs: Add SQLite-specific query limitations and compatibility testing…
ocean Jan 14, 2026
0c08926
Fix: Handle :serial and :bigserial types in migrations
ocean Jan 14, 2026
ea3b047
refactor: Add explicit nil handling in json_encode/1 function
ocean Jan 14, 2026
6419a18
refactor: Simplify redundant assertion condition in returning_test.exs
ocean Jan 14, 2026
12c4d50
style: Format ecto_sqlite3_returning_debug_test.exs for code style co…
ocean Jan 14, 2026
dd49e41
docs: Add completion summary for CI fixes
ocean Jan 14, 2026
36f534c
fix: Remove unused variables and imports in test files
ocean Jan 14, 2026
c24a071
refactor: Improve AccountUser schema with associations and validation
ocean Jan 14, 2026
ccab556
refactor: Address code review feedback and improve test quality
ocean Jan 14, 2026
95a3ae2
feat: Add CHECK constraint support for column-level constraints
ocean Jan 14, 2026
f484d4f
fix: Add datetime microsecond type loading support
ocean Jan 14, 2026
441fc97
docs: Update CHANGELOG with summary of unreleased changes
ocean Jan 14, 2026
d38d7bf
feat: Add comprehensive type loader/dumper support
ocean Jan 14, 2026
e5d0cfb
chore: Remove unneeded docs
ocean Jan 14, 2026
9dcdddb
chore: Sync beads with type enhancement issues
ocean Jan 14, 2026
7d5f7c2
docs: Fix incorrect BLOB null byte claim and add missing comma
ocean Jan 14, 2026
d160e3b
test: Fix microsecond precision assertions in datetime tests
ocean Jan 14, 2026
a5f565f
test: Add assertion for update result in timestamp compat test
ocean Jan 14, 2026
e436678
test: Remove debug IO.inspect calls from timestamp compat test
ocean Jan 14, 2026
ef97b80
refactor: Improve test reliability and error handling
ocean Jan 14, 2026
fffd42e
fix: Add nil-handling to date, time, and bool encode functions
ocean Jan 14, 2026
232a9f6
refactor: Improve test accuracy in type loader/dumper tests
ocean Jan 14, 2026
bfb0a79
refactor: Address code review feedback for test quality and documenta…
ocean Jan 14, 2026
f363618
Fix datetime_decode to handle timezone-aware ISO8601 strings
ocean Jan 14, 2026
8b1d2de
Add Ecto dumper path tests for nil value encoding
ocean Jan 14, 2026
65dec2b
Update decimal SQL query assertions to accept both numeric and string…
ocean Jan 14, 2026
8ee23c9
Strengthen microsecond preservation tests and fix DB collision issue
ocean Jan 15, 2026
63d3db4
tests: Improve type tests
ocean Jan 15, 2026
81b2cf3
Add microsecond assertions for datetime_usec fields in round-trip test
ocean Jan 15, 2026
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
52 changes: 52 additions & 0 deletions lib/ecto/adapters/libsql.ex
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ defmodule Ecto.Adapters.LibSql do
def loaders(:date, type), do: [&date_decode/1, type]
def loaders(:time, type), do: [&time_decode/1, type]
def loaders(:decimal, type), do: [&decimal_decode/1, type]
def loaders(:json, type), do: [&json_decode/1, type]
def loaders(:map, type), do: [&json_decode/1, type]
def loaders({:array, _}, type), do: [&json_array_decode/1, type]
def loaders(_primitive, type), do: [type]

defp bool_decode(0), do: {:ok, false}
Expand Down Expand Up @@ -265,6 +268,31 @@ defmodule Ecto.Adapters.LibSql do

defp decimal_decode(value), do: {:ok, value}

defp json_decode(value) when is_binary(value) do
case Jason.decode(value) do
{:ok, decoded} -> {:ok, decoded}
{:error, _} -> :error
end
end

defp json_decode(value) when is_map(value), do: {:ok, value}
defp json_decode(value), do: {:ok, value}

defp json_array_decode(value) when is_binary(value) do
case value do
"" -> {:ok, []} # Empty string defaults to empty array
_ ->
case Jason.decode(value) do
{:ok, decoded} when is_list(decoded) -> {:ok, decoded}
{:ok, _} -> :error
{:error, _} -> :error
end
end
end

defp json_array_decode(value) when is_list(value), do: {:ok, value}
defp json_array_decode(_value), do: :error
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@doc false
def dumpers(:binary, type), do: [type]
def dumpers(:binary_id, type), do: [type]
Expand All @@ -274,11 +302,18 @@ defmodule Ecto.Adapters.LibSql do
def dumpers(:date, type), do: [type, &date_encode/1]
def dumpers(:time, type), do: [type, &time_encode/1]
def dumpers(:decimal, type), do: [type, &decimal_encode/1]
def dumpers(:json, type), do: [type, &json_encode/1]
def dumpers(:map, type), do: [type, &json_encode/1]
def dumpers({:array, _}, type), do: [type, &array_encode/1]
def dumpers(_primitive, type), do: [type]

defp bool_encode(false), do: {:ok, 0}
defp bool_encode(true), do: {:ok, 1}

defp datetime_encode(%DateTime{} = datetime) do
{:ok, DateTime.to_iso8601(datetime)}
end

defp datetime_encode(%NaiveDateTime{} = datetime) do
{:ok, NaiveDateTime.to_iso8601(datetime)}
end
Comment thread
ocean marked this conversation as resolved.
Expand All @@ -294,4 +329,21 @@ defmodule Ecto.Adapters.LibSql do
defp decimal_encode(%Decimal{} = decimal) do
{:ok, Decimal.to_string(decimal)}
end

defp json_encode(value) when is_binary(value), do: {:ok, value}
defp json_encode(value) when is_map(value) or is_list(value) do
case Jason.encode(value) do
{:ok, json} -> {:ok, json}
{:error, _} -> :error
end
end
defp json_encode(value), do: {:ok, value}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

defp array_encode(value) when is_list(value) do
case Jason.encode(value) do
{:ok, json} -> {:ok, json}
{:error, _} -> :error
end
end
defp array_encode(value), do: {:ok, value}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
end
104 changes: 104 additions & 0 deletions test/ecto_returning_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
defmodule EctoLibSql.EctoReturningStructTest do
use ExUnit.Case, async: false

defmodule TestRepo do
use Ecto.Repo, otp_app: :ecto_libsql, adapter: Ecto.Adapters.LibSql
end

defmodule User do
use Ecto.Schema
import Ecto.Changeset

schema "users" do
field :name, :string
field :email, :string
timestamps()
end

def changeset(user, attrs) do
user
|> cast(attrs, [:name, :email])
|> validate_required([:name, :email])
end
end

@test_db "z_ecto_libsql_test-ecto_returning.db"

setup_all do
{:ok, _} = TestRepo.start_link(database: @test_db)

# Create table
Ecto.Adapters.SQL.query!(TestRepo, """
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL,
inserted_at DATETIME,
updated_at DATETIME
)
""")

on_exit(fn ->
EctoLibSql.TestHelpers.cleanup_db_files(@test_db)
end)

:ok
end
Comment thread
coderabbitai[bot] marked this conversation as resolved.

test "Repo.insert returns populated struct with id and timestamps" do
changeset = User.changeset(%User{}, %{name: "Alice", email: "alice@example.com"})

IO.puts("\n=== Test: INSERT RETURNING via Repo.insert ===")
result = TestRepo.insert(changeset)

IO.inspect(result, label: "Insert result")

case result do
{:ok, user} ->
IO.inspect(user, label: "Returned user struct")

# These assertions should pass if RETURNING struct mapping works
assert user.id != nil, "❌ FAIL: ID is nil (struct mapping broken)"
assert is_integer(user.id) and user.id > 0, "ID should be positive integer"
assert user.name == "Alice", "Name should match"
assert user.email == "alice@example.com", "Email should match"
assert user.inserted_at != nil, "❌ FAIL: inserted_at is nil (timestamp conversion broken)"
assert user.updated_at != nil, "❌ FAIL: updated_at is nil (timestamp conversion broken)"

IO.puts("✅ PASS: Struct mapping and timestamp conversion working")
:ok

{:error, changeset} ->
IO.inspect(changeset, label: "Error changeset")
flunk("Insert failed: #{inspect(changeset)}")
end
end

test "Multiple inserts return correctly populated structs" do
results =
for i <- 1..3 do
user_data = %{
name: "User#{i}",
email: "user#{i}@example.com"
}

changeset = User.changeset(%User{}, user_data)
{:ok, user} = TestRepo.insert(changeset)
user
end

assert length(results) == 3

Enum.each(results, fn user ->
assert user.id != nil, "All users should have IDs"
assert user.inserted_at != nil, "All users should have inserted_at"
assert user.updated_at != nil, "All users should have updated_at"
end)

# IDs should be unique
ids = Enum.map(results, & &1.id)
assert length(Enum.uniq(ids)) == 3, "All IDs should be unique"

IO.puts("✅ PASS: Multiple inserts return populated structs")
end
end
77 changes: 77 additions & 0 deletions test/returning_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
defmodule EctoLibSql.ReturningTest do
use ExUnit.Case, async: true

setup do
{:ok, conn} = DBConnection.start_link(EctoLibSql, database: ":memory:")
{:ok, conn: conn}
end

test "INSERT RETURNING returns columns and rows", %{conn: conn} do
# Create table
{:ok, _, _} =
DBConnection.execute(
conn,
%EctoLibSql.Query{statement: "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)"},
[]
)

# Insert with RETURNING
query = %EctoLibSql.Query{statement: "INSERT INTO users (name, email) VALUES (?, ?) RETURNING id, name, email"}
{:ok, _, result} = DBConnection.execute(conn, query, ["Alice", "alice@example.com"])

IO.inspect(result, label: "INSERT RETURNING result")

# Check structure
assert result.columns != nil, "Columns should not be nil"
assert result.rows != nil, "Rows should not be nil"
assert length(result.columns) == 3, "Should have 3 columns"
assert length(result.rows) == 1, "Should have 1 row"

# Check values
[[id, name, email]] = result.rows
IO.puts("ID: #{inspect(id)}, Name: #{inspect(name)}, Email: #{inspect(email)}")

assert is_integer(id), "ID should be integer"
assert id > 0, "ID should be positive"
assert name == "Alice", "Name should match"
assert email == "alice@example.com", "Email should match"
end

test "INSERT RETURNING with timestamps", %{conn: conn} do
# Create table with timestamps
{:ok, _, _} =
DBConnection.execute(
conn,
%EctoLibSql.Query{
statement:
"CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT, inserted_at TEXT, updated_at TEXT)"
},
[]
)

# Insert with RETURNING
now = DateTime.utc_now() |> DateTime.to_iso8601()

query = %EctoLibSql.Query{
statement:
"INSERT INTO posts (title, inserted_at, updated_at) VALUES (?, ?, ?) RETURNING id, title, inserted_at, updated_at"
}

{:ok, _, result} = DBConnection.execute(conn, query, ["Test Post", now, now])

IO.inspect(result, label: "INSERT RETURNING with timestamps")

assert result.columns == ["id", "title", "inserted_at", "updated_at"]
[[id, title, inserted_at, updated_at]] = result.rows

IO.puts("ID: #{inspect(id)}")
IO.puts("Title: #{inspect(title)}")
IO.puts("inserted_at: #{inspect(inserted_at)}")
IO.puts("updated_at: #{inspect(updated_at)}")

assert is_integer(id)
assert title == "Test Post"
assert is_binary(inserted_at) or inserted_at == now
assert is_binary(updated_at) or updated_at == now
end
end
121 changes: 121 additions & 0 deletions test/type_compatibility_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
defmodule EctoLibSql.TypeCompatibilityTest do
use ExUnit.Case, async: false

defmodule TestRepo do
use Ecto.Repo, otp_app: :ecto_libsql, adapter: Ecto.Adapters.LibSql
end

defmodule Record do
use Ecto.Schema
import Ecto.Changeset

schema "records" do
field :bool_field, :boolean
field :int_field, :integer
field :float_field, :float
field :string_field, :string
field :map_field, :map
field :array_field, {:array, :string}
field :date_field, :date
field :time_field, :time
field :utc_datetime_field, :utc_datetime
field :naive_datetime_field, :naive_datetime

timestamps()
end

def changeset(record, attrs) do
record
|> cast(attrs, [
:bool_field, :int_field, :float_field, :string_field,
:map_field, :array_field, :date_field, :time_field,
:utc_datetime_field, :naive_datetime_field
])
end
end

@test_db "z_ecto_libsql_test-type_compat.db"

setup_all do
{:ok, _} = TestRepo.start_link(database: @test_db)

Ecto.Adapters.SQL.query!(TestRepo, """
CREATE TABLE IF NOT EXISTS records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
bool_field INTEGER,
int_field INTEGER,
float_field REAL,
string_field TEXT,
map_field TEXT,
array_field TEXT,
date_field TEXT,
time_field TEXT,
utc_datetime_field TEXT,
naive_datetime_field TEXT,
inserted_at DATETIME,
updated_at DATETIME
)
""")

on_exit(fn ->
EctoLibSql.TestHelpers.cleanup_db_files(@test_db)
end)

:ok
end

test "all field types round-trip correctly" do
now_utc = DateTime.utc_now()
now_naive = NaiveDateTime.utc_now()
today = Date.utc_today()
current_time = Time.new!(12, 30, 45)

attrs = %{
bool_field: true,
int_field: 42,
float_field: 3.14,
string_field: "test",
map_field: %{"key" => "value"},
array_field: ["a", "b", "c"],
date_field: today,
time_field: current_time,
utc_datetime_field: now_utc,
naive_datetime_field: now_naive
}

# Insert
changeset = Record.changeset(%Record{}, attrs)
{:ok, inserted} = TestRepo.insert(changeset)

IO.puts("\n=== Type Compatibility Test ===")
IO.inspect(inserted, label: "Inserted record")

# Verify inserted struct
assert inserted.id != nil
assert inserted.bool_field == true
assert inserted.int_field == 42
assert inserted.float_field == 3.14
assert inserted.string_field == "test"
assert inserted.map_field == %{"key" => "value"}
assert inserted.array_field == ["a", "b", "c"]
assert inserted.date_field == today
assert inserted.time_field == current_time

# Query back
queried = TestRepo.get(Record, inserted.id)
IO.inspect(queried, label: "Queried record")

# Verify queried struct - all types should match
assert queried.id == inserted.id
assert queried.bool_field == true, "Boolean should roundtrip"
assert queried.int_field == 42, "Integer should roundtrip"
assert queried.float_field == 3.14, "Float should roundtrip"
assert queried.string_field == "test", "String should roundtrip"
assert queried.map_field == %{"key" => "value"}, "Map should roundtrip"
assert queried.array_field == ["a", "b", "c"], "Array should roundtrip"
assert queried.date_field == today, "Date should roundtrip"
assert queried.time_field == current_time, "Time should roundtrip"
Comment thread
coderabbitai[bot] marked this conversation as resolved.

IO.puts("✅ PASS: All types round-trip correctly")
end
end
Loading