An OData v4 query parser and Ecto query builder for Elixir. Parse OData query strings or full URIs and get back Ecto queries ready to execute against your own repo. Designed for exposing internal Elixir services to BI tools like Power BI and Excel.
Add ex_odata4 to your dependencies in mix.exs:
def deps do
[
{:ex_odata4, "~> 0.2.2"}
]
endRegister your Ecto schemas in config/config.exs:
config :ex_odata4, schemas: %{
"Orders" => MyApp.Orders,
"Products" => MyApp.Products
}OData field names are automatically derived from your schema — Ecto's snake_case atoms become PascalCase OData names. So :first_name is exposed as FirstName, :amount as Amount, and so on. No additional mapping is required.
ExOdata4.parse_uri("/Orders?$filter=Amount gt 1000&$top=25&$skip=0")
|> MyApp.Repo.all()ExOdata4.get("Orders", "$filter=Status eq 'active'&$orderby=Amount desc&$top=10")
|> MyApp.Repo.all()Both functions return an %Ecto.Query{} ready to pipe into your repo.
For Power BI and Excel, serve the $metadata document from your router:
# In your Phoenix router or Plug
get "/$metadata", fn conn, _ ->
xml = ExOdata4.Metadata.generate(MyApp.Orders, namespace: "MyApp")
conn
|> put_resp_content_type("application/xml")
|> send_resp(200, xml)
endgenerate/2 accepts an optional :namespace (defaults to "Default").
| Option | Example |
|---|---|
$filter |
$filter=Name eq 'John' |
$top |
$top=25 |
$skip |
$skip=50 |
$orderby |
$orderby=Amount desc,Name asc |
$metadata |
GET /$metadata |
| Operator | Meaning |
|---|---|
eq |
Equal |
ne |
Not equal |
gt |
Greater than |
ge |
Greater than or equal |
lt |
Less than |
le |
Less than or equal |
and |
Logical and |
or |
Logical or |
| Function | Example | SQL |
|---|---|---|
contains |
contains(Name, 'Jo') |
LIKE '%Jo%' |
startswith |
startswith(Name, 'Jo') |
LIKE 'Jo%' |
endswith |
endswith(Email, '.com') |
LIKE '%.com' |
tolower |
tolower(Name) eq 'john' |
lower(name) = 'john' |
toupper |
toupper(Name) eq 'JOHN' |
upper(name) = 'JOHN' |
year |
year(Date) eq 2024 |
extract(year from date) = 2024 |
month |
month(Date) eq 1 |
extract(month from date) = 1 |
day |
day(Date) eq 15 |
extract(day from date) = 15 |
hour |
hour(Timestamp) gt 8 |
extract(hour from timestamp) > 8 |
Functions can be combined with logical operators:
$filter=contains(Name, 'John') and Amount gt 100
$filter=tolower(Status) eq 'active' or year(Date) gt 2023
- Strings:
'hello' - Integers:
42,-7 - Decimals:
3.14 - Booleans:
true,false - Null:
null - Dates:
2024-01-01 - DateTimeOffset:
2024-01-01T00:00:00Z - GUIDs:
00000000-0000-0000-0000-000000000000
$metadata automatically maps Ecto types to OData EDM types:
| Ecto type | EDM type |
|---|---|
:string |
Edm.String |
:integer |
Edm.Int32 |
:float |
Edm.Double |
:decimal |
Edm.Decimal |
:boolean |
Edm.Boolean |
:date |
Edm.Date |
:utc_datetime / :naive_datetime |
Edm.DateTimeOffset |
:binary_id |
Edm.Guid |
The goal is near-parity with the OData v4 spec. Features are shipped in order of practical value rather than spec completeness.
| Feature | Notes |
|---|---|
$select |
Field projection |
$count |
Total result count |
not |
Logical negation |
| Math functions | round(), floor(), ceiling() |
$expand |
Loading related entities — planned, takes time to get right |
| Navigation properties | e.g. Orders/Customer/Name eq 'John' — planned |
| Lambda operators | any(), all() |
Contributions are welcome. Open an issue or PR on GitHub.
MIT