ryanzidago

ryanzidago

Bylaw Ecto Query - validate ecto queries before running them

Hi all,

I’ve released bylaw_ecto_query, a small OSS library for validating prepared Ecto.Query structs before they run.

Why I wrote it

Imagine you ship an API endpoint that does not order the results determinisitcally. A customer sees inconsistent results, and need to be bothered enough to write support ticket. Now the issue has to be prioritised, investigated, fixed, reviewed, deployed, and communicated back, involving multiple stakeholders in the process.

You can test for this, but then you need to remember that deterministic ordering is the convention, remember to write a test for each relevant endpoint or query, and keep doing that consistently. With bylaw_ecto_query, you define that rule once as an invariant and have it checked wherever the relevant query shape appears.

The package makes application-specific query rules explicit and easy to enforce from c:Ecto.Repo.prepare_query/3. It is intended for teams that want checks around query safety, tenancy, visibility, ordering, joins, and similar conventions.

This is different from source-level linting tools like Credo or security-focused tools like Sobelow. Those inspect source code. bylaw_ecto_query validates the prepared %Ecto.Query{} struct at the repo boundary, which means it catches queries built dynamically at runtime that no source-level tool can see.

This is part of a small family of Bylaw packages I’m writing to help encode project conventions around application code, database schemas, and queries. The general idea is to make those conventions executable so they are easier for both humans and AI coding agents to follow consistently.

I’m still very much experimenting with the shape of these packages and how far I can push the boundaries of what can be validated. The checks aren’t meant to catch all cases, but help us steer the code in some desired direction.

Would especially appreciate feedback around:

  • how easy it is to set up
  • how useful the checks are
  • how many false positives or false negatives you encounter

Example setup

# config/dev.exs and config/test.exs
config :my_app, :bylaw, validate_ecto_queries?: true

# config/prod.exs
config :my_app, :bylaw, validate_ecto_queries?: false
defmodule MyApp.Repo do
  use Ecto.Repo,
    otp_app: :my_app,
    adapter: Ecto.Adapters.Postgres

  @query_checks [
    Bylaw.Ecto.Query.Checks.RequiredOrder,
    {Bylaw.Ecto.Query.Checks.MandatoryWhereKeys, keys: [:organization_id]}
  ]

  @impl Ecto.Repo
  def prepare_query(operation, query, opts) do
    if bylaw_ecto_query_enabled?() do
      validate_query!(operation, query)
    end

    {query, opts}
  end

  defp validate_query!(operation, query) do
    case Bylaw.Ecto.Query.validate(operation, query, @query_checks) do
      :ok -> :ok
      {:error, issues} -> raise Bylaw.Ecto.Query.Issue.format_many(issues)
    end
  end

  defp bylaw_ecto_query_enabled? do
    :my_app
    |> Application.get_env(:bylaw, [])
    |> Keyword.get(:validate_ecto_queries?, false)
  end
end

Included checks

Check Description
CartesianJoins Prevents explicit cartesian joins.
ConflictingWherePredicates Detects root where predicates that cannot all be satisfied.
DateDatetimeMixedComparisons Catches date fields compared to datetime fields without explicit truncation.
DeterministicOrder Requires ordered queries to include the root schema primary key.
DuplicateJoins Detects repeated equivalent joins.
EmptyInPredicates Catches root where predicates that rely on empty in lists.
ExplicitVisibilityPredicates Requires configured visibility-sensitive fields to be constrained explicitly.
HalfOpenTemporalIntervals Requires temporal interval predicates to use half-open ranges.
HardDeleteOnSoftDeleteSchema Prevents delete_all against schemas with soft-delete fields.
LeftJoinWherePredicates Catches root where predicates that null-reject left_join bindings.
MandatoryJoinKeys Requires explicit schema joins to preserve configured mandatory keys.
MandatoryWhereKeys Requires root where predicates to reference configured keys.
ManualJoinInsteadOfAssoc Encourages assoc/2 when a root association exists.
NamedBindings Requires query expressions to use named binding aliases.
OffsetWithoutLimit Prevents offset without limit.
RequiredOrder Requires order_by for query shapes that need stable row order.
UnboundedDeletes Requires delete_all queries to be bounded.
UnboundedUpdates Requires update_all queries to be bounded.
UtcDatetimeNaiveComparisons Catches UTC datetime fields compared with NaiveDateTime values.

Caveat

This package inspects prepared %Ecto.Query{} structs. Ecto exposes Ecto.Query.t(), but the internal shape of query expressions is not a stable extension API, so enabled checks may need to be reviewed when upgrading Ecto.

That tradeoff is acceptable for me for now. Partly because of Hyrum’s Law, and partly because I think the practical benefit can outweigh the maintenance cost.

Next steps

One thing I’m considering next is adding more configuration so checks can run only for certain schemas and/or tables.

Links

Install with:

def deps do
  [
    {:bylaw_ecto_query, "~> 0.1.0"}
  ]
end

HexDocs:

Hex:

GitHub:

First Post! Switch mode

binarytemple

binarytemple

Love it l, really interesting approach

— All posts loaded —

Where Next?

Trending in Announcing Top

bluzky
You may know https://ui.shadcn.com/, a UI component library for React. I really love it’s design style and components. I’ve built some co...
387 14960 120
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
shahryarjb
The Chelekom project is a library of Phoenix and LiveView components generated via Mix tasks to fit developer needs seamlessly. One of i...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
quatermain
Hello, I’m sharing my plugin here in forum after some time so it has time to mature and proof yourself. I use Claude Code daily on a pr...
New

Other Trending Topics Top

type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New
akoutmos
@hugobarauna and I (Alex Koutmos) have been hard at work on writing a book on Nerves that takes you from simply blinking LEDs to building...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
bjorng
We want to introduce a new native datatype to Erlang: native records. Although replacing all tuple records with native records is not our...
New
spammy
I’m looking to build a personal workflow to quickly deploy web applications written in elixir/phoenix, for local consumption (ie not on t...
New
alexslade
Fly’s CEO posted this recently - Turn And Face The Strange · The Fly Blog It says that Fly is going all-in on sprites, which is a worry ...
New

We're in Beta

About us Mission Statement