diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 77f11fd5..de6cc8e2 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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": [] diff --git a/CHANGELOG.md b/CHANGELOG.md index 19bc611a..106a1145 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/ecto/adapters/libsql/connection.ex b/lib/ecto/adapters/libsql/connection.ex index e72af65b..1d296eec 100644 --- a/lib/ecto/adapters/libsql/connection.ex +++ b/lib/ecto/adapters/libsql/connection.ex @@ -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)] diff --git a/test/issue_63_in_clause_test.exs b/test/issue_63_in_clause_test.exs index 1298dbe5..5651f826 100644 --- a/test/issue_63_in_clause_test.exs +++ b/test/issue_63_in_clause_test.exs @@ -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