Skip to content
Merged
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
4 changes: 3 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@
"Bash(gh pr diff:*)",
"Bash(gh pr checks:*)",
"Bash(gh run view:*)",
"Bash(gh pr checkout:*)"
"Bash(gh pr checkout:*)",
"mcp__acp__Bash",
"mcp__acp__Edit"
],
"deny": [],
"ask": []
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed

- **IN Clause with Ecto.Query.Tagged Structs** - Fixed issue #63 where `~w()` sigil word lists in IN clauses returned zero results due to Tagged struct wrapping. Now properly extracts list values from `Ecto.Query.Tagged` structs before generating IN clauses, enabling these patterns to work correctly.

## [0.8.8] - 2026-01-23

### Fixed
Expand Down
21 changes: 21 additions & 0 deletions lib/ecto/adapters/libsql/connection.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1249,6 +1249,27 @@ defmodule Ecto.Adapters.LibSql.Connection do
[expr(left, sources, query), " IN (", args, ?)]
end

# Catch-all for IN with non-list right side (e.g. Ecto.Query.Tagged from ~w() sigil, JSON-encoded arrays)
# This handles cases where the right side has been pre-processed or wrapped by Ecto
defp expr({:in, _, [left, right]}, sources, query) do
case right do
%Ecto.Query.Tagged{value: val} when is_list(val) ->
# Extract list from Tagged struct and generate proper IN clause
args = Enum.map_intersperse(val, ?,, &expr(&1, sources, query))
[expr(left, sources, query), " IN (", args, ?)]

_ ->
# Default fallback: use JSON_EACH to handle JSON-encoded arrays or other complex types
[
expr(left, sources, query),
" IN (SELECT value FROM JSON_EACH(",
expr(right, sources, query),
?),
?)
]
end
end

# LIKE
defp expr({:like, _, [left, right]}, sources, query) do
[expr(left, sources, query), " LIKE ", expr(right, sources, query)]
Expand Down
28 changes: 28 additions & 0 deletions test/issue_63_in_clause_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,32 @@ defmodule EctoLibSql.Issue63InClauseTest do
# Empty IN clause should match nothing
assert result == []
end

test "IN clause with literal word list (~w) - e.g. oban" do
# test data matching Oban's state values
Ecto.Adapters.SQL.query!(TestRepo, """
INSERT INTO test_items (state, name, inserted_at, updated_at)
VALUES ('scheduled', 'job1', datetime('now'), datetime('now')),
('retryable', 'job2', datetime('now'), datetime('now')),
('available', 'job3', datetime('now'), datetime('now')),
('completed', 'job4', datetime('now'), datetime('now'))
""")

# This is how Oban's Lite engine queries jobs - using ~w() sigil
# ~w() creates a compile-time list that Ecto wraps in %Ecto.Query.Tagged{}
# This was causing "datatype mismatch" because the Tagged struct wasn't
# being handled by the IN clause expression generator
query =
from(t in "test_items",
where: t.state in ~w(scheduled retryable),
select: t.name
)

# should not raise "datatype mismatch" error
result = TestRepo.all(query)

assert length(result) == 2
assert "job1" in result
assert "job2" in result
end
end