Skip to content

Commit 241db3a

Browse files
authored
Merge pull request #64 from ocean/fix/in-clause-json-encoding
Fix issue #63: IN clause datatype mismatch with JSON-encoded lists
2 parents 638a7ee + fc23cde commit 241db3a

3 files changed

Lines changed: 142 additions & 25 deletions

File tree

lib/ecto_libsql/query.ex

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -90,24 +90,12 @@ defmodule EctoLibSql.Query do
9090
end
9191
end
9292

93-
# List/Array encoding: lists are encoded to JSON arrays
94-
# Lists must contain only JSON-serializable values (strings, numbers, booleans,
95-
# nil, lists, and maps). This enables array parameter support in raw SQL queries.
96-
defp encode_param(value) when is_list(value) do
97-
case Jason.encode(value) do
98-
{:ok, json} ->
99-
json
100-
101-
{:error, %Jason.EncodeError{message: msg}} ->
102-
raise ArgumentError,
103-
message:
104-
"Cannot encode list parameter to JSON. List contains non-JSON-serializable value. " <>
105-
"Lists can only contain strings, numbers, booleans, nil, lists, and maps. " <>
106-
"Reason: #{msg}. List: #{inspect(value)}"
107-
end
108-
end
109-
11093
# Pass through all other values unchanged
94+
# Note: Lists are not automatically JSON-encoded here.
95+
# - For Ecto queries with IN clauses: Ecto's query builder expands lists into individual parameters
96+
# - For array fields in schemas: Ecto dumpers handle JSON encoding via array_encode/1
97+
# - For raw SQL with arrays: Users should pre-encode lists using Jason.encode!
98+
# This design allows IN clauses to work correctly while still supporting array fields.
11199
defp encode_param(value), do: value
112100

113101
# Pass through results from Native.ex unchanged.

test/issue_63_in_clause_test.exs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
defmodule EctoLibSql.Issue63InClauseTest do
2+
@moduledoc """
3+
Test case for issue #63: Datatype mismatch due to JSON encoding of lists in IN statements.
4+
5+
The issue occurs when lists are used as parameters in IN clauses.
6+
Instead of expanding the list into individual parameters, the entire list
7+
was being JSON-encoded as a single string parameter, causing SQLite to raise
8+
a "datatype mismatch" error.
9+
"""
10+
11+
use EctoLibSql.Integration.Case, async: false
12+
13+
alias EctoLibSql.Integration.TestRepo
14+
alias EctoLibSql.Schemas.Product
15+
16+
import Ecto.Query
17+
18+
@test_db "z_ecto_libsql_test-issue_63.db"
19+
20+
setup_all do
21+
Application.put_env(:ecto_libsql, EctoLibSql.Integration.TestRepo,
22+
adapter: Ecto.Adapters.LibSql,
23+
database: @test_db
24+
)
25+
26+
{:ok, _} = EctoLibSql.Integration.TestRepo.start_link()
27+
28+
# Create test table with state column
29+
Ecto.Adapters.SQL.query!(TestRepo, """
30+
CREATE TABLE IF NOT EXISTS test_items (
31+
id INTEGER PRIMARY KEY AUTOINCREMENT,
32+
state TEXT,
33+
name TEXT,
34+
inserted_at TEXT,
35+
updated_at TEXT
36+
)
37+
""")
38+
39+
on_exit(fn ->
40+
EctoLibSql.TestHelpers.cleanup_db_files(@test_db)
41+
end)
42+
43+
:ok
44+
end
45+
46+
setup do
47+
# Clear table before each test
48+
Ecto.Adapters.SQL.query!(TestRepo, "DELETE FROM test_items", [])
49+
:ok
50+
end
51+
52+
test "IN clause with list parameter should not JSON-encode the list" do
53+
# Insert test data with various states
54+
Ecto.Adapters.SQL.query!(TestRepo, """
55+
INSERT INTO test_items (state, name, inserted_at, updated_at)
56+
VALUES ('scheduled', 'item1', datetime('now'), datetime('now')),
57+
('retryable', 'item2', datetime('now'), datetime('now')),
58+
('completed', 'item3', datetime('now'), datetime('now')),
59+
('failed', 'item4', datetime('now'), datetime('now'))
60+
""")
61+
62+
# This query should work without datatype mismatch error
63+
# Using a list parameter in an IN clause
64+
states = ["scheduled", "retryable"]
65+
66+
query =
67+
from(t in "test_items",
68+
where: t.state in ^states,
69+
select: t.name
70+
)
71+
72+
# Execute the query - this should not raise "datatype mismatch" error
73+
result = TestRepo.all(query)
74+
75+
# Should return the two items with scheduled or retryable state
76+
assert length(result) == 2
77+
assert "item1" in result
78+
assert "item2" in result
79+
end
80+
81+
test "IN clause with multiple parameter lists should work correctly" do
82+
# Insert test data
83+
Ecto.Adapters.SQL.query!(TestRepo, """
84+
INSERT INTO test_items (state, name, inserted_at, updated_at)
85+
VALUES ('active', 'item1', datetime('now'), datetime('now')),
86+
('inactive', 'item2', datetime('now'), datetime('now')),
87+
('pending', 'item3', datetime('now'), datetime('now'))
88+
""")
89+
90+
# Query with multiple filters including IN clause
91+
states = ["active", "pending"]
92+
93+
query =
94+
from(t in "test_items",
95+
where: t.state in ^states,
96+
select: t.name
97+
)
98+
99+
result = TestRepo.all(query)
100+
101+
assert length(result) == 2
102+
assert "item1" in result
103+
assert "item3" in result
104+
end
105+
106+
test "IN clause with empty list parameter" do
107+
# Insert test data
108+
Ecto.Adapters.SQL.query!(TestRepo, """
109+
INSERT INTO test_items (state, name, inserted_at, updated_at)
110+
VALUES ('test', 'item1', datetime('now'), datetime('now'))
111+
""")
112+
113+
# Query with empty list should return no results
114+
query =
115+
from(t in "test_items",
116+
where: t.state in ^[],
117+
select: t.name
118+
)
119+
120+
result = TestRepo.all(query)
121+
122+
# Empty IN clause should match nothing
123+
assert result == []
124+
end
125+
end

test/type_loader_dumper_test.exs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -650,19 +650,20 @@ defmodule EctoLibSql.TypeLoaderDumperTest do
650650
describe "array types" do
651651
test "array fields load and dump as JSON arrays" do
652652
array = ["a", "b", "c"]
653+
json_array = Jason.encode!(array)
653654

654655
{:ok, _} =
655656
Ecto.Adapters.SQL.query(
656657
TestRepo,
657658
"INSERT INTO all_types (array_field) VALUES (?)",
658-
[array]
659+
[json_array]
659660
)
660661

661662
{:ok, result} = Ecto.Adapters.SQL.query(TestRepo, "SELECT array_field FROM all_types")
662663

663664
# Should be stored as JSON array string
664-
assert [[json_string]] = result.rows
665-
assert {:ok, decoded} = Jason.decode(json_string)
665+
assert [[^json_array]] = result.rows
666+
assert {:ok, decoded} = Jason.decode(json_array)
666667
assert decoded == ["a", "b", "c"]
667668
end
668669

@@ -682,16 +683,18 @@ defmodule EctoLibSql.TypeLoaderDumperTest do
682683
end
683684

684685
test "handles empty arrays" do
686+
empty_json = Jason.encode!([])
687+
685688
{:ok, _} =
686689
Ecto.Adapters.SQL.query(
687690
TestRepo,
688691
"INSERT INTO all_types (array_field) VALUES (?)",
689-
[[]]
692+
[empty_json]
690693
)
691694

692695
{:ok, result} = Ecto.Adapters.SQL.query(TestRepo, "SELECT array_field FROM all_types")
693696

694-
assert [["[]"]] = result.rows
697+
assert [[^empty_json]] = result.rows
695698
end
696699

697700
test "empty string defaults to empty array" do
@@ -762,6 +765,7 @@ defmodule EctoLibSql.TypeLoaderDumperTest do
762765
}
763766

764767
# Insert via raw SQL
768+
# Note: arrays and maps must be pre-encoded to JSON when using raw SQL
765769
{:ok, _} =
766770
Ecto.Adapters.SQL.query(
767771
TestRepo,
@@ -789,9 +793,9 @@ defmodule EctoLibSql.TypeLoaderDumperTest do
789793
attrs.naive_datetime_usec_field,
790794
attrs.utc_datetime_field,
791795
attrs.utc_datetime_usec_field,
792-
attrs.map_field,
793-
attrs.json_field,
794-
attrs.array_field
796+
Jason.encode!(attrs.map_field),
797+
Jason.encode!(attrs.json_field),
798+
Jason.encode!(attrs.array_field)
795799
]
796800
)
797801

0 commit comments

Comments
 (0)