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.

Installation

Add ex_odata4 to your dependencies in mix.exs:

def deps do
  [
    {:ex_odata4, "~> 0.2.3"}
  ]
end

Configuration

Register 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.

Usage

Parsing a full OData URI

ExOdata4.parse_uri("/Orders?$filter=Amount gt 1000&$top=25&$skip=0")
|> MyApp.Repo.all()

Parsing a query string directly

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.

$metadata

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)
end

generate/2 accepts an optional :namespace (defaults to "Default").

Supported OData query options

OptionExample
$filter$filter=Name eq 'John'
$top$top=25
$skip$skip=50
$orderby$orderby=Amount desc,Name asc
$metadataGET /$metadata

Filter operators

OperatorMeaning
eqEqual
neNot equal
gtGreater than
geGreater than or equal
ltLess than
leLess than or equal
andLogical and
orLogical or

Filter functions

FunctionExampleSQL
containscontains(Name, 'Jo')LIKE '%Jo%'
startswithstartswith(Name, 'Jo')LIKE 'Jo%'
endswithendswith(Email, '.com')LIKE '%.com'
tolowertolower(Name) eq 'john'lower(name) = 'john'
touppertoupper(Name) eq 'JOHN'upper(name) = 'JOHN'
yearyear(Date) eq 2024extract(year from date) = 2024
monthmonth(Date) eq 1extract(month from date) = 1
dayday(Date) eq 15extract(day from date) = 15
hourhour(Timestamp) gt 8extract(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

Supported literal types

  • 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

EDM type mapping

$metadata automatically maps Ecto types to OData EDM types:

Ecto typeEDM type
:stringEdm.String
:integerEdm.Int32
:floatEdm.Double
:decimalEdm.Decimal
:booleanEdm.Boolean
:dateEdm.Date
:utc_datetime / :naive_datetimeEdm.DateTimeOffset
:binary_idEdm.Guid

Not yet supported

The goal is near-parity with the OData v4 spec. Features are shipped in order of practical value rather than spec completeness.

FeatureNotes
$selectField projection
$countTotal result count
notLogical negation
Math functionsround(), floor(), ceiling()
$expandLoading related entities — planned, takes time to get right
Navigation propertiese.g. Orders/Customer/Name eq 'John' — planned
Lambda operatorsany(), all()

Contributions are welcome. Open an issue or PR on GitHub.

License

MIT