https://github.com/jechol/ash_sql_sort_breaks_distinct_repro
ash_sql sort breaks distinct repro
https://github.com/jechol/ash_sql_sort_breaks_distinct_repro
Last synced: 8 months ago
JSON representation
ash_sql sort breaks distinct repro
- Host: GitHub
- URL: https://github.com/jechol/ash_sql_sort_breaks_distinct_repro
- Owner: jechol
- Created: 2025-09-08T05:03:17.000Z (9 months ago)
- Default Branch: main
- Last Pushed: 2025-09-08T05:03:35.000Z (9 months ago)
- Last Synced: 2025-09-23T19:44:00.049Z (9 months ago)
- Language: Elixir
- Size: 14.6 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# ash_sql sort breaks distinct repro
## Problem Overview
**Issue**: When a related resource in a `has_many` relationship has `prepare build(sort: [:id])` defined, DISTINCT doesn't work properly, causing duplicate records to be returned.
## Bug Details
### Data Structure
- `Customer` has_many `:purchased_products` (through Product)
- `Product` has_many `:orders` + **`prepare build(sort: [:id])`** ← Root cause
- `Order` belongs_to `:customer`, `:product` (acts as join table)
### Expected vs Actual Behavior
- **Expected**: Even if a Customer orders the same Product multiple times, `purchased_products` should return that Product only once (DISTINCT applied)
- **Actual**: When Product has sorting, the same Product is returned multiple times
## Root Cause Analysis
### Generated SQL Query
```sql
SELECT DISTINCT
c0."id",
s1."id",
s1."__order__"
FROM
"public"."customers" AS c0
INNER JOIN LATERAL (
SELECT
sp0."id" AS "id",
row_number() OVER "order" AS "__order__"
FROM
"public"."products" AS sp0
INNER JOIN "public"."orders" AS so1 ON sp0."id" = so1."product_id"
WHERE
(so1."customer_id"::UUID = c0."id"::UUID::UUID)
WINDOW
"order" AS (
ORDER BY
sp0."id"
)
ORDER BY
sp0."id"
) AS s1 ON TRUE
WHERE
(c0."id"::UUID = ANY ('{f463c739-5c1b-43d7-8470-be43c36f65de}'::UUID []))
ORDER BY
s1."__order__"
```
### Issues Identified
1. **DISTINCT only applied to top-level query**: `SELECT DISTINCT` is only in the outer query
2. **Duplicates occur inside LATERAL JOIN**: Product's `prepare build(sort: [:id])` generates `ORDER BY` and `row_number()`, but when there are multiple Orders for the same Product, duplicate records are created inside the LATERAL JOIN
3. **DISTINCT is ineffective**: The outer DISTINCT considers `s1."__order__"` as well, so even for the same Product, different `__order__` values prevent duplicate removal
## Bug Reproduction
### Failing Test Due to Bug
```bash
MIX_ENV=test mix ecto.reset
mix test
```
The test `"purchased_products"` in `customer_test.exs` **fails** because of this bug.
### Test Scenario That Demonstrates the Bug
1. Create 1 Customer and 1 Product
2. Create 2 Orders for the same Customer and Product
3. Execute `customer |> Ash.load!([:purchased_products])`
4. **Expected**: 1 Product in `purchased_products`
5. **Actual**: 2 Products returned (duplicates occur)
### Evidence of the Problem
The issue can be confirmed by removing the following from `lib/my_domain/product.ex`:
```elixir
preparations do
prepare build(sort: [:id]) # ← This line causes the bug
end
```
When this sorting preparation is removed, the test passes and DISTINCT works correctly. This proves that the sorting preparation is the root cause of the bug.