solnic

solnic

Drops.Relation - High-Level Relation Abstraction on top of Ecto

Hey folks,

I’m back with a new library for y’all :slight_smile: Gonna x-post from my blog:


I’m excited to announce the latest addition to the Elixir Drops suite of libraries: Drops.Relation. This new library provides a high-level API for defining database relations with automatic schema inference and composable queries, simplifying database interactions when developing applications in Elixir.

Drops.Relation is based on 10 years of my work on the Ruby Object Mapper project and brings the most powerful features of ROM to Elixir.

What is Drops.Relation?

Drops.Relation bridges the gap between Ecto and application-level data handling and management. It automatically introspects your database tables, generates Ecto schemas, and provides a convenient query API that feels like working directly with Ecto.Repo while adding powerful composition features.

To put it simply it makes you develop applications faster and simplifies maintanance.

Think of it as a smart wrapper around Ecto that eliminates boilerplate while adding sophisticated query composition capabilities, while preserving your full control over your relations and their schemas.

Getting Started

Add it to your mix.exs:

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

Configure it in your config.exs:

config :my_app, :drops,
  relation: [
    repo: MyApp.Repo
  ]

Then run the installation task:

mix drops.relation.install

This should create aliases for ecto tasks that will ensure that schemas are refreshed whenever you run migrations.

To test it out, just run migrations:

mix ecto.migrate

If things go well, you will see this at the end of the output:

Cache refresh completed

From there, you can start defining your relations with inferred schemas :sparkles:

Automatic Schema Inference

One of Drops.Relation’s standout features is automatic schema inference. Instead of manually defining every field, type, and constraint, you can let the library introspect your database:

defmodule MyApp.Users do
  use Drops.Relation, otp_app: :my_app

  schema("users", infer: true)
end

The library automatically discovers:

  • Column names and types
  • Primary and foreign keys
  • Indexes and constraints
  • Default values and nullability

You can access the generated schema programmatically:

schema = MyApp.Users.schema()
schema[:email]
# %Drops.Relation.Schema.Field{
#   name: :email,
#   type: :string,
#   source: :email,
#   meta: %{
#     default: nil,
#     index: true,
#     type: :string,
#     primary_key: false,
#     foreign_key: false,
#     nullable: false
#   }
# }

Familiar Repository API

Drops.Relation provides all the familiar Ecto.Repo functions you’re used to but makes them more accessible:

# Reading data
user = Users.get(1)
user = Users.get_by(email: "john@example.com")
users = Users.all()
users = Users.all_by(active: true)

# Writing data
{:ok, user} = Users.insert(%{name: "John", email: "john@example.com"})
{:ok, user} = Users.update(user, %{name: "Jane"})
{:ok, user} = Users.delete(user)

# Aggregations
count = Users.count()
avg_age = Users.aggregate(:avg, :age)

Composable Queries

Where Drops.Relation really shines is in query composition. You can chain query operations together to build complex queries:

# Basic composition
active_users = Users
|> Users.restrict(active: true)
|> Users.order(:name)
|> Enum.to_list()

# Complex restrictions with multiple conditions
admins = Users
|> Users.restrict(role: ["admin", "super_admin"])
|> Users.restrict(active: true)
|> Users.order([{:last_login, :desc}, :name])

# Works seamlessly with Enum functions
user_names = Users
|> Users.restrict(active: true)
|> Enum.map(& &1.name)

In the first release Drops.Relation provides 3 high-level query operations:

  • Drops.Relation.restrict - filters data based on conditions
  • Drops.Relation.order - sets ordering
  • Drops.Relation.preload - preloads associations

More operations will be added in future releases.

Custom Queries with defquery

For more complex scenarios, you can define reusable query functions using the defquery macro:

defmodule MyApp.Users do
  use Drops.Relation, otp_app: :my_app

  schema("users", infer: true)

  defquery active() do
    from(u in relation(), where: u.active == true)
  end

  defquery by_role(role) when is_binary(role) do
    from(u in relation(), where: u.role == ^role)
  end

  defquery by_role(roles) when is_list(roles) do
    from(u in relation(), where: u.role in ^roles)
  end

  defquery recent(days \\ 7) do
    cutoff = DateTime.utc_now() |> DateTime.add(-days, :day)
    from(u in relation(), where: u.inserted_at >= ^cutoff)
  end

  defquery with_posts() do
    from(u in relation(),
         join: p in assoc(u, :posts),
         distinct: u.id)
  end
end

These custom queries are fully composable with built-in operations:

# Compose custom queries
recent_admins = Users
|> Users.active()
|> Users.by_role("admin")
|> Users.recent(30)
|> Users.order(:name)
|> Enum.to_list()

# Mix with restrict operations
active_users_with_email = Users
|> Users.active()
|> Users.restrict(email: {:not, nil})
|> Users.order(:email)

Advanced Feature: Boolean Query Logic

For complex query logic involving multiple conditions, Drops.Relation provides a powerful query macro with boolean operations:

import Drops.Relation.Query

# Simple AND operation
adult_active_users = Users
|> query([u], u.active() and u.adult())
|> Enum.to_list()

# Complex nested conditions
complex_query = Users
|> query([u],
    (u.active() and u.adult()) or
    (u.inactive() and u.with_email())
  )
|> Users.order(:name)
|> Enum.to_list()

# Mix built-in and custom operations
filtered_users = Users
|> query([u],
    u.active() and
    u.restrict(role: ["admin", "user"])
  )
|> Enum.to_list()

What’s Next?

Future releases will include:

  • Enhanced association handling
  • Type-safe high-level query handling
  • More composable query operations
  • Integration with other Elixir Drops libraries
  • Generators for Phoenix

Try It!

Drops.Relation is available on Hex.pm and the source code is on GitHub.

The library is currently in its early stages, testing and feedback are welcome!

Give it a try and let me know what you think! I’m always interested in feedback and contributions from the community :purple_heart:


Where Next?

Popular in Libraries Top

tmbb
I’ve decided to create this topic to discuss optimization possibilities for something like Phoenix LiveView. I’ve created this topic unde...
New
devonestes
Introducing assertions, the library that helps you write really great test assertions! GitHub: https://github.com/devonestes/assertions ...
New
entone
ExARI An Elixir library for interfacing with Asterisk using the ARI protocol. Be sure to check out the example application https://git...
New
woylie
I released Doggo, a collection of unstyled Phoenix components. Features Unstyled Phoenix components. Storybook that can be added to...
New
treble37
Just looking for a little feedback on a tiny helper library I built - Sometimes I find the need to convert maps with atom keys to maps...
New
kelvinst
There was a feature proposal recently on Elixir core mailing list where an idea born. I got very interested in the idea, but it is not go...
New
martinthenth
Hello everybody :wave: Recently, some of my colleagues talked about database ids and uuids and their problems, and I remembered the pain...
New
woylie
Flop is an Elixir library that applies filtering, ordering and pagination parameters to your Ecto queries. offset-based pagination with...
New
OvermindDL1
I created a new library (rather I pulled out a couple files from my big project), it manages an operating system PID file for the BEAM. ...
New
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...
381 12391 119
New

Other popular topics Top

belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
beno
I will often find my self writing things similar to: case some_value do nil -> something() "" -> something() _ -> someth...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

Sub Categories:

We're in Beta

About us Mission Statement