From 5a08eaf742342b181a270db121495fbec5e02407 Mon Sep 17 00:00:00 2001 From: nadilas Date: Tue, 27 Jan 2026 20:17:49 +0100 Subject: [PATCH 1/2] fix: handle Ecto.SubQuery in IN expressions The catch-all IN clause handler was wrapping SubQuery expressions in JSON_EACH(), producing invalid SQL like: WHERE id IN (SELECT value FROM JSON_EACH(?)) This adds a specific pattern match for %Ecto.SubQuery{} before the catch-all, generating proper inline subqueries: WHERE id IN (SELECT s0.id FROM table AS s0 WHERE ...) Also adds a general SubQuery expression handler that properly resolves parent query aliases and combination queries. This fixes compatibility with libraries like Oban that use subqueries in UPDATE...WHERE id IN (subquery) patterns. --- lib/ecto/adapters/libsql/connection.ex | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/ecto/adapters/libsql/connection.ex b/lib/ecto/adapters/libsql/connection.ex index 1d296ee..52bd8db 100644 --- a/lib/ecto/adapters/libsql/connection.ex +++ b/lib/ecto/adapters/libsql/connection.ex @@ -1249,6 +1249,11 @@ defmodule Ecto.Adapters.LibSql.Connection do [expr(left, sources, query), " IN (", args, ?)] end + # IN with subquery - generate proper SQL subquery instead of JSON_EACH + defp expr({:in, _, [left, %Ecto.SubQuery{} = subquery]}, sources, query) do + [expr(left, sources, query), " IN ", expr(subquery, sources, query)] + 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 @@ -1317,6 +1322,18 @@ defmodule Ecto.Adapters.LibSql.Connection do expr(arg, sources, query) end + # SubQuery expression - generates inline SQL subquery + defp expr(%Ecto.SubQuery{query: query}, sources, parent_query) do + combinations = + Enum.map(query.combinations, fn {type, combination_query} -> + {type, put_in(combination_query.aliases[@parent_as], {parent_query, sources})} + end) + + query = put_in(query.combinations, combinations) + query = put_in(query.aliases[@parent_as], {parent_query, sources}) + [?(, all(query, subquery_as_prefix(sources)), ?)] + end + # Literal values (numbers, strings, etc.) defp expr(literal, _sources, _query) when is_number(literal) do to_string(literal) From 9ef48faa6ee228728498e70e2b4085326d00276d Mon Sep 17 00:00:00 2001 From: nadilas Date: Tue, 27 Jan 2026 20:56:13 +0100 Subject: [PATCH 2/2] fix: handle Ecto.Query.Tagged in expr for type-cast fragments Ecto's query planner transforms {:type, _, [expr, type]} AST nodes into %Ecto.Query.Tagged{} structs. The SQL generator only handled the pre-planning tuple form, causing type-wrapped fragments (e.g. type(fragment(...), :integer)) to fall through to the catch-all expr clause which rendered a single '?' placeholder. This caused parameter count mismatches with Hrana/Turso. Fixes Oban Web JobQuery.limit_query crash on completed jobs page. --- lib/ecto/adapters/libsql/connection.ex | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/ecto/adapters/libsql/connection.ex b/lib/ecto/adapters/libsql/connection.ex index 52bd8db..a7f222e 100644 --- a/lib/ecto/adapters/libsql/connection.ex +++ b/lib/ecto/adapters/libsql/connection.ex @@ -1317,11 +1317,16 @@ defmodule Ecto.Adapters.LibSql.Connection do quote_name(name) end - # Type casting + # Type casting (pre-planning AST form) defp expr({:type, _, [arg, _type]}, sources, query) do expr(arg, sources, query) end + # Type casting (post-planning Tagged struct form) + defp expr(%Ecto.Query.Tagged{value: value}, sources, query) do + expr(value, sources, query) + end + # SubQuery expression - generates inline SQL subquery defp expr(%Ecto.SubQuery{query: query}, sources, parent_query) do combinations =