An open API service indexing awesome lists of open source software.

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

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.