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:


https://github.com/solnic/drops_relation

Where Next?

Popular in Announcing Top

mspanc
I am pleased to announce an initial release of the Membrane Framework - an Elixir-based framework with special focus on processing multim...
New
jakub-zawislak
Hi everyone, I’m coming from the Symfony (PHP) framework. I like Phoenix, but it has a one thing that was build much better in the Symfo...
New
gabrielpoca
Hello everyone! I want to share with you something that I’m really proud of: https://stillstatic.io/ Still is a static site builder for...
New
Crowdhailer
The latest release of Ace (0.10.0) includes serving content over HTTP/2. I have started writing a webserver to teach my self more about...
New
Eiji
ExApi is a library that I’m developing now and hope release soon This library will allow to: list all apis list all api implementation...
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 52673 488
New
Crowdhailer
Experimenting with this code. OK.try do user <- fetch_user(1) cart <- fetch_cart(1) order = checkout(cart, user) save_orde...
New
sbs
Only 650 LOC, wrote for fun :slight_smile: https://github.com/sunboshan/qrcode
New
aditya7iyengar
Rummage.Ecto and Rummage.Phoenix provide ways to perform Searching, Sorting and Pagination over Ecto queries and Phoenix collections. Fo...
New
hpopp
After just over two years in development, this latest version of Pigeon is what I finally consider done in regards to my original vision ...
New

Other popular topics Top

sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 29603 241
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36352 110
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 48342 226
New
AstonJ
Seen any cool LiveView demos, sample apps or examples? Please post them here! :003:
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement