Skip to content

feat: add native_in/2 function for SQL IN (...) syntax#728

Merged
zachdaniel merged 2 commits intoash-project:mainfrom
jkreddy020203:feat/native-in-function
Mar 27, 2026
Merged

feat: add native_in/2 function for SQL IN (...) syntax#728
zachdaniel merged 2 commits intoash-project:mainfrom
jkreddy020203:feat/native-in-function

Conversation

@jkreddy020203
Copy link
Copy Markdown
Contributor

Summary

  • Adds AshPostgres.Functions.NativeIn as an escape hatch for cases where PostgreSQL's query planner produces suboptimal plans with = ANY(...) array syntax
  • The native_in/2 function generates IN ($1, $2, ...) with individually typed parameters instead of = ANY($1::type[])
  • Values are properly typed based on the left-hand field's Ecto type (e.g., UUID fields encode correctly)

Usage

Post
|> Ash.Query.filter(native_in(id, [^id1, ^id2, ^id3]))
|> Ash.read!()

Generates:

WHERE id IN ($1, $2, $3)

Instead of:

WHERE id = ANY($1::uuid[])

Context

Closes ash-project/ash#2605

PostgreSQL's query planner may choose different (sometimes suboptimal) indexes when using = ANY('{...}'::type[]) compared to IN ($1, $2, ...). This was reported as a significant performance issue on PostgreSQL 14. As suggested by @zachdaniel, this provides a function-based escape hatch rather than changing the default behavior.

Changes

  • lib/functions/native_in.ex — Function definition ([:any, {:array, :any}] args)
  • lib/sql_implementation.ex — Expression handler that builds a SQL fragment with expanded parameters
  • lib/data_layer.ex — Register the function
  • test/native_in_test.exs — Tests for SQL generation and functional correctness

Test plan

  • Verifies generated SQL uses IN (...) not ANY(...)
  • Returns correct matching records
  • Returns empty list when no values match
  • Works with a single value
  • Full test suite passes with 0 regressions

Adds AshPostgres.Functions.NativeIn as an escape hatch for cases where
PostgreSQL's query planner produces suboptimal plans with = ANY(...)
array syntax. The native_in/2 function generates IN ($1, $2, ...) with
individually typed parameters instead.

Usage: filter(query, native_in(field, [^v1, ^v2, ^v3]))

Closes ash-project/ash#2605
@zachdaniel
Copy link
Copy Markdown
Contributor

WDYT about postgres_in? Makes it more clear what it maps to IMO.

Renames the function from native_in/2 to postgres_in/2 so it clearly
maps to PostgreSQL's IN syntax.
@zachdaniel zachdaniel merged commit 73bd66d into ash-project:main Mar 27, 2026
104 of 105 checks passed
@zachdaniel
Copy link
Copy Markdown
Contributor

🚀 Thank you for your contribution! 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

IN queries are automatically converted in ANY queries and break the query planner

2 participants