From fa776d81c49a00610dca23864fbf375dd36f4ec2 Mon Sep 17 00:00:00 2001 From: Jesse Stimpson Date: Sun, 7 Apr 2024 09:22:59 -0400 Subject: [PATCH] ExDoc --- README.md | 123 +----- lib/ecto/adapters/foundationdb.ex | 404 ++++++++++++------ lib/ecto/adapters/foundationdb/database.ex | 4 +- .../adapters/foundationdb/ecto_adapter.ex | 4 +- .../foundationdb/ecto_adapter_queryable.ex | 4 +- .../foundationdb/ecto_adapter_schema.ex | 4 +- .../foundationdb/ecto_adapter_storage.ex | 4 +- .../foundationdb/ecto_adapter_transaction.ex | 4 +- lib/ecto/adapters/foundationdb/layer.ex | 130 +----- .../adapters/foundationdb/layer/fields.ex | 6 +- .../foundationdb/layer/index_inventory.ex | 4 +- .../foundationdb/layer/indexer/max_value.ex | 14 +- .../adapters/foundationdb/layer/ordering.ex | 6 +- lib/ecto/adapters/foundationdb/layer/pack.ex | 17 +- lib/ecto/adapters/foundationdb/layer/query.ex | 4 +- lib/ecto/adapters/foundationdb/layer/tx.ex | 4 +- lib/ecto/adapters/foundationdb/migration.ex | 4 +- lib/ecto/adapters/foundationdb/migrator.ex | 7 +- lib/ecto/adapters/foundationdb/options.ex | 2 +- lib/ecto/adapters/foundationdb/query_plan.ex | 6 +- lib/ecto/adapters/foundationdb/schema.ex | 4 +- lib/ecto/adapters/foundationdb/supervisor.ex | 5 +- mix.exs | 22 +- mix.lock | 6 + 24 files changed, 372 insertions(+), 420 deletions(-) diff --git a/README.md b/README.md index 0364cdf..f641362 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ [![CI](https://github.com/foundationdb-beam/ecto_foundationdb/actions/workflows/ci.yml/badge.svg)](https://github.com/foundationdb-beam/ecto_foundationdb/actions/workflows/ci.yml) -**Work in progress.** - ## Driver An Ecto Adapter for FoundationDB, written using [foundationdb-beam/erlfdb](https://github.com/foundationdb-beam/erlfdb) @@ -11,7 +9,18 @@ as the driver for communicating with FoundationDB. ## Installation -`ecto_foundationdb` is still under development; it's not ready for any production workloads, but it is ready for use in dev and test environments. +Install the latest stable release of FoundationDB from the +[official FoundationDB Releases](https://github.com/apple/foundationdb/releases). + +You will only need to install the `foundationdb-server` package if you're +running an instance of the FoundationDB Server. For example, it's common to +run the `foundationdb-server` on your development machine and on managed +instances running a FoundationDB cluster, but not for your stateless Elixir +application server in production. + +`foundationdb-clients` is always required. + +Include `:ecto_foundationdb` in your list of dependencies in `mix.exs`: ```elixir defp deps do @@ -23,95 +32,18 @@ end ## Usage -Define your repo similar to this. - -```elixir -defmodule MyApp.Repo do - use Ecto.Repo, otp_app: :my_app, adapter: Ecto.Adapters.FoundationDB -end -``` - -Configure your repo similar to the following. - -```elixir -config :my_app, - ecto_repos: [MyApp.Repo] - -config :my_app, MyApp.Repo, - cluster_file: "/etc/foundationdb/fdb.cluster", - migrator: MyApp.Migrator -``` +See the [documentation](https://hexdocs.pm/ecto_foundationdb) for usage +information. ### Tenants -`ecto_foundationdb` requires the use of FoundationDB Tenants, which can be enabled on your cluster -with the following configuration in an `fdbcli` prompt. +`ecto_foundationdb` requires the use of FoundationDB Tenants, which can be +enabled on your cluster with the following configuration in an `fdbcli` prompt. ``` fdb> configure tenant_mode=optional_experimental ``` -Creating a schema to be used in a tenant. - -```elixir -defmodule User do - use Ecto.Schema - @schema_context usetenant: true - # ... -end -``` - -Setting up a tenant. - -```elixir -alias Ecto.Adapters.FoundationDB -alias Ecto.Adapters.FoundationDB.Tenant - -tenant = Tenant.open!(MyApp.Repo, "some-org") -``` - -Each call on your `Repo` must specify a tenant via the Ecto `prefix` option. - -Inserting a new struct. - -```elixir -user = %User{name: "John"} - |> FoundationDB.usetenant(tenant) - -MyApp.Repo.insert!(user) -``` - -**Note**: Because we are using the "some-org" tenant, John is -considered a member of "some-org" without having to specify the relationship -in the User schema itself. - -Querying for a struct using the primary key. - -```elixir -MyApp.Repo.get!(User, user.id, prefix: tenant) -``` - -Failure to specify a tenant will result in a raised exception at runtime. - -## Layer - -Because FoundationDB is key-value store, support for typical data access patterns must be -implemented as a [Layer](https://apple.github.io/foundationdb/layer-concept.html) that sits on top -of the underlying data store. - -The layer implemented in `ecto_foundationdb` shares some features with the official -[Record Layer](https://github.com/FoundationDB/fdb-record-layer); however, we have not yet -endeavored to implement it. A mechanism to introduce pluggable Layers into this adapter is not on the roadmap. - -The `ecto_foundationdb` Layer supports the following data access patterns. - -- **Set/get/delete**: Write and read a Struct based on a unique primary key. -- **Get all**: Retrieval of all Structs in a given tenant. -- **Simple index**: Retrieval of all Structs in a tenant that match a particular field value. The index must be created ahead of time using a migration. -- **Timeseries**: Retrieval of all Structs in a tenant that include a timestamp in between a given timespan. The timeseries index must be created ahead of time using a migration. - -Please see the full layer documentation at [Ecto.Adapaters.FoundationDB.Layer](lib/ecto/adapters/foundationdb/layer.ex) - ## Running tests To run the integration tests, use the following. @@ -119,26 +51,3 @@ To run the integration tests, use the following. ```sh mix test ``` - -## Features - -Roughly in order of priority. - -- [x] Sandbox -- [x] Basic crud operations -- [x] Single index -- [x] Initial documentation -- [x] Time series index (auto gen pk, optionally skip primary write) -- [x] `TestRepo.stream` -- [x] Layer documentation -- [x] Layer isolation and docs -- [x] Migration tooling (handling many tenants) -- [x] Migration locking -- [x] Pluggable Indexers -- [x] Benchmarking -- [x] Index caching -- [x] Mapped Range Indexes -- [x] Hierarchical multi index -- [x] Ecto Transactions -- [ ] FDB Watches -- [ ] Logging for all Adapter behaviours diff --git a/lib/ecto/adapters/foundationdb.ex b/lib/ecto/adapters/foundationdb.ex index 0b9570a..180c7d3 100644 --- a/lib/ecto/adapters/foundationdb.ex +++ b/lib/ecto/adapters/foundationdb.ex @@ -4,6 +4,29 @@ defmodule Ecto.Adapters.FoundationDB do It uses `:erlfdb` for communicating to the database. + ## Installation + + Install the latest stable release of FoundationDB from the + [official FoundationDB Releases](https://github.com/apple/foundationdb/releases). + + You will only need to install the `foundationdb-server` package if you're + running an instance of the FoundationDB Server. For example, it's common to + run the `foundationdb-server` on your development machine and on managed + instances running a FoundationDB cluster, but not for your stateless Elixir + application server in production. + + `foundationdb-clients` is always required. + + Include `:ecto_foundationdb` in your list of dependencies in `mix.exs`: + + ```elixir + defp deps do + [ + {:ecto_foundationdb, git: "https://github.com/foundationdb-beam/ecto_foundationdb.git", branch: "main"} + ] + end + ``` + ## Standard Options * `:cluster_file` - The path to the fdb.cluster file. The default is @@ -24,26 +47,164 @@ defmodule Ecto.Adapters.FoundationDB do * `:migration_step` - The maximum number of keys to process in a single transaction when running migrations. Defaults to `1000`. If you use a number that is too large, the FDB transactions run by the Migrator will fail. + * `:idx_cache` - When set to `:enabled`, the Ecto ets cache is used to store the + available indexes per tenant. This speeds up all database operations. + Defaults to `:enabled`. - ## Limitations and caveats + ## Usage - There are some limitations when using Ecto with FoundationDB. + Define these modules in your `MyApp` application: - ### Tenants + ```elixir + defmodule MyApp.Repo do + use Ecto.Repo, otp_app: :my_app, adapter: Ecto.Adapters.FoundationDB + end + + defmodule MyApp.Migrator do + @behaviour Ecto.Adapters.FoundationDB.Migrator + @impl true + def migrations(), do: [] + end + ``` + + Edit your `config.exs` file to include the following: + + ```elixir + config :my_app, + ecto_repos: [MyApp.Repo] + + config :my_app, MyApp.Repo, + cluster_file: "/etc/foundationdb/fdb.cluster", + migrator: MyApp.Migrator + ``` + + ## Tenants + + `:ecto_foundationdb` requires the use of [FoundationDB Tenants](https://apple.github.io/foundationdb/tenants.html). + + Each tenant you create has a separate keyspace from all others, and a given FoundationDB + Transaction is guaranteed to be isolated to a particular tenant's keyspace. + + You'll use the Ecto `:prefix` option to specify the relevant tenant for each Ecto operation + in your application. + + When a struct is retrieved from a tenant (using `:prefix`), that's struct's metadata + holds onto the tenant reference. This helps to protect your application from a + struct accidentally crossing tenant boundaries due to some unforeseen bug. + + Creating a schema to be used in a tenant. + + ```elixir + defmodule User do + use Ecto.Schema + @schema_context usetenant: true + @primary_key {:id, :binary_id, autogenerate: true} + schema "users" do + field(:name, :string) + field(:department, :string) + timestamps() + end + # ... + end + ``` + + Setting up a tenant. + + ```elixir + alias Ecto.Adapters.FoundationDB.Tenant + + tenant = Tenant.open!(MyApp.Repo, "some-org") + ``` + + Inserting a new struct. + + ```elixir + user = %User{name: "John", department: "Engineering"} + |> FoundationDB.usetenant(tenant) + + user = MyApp.Repo.insert!(user) + ``` + + Querying for a struct using the primary key. + + ```elixir + MyApp.Repo.get!(User, user.id, prefix: tenant) + ``` + + ## Migrations + + At first glance, `:ecto_foundationdb` migrations may look similar to that of `:ecto_sql`, + but the actual execution of migrations and how your app must be configured are very + different, so please read this section in full. + + If your app uses indexes on any of your schemas, you must define a `:migrator` + option on your repo that is a module implementing the `Ecto.Adapters.FoundationDB.Migrator` + behaviour. + + Migrations are only used for index management. There are no tables to add, drop, or modify. + When you add a field to your Schema, any read requests on old objects will return `nil` in + that field. + + As tenants are opened during your application runtime, migrations will be executed + automatically. This distributes the migration across a potentially long period of time, + as migrations will not be executed unless the tenant is opened. + + The `:migrator` is a module in your application runtime that provides the full list of + ordered migrations. These are the migrations that will be executed when a tenant is opened. + If you leave out a migration from the list, it will not be applied. + + The module `MyApp.Migrator` will change over time to include new migrations, like so: + + ```elixir + defmodule MyApp.Migrator do + @behaviour Ecto.Adapters.FoundationDB.Migrator + + @impl true + def migrations() do + [ + {0, MyApp.IndexUserByDepartment} + # At a later date, we my add a new index with a corresponding addition + # tho the migrations list: + # {1, MyApp.IndexUserByRole} + ] + end + end + ``` + + Each migration is contained in a separate module, much like EctoSQL's. However, the operations + to be carried out **must be returned as a list.** For example, the creation an index + may look like this: + + ```elixir + defmodule MyApp.IndexUserByDepartment do + use Ecto.Adapters.FoundationDB.Migration + def change() do + [ + create index(User, [:department]) + ] + end + end + ``` + + Your `MyApp.Migrator` and `MyApp.` modules are part of your codebase. They must be + included in your application release. - As discussed in the README, we require the use of tenants. When a struct is retrieved - from a tenant (using `:prefix`), that's struct's metadata holds onto the tenant - reference. This helps to protect your application from a struct accidentally - crossing tenant boundaries due to some unforeseen bug. + Migrations can be completed in full at any time with a call to + `Ecto.Adapters.FoundationDB.Migrator.up_all/1`. Depending on how many tenants you have in + your database, and the size of the data for each tenant, this operation might take a long + time and make heavy use of system resources. It is safe to interrupt this operation, as + migrations are performed transactionally. However, if you interrupt a migration, the next + attempt for that tenant may have a brief delay (~5 sec); the migrator must ensure that the + previous migration has indeed stopped executing. - ### Data Types + ## Data Types - `ecto_foundationdb` stores your struct's data using `:erlang.term_to_binary/1`, and + `:ecto_foundationdb` stores your struct's data using `:erlang.term_to_binary/1`, and retrieves it with `:erlang.binary_to_term/1`. As such, there is no data type conversion between Elixir types and Database Types. Any term you put in your struct will be stored and later retrieved. - Data types are used by `ecto_foundationdb` for the creation and querying of indexes. + Data types are used by `:ecto_foundationdb` for the creation and querying of indexes. Certain Ecto types are encoded into the FDB key, which allows you to formulate Between queries on indexes of these types: @@ -64,76 +225,85 @@ defmodule Ecto.Adapters.FoundationDB do See Queries for more information. - ### Key and Value Size + ## Indexes - FoundationDB imposes strict limits on the size of keys and the size of values. Please - be aware of these limitations as you develop. `ecto_foundationdb` doesn't make - any explicit attempt to protect you from these errors. + The implication of the [FoundationDB Layer Concept](https://apple.github.io/foundationdb/layer-concept.html) + is that the manner in which data can be stored and accessed is the responsibility of the client + application. To achieve some general purpose data storage and access patterns commonly desired + in web applications, `:ecto_foundationdb` provides some support for indexes out of the box, and + also allows your application to define custom indexes. - ### Layer + When you define a migration that calls `create index(User, [:department])`, a `Default` index is + created on the `:department` field. The following two indexes are equivalent: - `ecto_foundationdb` implements a specific Layer on the FoundationDB - key-value store. This Layer is intended to be generally useful, but you - may not find it suitable for your workloads. The Layer is documented in - detail at `Ecto.Adapters.FoundationDB.Layer`. This project does not support - plugging in other Layers. + ```elixir + # ... + create index(User, [:department]) + # is equivalent to + create index(User, [:department], options: [indexer: Ecto.Adapters.FoundationDB.Layer.Indexer.Default]))] + # ... + ``` - ### Queries + A Default index on several fields is supported: - The Layer implemenation affords us a limited set of query types. Any queries - beyond these will raise an exception. If you require more complex queries, - we suggest that you first extract all the data that you need using a supported - query and then constrain, aggregate, and group as needed with Elixir functions. + ```elixir + create index(User, [:inserted_at]) + create index(User, [:birthyear, :department, :birthdate]) + ``` - In other words, you will be writing less SQL and more Elixir. + The index value can be any Elixir term. All types support Equal queries. However, certain Ecto Schema types + support Between queries. When an index is created on timestasmp-like fields, it is + an effective time series index. See the query examples below. - * `Repo.get` using primary key - * `Repo.all` using no constraint. This will return all such structs for - the tenant. - * `Repo.all` using an Equal constraint on an index field. This will return - matching structs for the tenant. + ### Index Queries - ```elixir - from(u in User, where: u.name == ^"John") - ``` - * `Repo.all` using a Between constraint on a compatible index field. This will - return structs in the tenant that have a timestamp between the given timespan - in the query. + Retrieve all users in the Engineering department (with an Equal constraint): - ```elixir - from(e in Event, - where: - e.timestamp > ^~N[1970-01-01 00:00:00] and - e.timestamp < ^~N[2100-01-01 00:00:00] - ) - ``` + ```elixir + iex> query = from(u in User, where: u.department == ^"Engineering") + iex> Repo.all(query, prefix: tenant) + ``` + + Retrieve all users with a birthyear from 1992 to 1994 (with a Between constraint): - ### Indexes + ```elixir + iex> query = from(u in User, + ...> where: u.birthyear >= ^1992 and u.birthyear <= ^1994 + ...> ) + iex> Repo.all(query, prefix: tenant) + ``` - Simple indexes and time series indexes are supported out of the box. They are - similar to Ecto SQL indexes in some ways, but critically different in others. + Retrieve all Engineers born in August 1992: - The out of the box indexes are called Default indexes, corresponding to the module - by the same name. + ```elixir + iex> query = from(u in User, + ...> where: u.birthyear == ^1992 and + ...> u.department == ^"Engineering" and + ...> u.birthdate >= ^~D[1992-08-01] and u.birthdate < ^~D[1992-09-01] + ...> ) + iex> Repo.all(query, prefix: tenant) + ``` - 1. An index is created via a migration file, as it is with Ecto SQL. However, - this is the only supported purpose of migration files so far. And this is - where the similarities with Ecto SQL end. + **Order matters!**: When you create an index using multiple fields, the FDB key that stores the index will be extended with + all the values in the order of your defined index fields. Because FoundationDB stores keys in a well-defined order, + the order of the fields in your index determines the Between queries you can perform. - 2. Indexes are managed within transactions, so that they will always be - consistent. + There can be 0 or 1 Between clauses, and if one exists, it must correspond to the final constraint in the where clause when + compared against the order of the index fields. - 3. Upon index creation, each tenant's data will be indexed in a stream of FDB - transactions. This stream maintains transactional isolation for each tenant - as they migrate. See `ProgressiveJob` for more. + ### Custom Indexes - 4. Migrations must be executed on a per tenant basis, and they can be - run in parallel. Migrations are managed automatically by this adapater. + As data is inserted, updated, deleted, and queried, the Indexer callbacks (via behaviour `Ecto.Adapters.FoundationDB.Indexer`) + are executed. Your Indexer module does all the work necessary to maintain the index within the transaction + that the data itself is being manipulated. Many different types of indexes are possible. You are in control! - ### Transactions + For an example, please see the [MaxValue implementation](https://github.com/foundationdb-beam/ecto_foundationdb/blob/main/lib/ecto/adapters/foundationdb/layer/indexer/max_value.ex) + which is used internally by `:ecto_foundatiomdb` to keep track of the maximum value of a field in a Schema. - `ecto_foundationdb` exposes the Ecto.Repo transaction function, and it also exposes - a FoundationDB-specific transaction. + ## Transactions + + The Repo functions will automatically use FoundationDB transactions behind the scenes. `:ecto_foundationdb` also + exposes the Ecto.Repo transaction function, allowing you to group operations together with transactional isolation: ```elixir TestRepo.transaction(fn -> @@ -141,6 +311,8 @@ defmodule Ecto.Adapters.FoundationDB do end, prefix: tenant) ``` + It also exposes a FoundationDB-specific transaction: + ```elixir FoundationDB.transactional(tenant, fn tx -> @@ -148,12 +320,12 @@ defmodule Ecto.Adapters.FoundationDB do end) ``` - Both of these calling convetions create a transaction on FDB. Typically you will use + Both of these calling conventions create a transaction on FDB. Typically you will use `Repo.transaction/1` when operating on your Ecto.Schema structs. If you wish - to do anything using the lower-level `:erlfdb` API, you will use + to do anything using the lower-level `:erlfdb` API (rare), you will use `FoundationDB.transactional/2`. - Please visit the (FoundationDB Developer Guid on transactions)[https://apple.github.io/foundationdb/developer-guide.html#transaction-basics] + Please read the [FoundationDB Developer Guid on transactions](https://apple.github.io/foundationdb/developer-guide.html#transaction-basics) for more information about how to develop with transactions. It's important to remember that even though the database gives ACID guarantees about the @@ -171,7 +343,9 @@ defmodule Ecto.Adapters.FoundationDB do ... Phoenix.PubSub.broadcast("my_topic", "new_user") # Not safe! :( end, prefix: tenant) + ``` + ```elixir # Instead, do this: TestRepo.transaction(fn -> TestRepo.insert(%User{name: "John"}) @@ -180,96 +354,66 @@ defmodule Ecto.Adapters.FoundationDB do Phoenix.PubSub.broadcast("my_topic", "new_user") # Safe :) ``` - ### Migrations - - At first glance, `:ecto_foundationdb` migrations may look similar to that of `:ecto_sql`, - but the actual execution of migrations and how your app must be configured are very - different, so please read this section in full. + ## Limitations and Caveats - If your app uses indexes on any of your schemas, you must define a `:migrator` - option on your repo that is a module implementing the `Ecto.Adapters.FoundationDB.Migrator` - behaviour. + ### FoundationDB - Migrations are only used for index management. There are no tables to add, drop, or modify. - When you add a field to your Schema, any read requests on old objects will return `nil` in - that field. Right now there is no way to rename a field. + Please read [FoundationDB Known Limitations](https://apple.github.io/foundationdb/known-limitations.html). - As tenants are opened during your application runtime, migrations will be executed - automatically. This distributes the migration across a potentially long period of time, - as migrations will not be executed unless the tenant is opened. - - Migrations can be completed in full with a call to - `Ecto.Adapters.FoundationDB.Migrator.up_all/1`. Depending on how many tenants you have in - your database, and the size of the data for each tenant, this operation might take a long - time and make heavy use of system resources. It is safe to interrupt this operation, as - migrations are performed transactionally. However, if you interrupt a migration, the next - attempt for that tenant may have a brief delay (~5 sec); the migrator must ensure that the - previous migration has indeed stopped executing. - - The `:migrator` is a module in your application runtime that provides the full list of - ordered migrations. These are the migrations that will be executed when a tenant is opened. - If you leave out a migration from the list, it will not be applied. - - For example, your migrator might look like this: + ### Layer - ```elixir - defmodule MyApp.Migrator do - @behaviour Ecto.Adapters.FoundationDB.Migrator + `ecto_foundationdb` implements a specific Layer on the FoundationDB + key-value store. This Layer is intended to be generally useful, but you + may not find it suitable for your workloads. The Layer is documented in + detail at `Ecto.Adapters.FoundationDB.Layer`. This project does not support + plugging in other Layers. - @impl true - def migrations(MyApp.Repo) do - [ - {0, MyApp.AMigrationForIndexCreation}, - {1, MyApp.AnIndexWeAddedLaterOn} - ] - end - end - ``` + ### Tenants - As each tenant is opened at runtime, it will advance version-by-version in - FDB transactions until it reaches the latest version. + * The use of a tenant implies an ownership relationship between the tenant and the data. + It's up to you how you use this relationship in your application. - Each migration is contained in a separate module, much like EctoSQL's. However, the operations - to be carried out **must be returned as a list.** For example, the creation of 2 indexes - may look like this: + * Failure to specify a tenant will result in a raised exception at runtime. - ```elixir - defmodule MyApp.AMigrationForIndexCreation do - use Ecto.Adapters.FoundationDB.Migration - def change() do - [ - create(index(User, [:name]), - create(index(Post, [:user_id])) - ] - end - end - ``` + ### Migrations - Note: The following are yet to be implemented. + The following are not supported: - 1. Dropping an index - 2. Moving down in migration versions (i.e. rollback) + * Renaming a table + * Renaming a field + * Dropping an index + * Moving down in migration bersions (rollback) - Finally, the Mix tasks regarding ecto migrations are not supported. + Finally, the Ecto Mix tasks are known to be unsupported: ```elixir # These commands are not supported. Do not use them with :ecto_foundationdb! + # mix ecto.create + # mix ecto.drop # mix ecto.migrate # mix ecto.gen.migration # mix ecto.rollback # mix ecto.migrations ``` + ### Key and Value Size + + [FoundationDB has limits on key and value size](https://apple.github.io/foundationdb/known-limitations.html#large-keys-and-values) + Please be aware of these limitations as you develop. `:ecto_foundationdb` doesn't make + any attempt to protect you from these errors. + ### Other Ecto Features Many of Ecto's features probably do not work with `ecto_foundationdb`. Please see the integration tests for a collection of use cases that is known to work. - Certainly, you'll find that most queries fail with an Ecto.Adapaters.FoundationDB.Exception.Unsupported - exception. The FoundationDB Layer concept precludes complex queries from being executed - within the database. Therefore, it only makes sense to implement a limited set of query types -- - specifically those that do have optimized database query semantics. All other filtering, aggregation, - and grouping must be done by your Elixir code. + Certainly, you'll find that most queries outside of those detailed in the documentation fail with + an `Ecto.Adapaters.FoundationDB.Exception.Unsupported` exception. The FoundationDB Layer concept + precludes complex queries from being executed within the database. Therefore, it only makes sense + to implement a limited set of query types -- specifically those that do have optimized database + query semantics. All other filtering, aggregation, and grouping must be done by your Elixir code. + + In other words, you'll be writing less SQL, and more Elixir. """ @behaviour Ecto.Adapter diff --git a/lib/ecto/adapters/foundationdb/database.ex b/lib/ecto/adapters/foundationdb/database.ex index 1e0807e..37e1015 100644 --- a/lib/ecto/adapters/foundationdb/database.ex +++ b/lib/ecto/adapters/foundationdb/database.ex @@ -1,4 +1,6 @@ defmodule Ecto.Adapters.FoundationDB.Database do - @moduledoc false + @moduledoc """ + See `Ecto.Adapters.FoundationDB`. + """ @type t() :: :erlfdb.database() end diff --git a/lib/ecto/adapters/foundationdb/ecto_adapter.ex b/lib/ecto/adapters/foundationdb/ecto_adapter.ex index c2b8923..175a87e 100644 --- a/lib/ecto/adapters/foundationdb/ecto_adapter.ex +++ b/lib/ecto/adapters/foundationdb/ecto_adapter.ex @@ -1,7 +1,5 @@ defmodule Ecto.Adapters.FoundationDB.EctoAdapter do - @moduledoc """ - Implemenation of Ecto.Adapter - """ + @moduledoc false @behaviour Ecto.Adapter @impl Ecto.Adapter diff --git a/lib/ecto/adapters/foundationdb/ecto_adapter_queryable.ex b/lib/ecto/adapters/foundationdb/ecto_adapter_queryable.ex index d0db7cb..d8c42f9 100644 --- a/lib/ecto/adapters/foundationdb/ecto_adapter_queryable.ex +++ b/lib/ecto/adapters/foundationdb/ecto_adapter_queryable.ex @@ -1,7 +1,5 @@ defmodule Ecto.Adapters.FoundationDB.EctoAdapterQueryable do - @moduledoc """ - Implemenation of Ecto.Adapter.Queryable - """ + @moduledoc false @behaviour Ecto.Adapter.Queryable alias Ecto.Adapters.FoundationDB.Exception.IncorrectTenancy diff --git a/lib/ecto/adapters/foundationdb/ecto_adapter_schema.ex b/lib/ecto/adapters/foundationdb/ecto_adapter_schema.ex index 67f63ac..14c4887 100644 --- a/lib/ecto/adapters/foundationdb/ecto_adapter_schema.ex +++ b/lib/ecto/adapters/foundationdb/ecto_adapter_schema.ex @@ -1,7 +1,5 @@ defmodule Ecto.Adapters.FoundationDB.EctoAdapterSchema do - @moduledoc """ - Implemenation of Ecto.Adapter.Schema - """ + @moduledoc false @behaviour Ecto.Adapter.Schema alias Ecto.Adapters.FoundationDB.Exception.IncorrectTenancy diff --git a/lib/ecto/adapters/foundationdb/ecto_adapter_storage.ex b/lib/ecto/adapters/foundationdb/ecto_adapter_storage.ex index 7d0919f..ed2b3d0 100644 --- a/lib/ecto/adapters/foundationdb/ecto_adapter_storage.ex +++ b/lib/ecto/adapters/foundationdb/ecto_adapter_storage.ex @@ -1,7 +1,5 @@ defmodule Ecto.Adapters.FoundationDB.EctoAdapterStorage do - @moduledoc """ - Implemenation of Ecto.Adapter.Storage - """ + @moduledoc false @behaviour Ecto.Adapter.Storage alias Ecto.Adapters.FoundationDB.Layer.Pack diff --git a/lib/ecto/adapters/foundationdb/ecto_adapter_transaction.ex b/lib/ecto/adapters/foundationdb/ecto_adapter_transaction.ex index a84096c..037801e 100644 --- a/lib/ecto/adapters/foundationdb/ecto_adapter_transaction.ex +++ b/lib/ecto/adapters/foundationdb/ecto_adapter_transaction.ex @@ -1,7 +1,5 @@ defmodule Ecto.Adapters.FoundationDB.EctoAdapterTransaction do - @moduledoc """ - Implemenation of Ecto.Adapter.Transaction - """ + @moduledoc false alias Ecto.Adapters.FoundationDB alias Ecto.Adapters.FoundationDB.Layer.Tx @behaviour Ecto.Adapter.Transaction diff --git a/lib/ecto/adapters/foundationdb/layer.ex b/lib/ecto/adapters/foundationdb/layer.ex index 2e7a24c..541c6bd 100644 --- a/lib/ecto/adapters/foundationdb/layer.ex +++ b/lib/ecto/adapters/foundationdb/layer.ex @@ -6,6 +6,20 @@ defmodule Ecto.Adapters.FoundationDB.Layer do such as Postgres, these patterns will be familiar. However, there are many differences (for example SQL is not supported), so this document seeks to describe the capabilities of the Ecto FoundationDB Layer in detail. + ## Keyspace Design + + All keys used by `:ecto_foundationdb` are encoded with [FoundationDB's Tuple Encoding](https://github.com/apple/foundationdb/blob/main/design/tuple.md) + The first element of the tuple is a string prefix that is intender to keep the `:ecto_foundationdb` keyspace + separate from other keys in the FoundationDB cluster. + + Your Schema data is stored with prefix "\\xFD". + + The data associated with schema migrations is stored with prefix "\\xFE". + + All values are either + * other keys (in the case of Default indexes) or + * Erlang term data encoded with `:erlang.term_to_binary/1` + ## Primary Write and Read Your Ecto Schema has a primary key field, which is usually a string or an integer. This primary @@ -28,94 +42,20 @@ defmodule Ecto.Adapters.FoundationDB.Layer do would be the organization the User belongs to. Since the User is in this tenant, we do not need to provide an identifier for this organization on the User object itself. - The User can be inserted and retrieved: - - iex> tenant = Tenant.open!(Repo, "an-org-id") - iex> user = Repo.insert!(%User{name: "John", department: "Engineering"}, prefix: tenant) - iex> Repo.get!(User, user.id, prefix: tenant) + "Primary Write" refers to the insertion of the User struct into the FoundationDB key-value store + under a single key that uniquely identifies the User. This key includes the `:id` value. - Within a tenant, all objects from your Schema can be retrieved at once. - - iex> Repo.all(User, prefix: tenant) - [%User{id: "some-uuid", name: "John", department: "Engineering"}] + Your struct data is stored as a `Keyword` encoded with `:erlang.term_to_binary/1`. Note: The Primary Write can be skipped by providing the `write_primary: false` option on the `@schema_context`. See below for more. - ## Index Write and Read - - As shown above, you can easily get all Users in a tenant. However, say you wanted to - get all Users from a certain department with high efficiency. - - Via an Ecto Migration, you can specify a Default index on the `:department` field. - - ```elixir - defmodule MyApp.Migration do - use Ecto.Adapters.FoundationDB.Migration - def change() do - [create(index(User, [:department]))] - end - end - ``` + ## Default Indexes - When this index is created via the migration, the Ecto FoundationDB Adapter writes a set of + When a Default index is created via a migration, the Ecto FoundationDB Adapter writes a set of keys and values to facilitate lookups based on the indexed field. - iex> query = from(u in User, where: u.department == ^"Engineering") - iex> Repo.all(query, prefix: tenant) - - The index value can be any Elixir term. All types support Equal queries. However, certain Ecto Schema types - support Between queries. For example, you can define an index on a naive_datetime_usec field to construct - an effective time series index. - - iex> query = from(e in Event, - ...> where: e.timestamp >= ^~N[2024-01-01 00:00:00] and e.timestamp < ^~N[2024-01-01 12:00:00] - ...> ) - iex> Repo.all(query, prefix: tenant) - - ### Multiple index fields - - **Order matters!**: When you create an index using multiple fields, the FDB key that stores the index will be extended with - all the values in the order of your defined index fields. For example, you may want to have a time series index, but also - have the ability to easily drop all events on a certain date. You can achieve this by creating an index on `[:date, :user_id, :time]`. - The order of the fields determines the Between queries you can perform. - - ```elixir - defmodule MyApp.Migration do - use Ecto.Adapters.FoundationDB.Migration - def change() do - [create(index(Event, [:date, :user_id, :time]))] - end - end - ```` - - With this index, the following queries will be efficient: - - ```elixir - iex> query = from(e in Event, - ...> where: e.date == ^~D[2024-01-01] and - e.user_id == ^"user-id" and - e.time >= ^~T[12:00:00] and e.time < ^~T[13:00:00] - ...> ) - - iex> query = from(e in Event, - ...> where: e.date >= ^~D[2024-01-01] and e.date < ^~D[2024-01-02] - ...> ) - ``` - - However, this query will raise a Runtime exception: - - ```elixir - iex> query = from(e in Event, - ...> where: e.date >= ^~D[2024-01-01] and e.date < ^~D[2024-01-02] - e.user_id == ^"user-id" - ...> ) - ``` - - There can be 0 or 1 Between clauses, and if one exists, it must correspond to the final constraint in the where clause when - compared against the order of the index fields. - - ### Advanced Options: `write_primary: false`, `mapped?: false` + ## Advanced Options: `write_primary: false`, `mapped?: false` If you choose to use `write_primary: false` on your schema, this skips the Primary Write. The consequence of this are as follows: @@ -126,35 +66,5 @@ defmodule Ecto.Adapters.FoundationDB.Layer do 3. The data can **only** be managed by providing a query on the index. You will not be able to access the data via the primary key. 4. If `write_primary: false`, then only one index can be created. - - ## User-defined Indexes - - The Ecto FoundationDB Adapter also supports user-defined indexes. These indexes are created and managed - by your application code. This is useful when you have a specific query pattern that is not covered by - the Default index or Time Series index. Internally, MaxValue is an example of a user-defined index that - the adapter uses to manage the schema_migrations and index caching. - - To create a user-defined index, you must define a module that implements the Indexer behaviour. - - Please see Ecto.Adapters.FoundationDB.Indexer for more information, and Ecto.Adapters.FoundationDB.Indexer.MaxValue - for an example implementation. - - ```elixir - - ## Transactions - - The Primary and Index writes are guaranteed to be consistent due to FoundationDB's - [ACID Transactions](https://apple.github.io/foundationdb/developer-guide.html#transaction-basics). - - As the application developer, you can also take advantage of Transactions to implement your own - data access semantics. For example, if you wanted to make sure that when a user is inserted, an - event is recorded of the operation, it can be done via a Transaction. - - iex> fun = fn -> - ...> Repo.insert!(%User{name: "John"}) - ...> Repo.insert!(%Event{timestamp: ~N[2024-02-18 12:34:56], data: "Welcome John"}) - ...> end - iex> Repo.transaction(fun, prefix: tenant) - """ end diff --git a/lib/ecto/adapters/foundationdb/layer/fields.ex b/lib/ecto/adapters/foundationdb/layer/fields.ex index ae0b39f..2c2aedb 100644 --- a/lib/ecto/adapters/foundationdb/layer/fields.ex +++ b/lib/ecto/adapters/foundationdb/layer/fields.ex @@ -1,9 +1,5 @@ defmodule Ecto.Adapters.FoundationDB.Layer.Fields do - @moduledoc """ - Some functions to assist with managing the fields in an object as it is written to FDB storage. - - The object is stored as a Keyword. - """ + @moduledoc false @doc """ Ecto provides a compiled set of 'select's. We simply pull the field names out diff --git a/lib/ecto/adapters/foundationdb/layer/index_inventory.ex b/lib/ecto/adapters/foundationdb/layer/index_inventory.ex index 8a1ee79..21ed8d9 100644 --- a/lib/ecto/adapters/foundationdb/layer/index_inventory.ex +++ b/lib/ecto/adapters/foundationdb/layer/index_inventory.ex @@ -1,7 +1,5 @@ defmodule Ecto.Adapters.FoundationDB.Layer.IndexInventory do - @moduledoc """ - This is an internal module that manages index creation and metadata. - """ + @moduledoc false alias Ecto.Adapters.FoundationDB.Layer.Indexer.Default alias Ecto.Adapters.FoundationDB.Layer.Indexer.MaxValue alias Ecto.Adapters.FoundationDB.Layer.Pack diff --git a/lib/ecto/adapters/foundationdb/layer/indexer/max_value.ex b/lib/ecto/adapters/foundationdb/layer/indexer/max_value.ex index d528369..a051404 100644 --- a/lib/ecto/adapters/foundationdb/layer/indexer/max_value.ex +++ b/lib/ecto/adapters/foundationdb/layer/indexer/max_value.ex @@ -1,14 +1,12 @@ defmodule Ecto.Adapters.FoundationDB.Layer.Indexer.MaxValue do - @moduledoc """ - From a specified field on a schema, stores the max value. + @moduledoc false + # From a specified field on a schema, stores the max value. - This has optimal performance for values where the max is increasing. - If the max decreases, then the full schema must be scanned to compute - the new max. + # This index assumes: + # * the field value is an unsigned integer + # * the max is monotonically non-decreasing - It is also written to assume that any value is an unsigned integer. A - value of -1 is returned if there are no values. - """ + # A value of -1 is returned if there are no values. alias Ecto.Adapters.FoundationDB.Exception.Unsupported alias Ecto.Adapters.FoundationDB.Layer.Indexer alias Ecto.Adapters.FoundationDB.Layer.Pack diff --git a/lib/ecto/adapters/foundationdb/layer/ordering.ex b/lib/ecto/adapters/foundationdb/layer/ordering.ex index ce62b8c..d6760ce 100644 --- a/lib/ecto/adapters/foundationdb/layer/ordering.ex +++ b/lib/ecto/adapters/foundationdb/layer/ordering.ex @@ -1,7 +1,7 @@ defmodule Ecto.Adapters.FoundationDB.Layer.Ordering do - @moduledoc """ - This module emulates `query.order_bys` behavior, because FoundationDB doesn't have native support for result ordering. - """ + @moduledoc false + # This module emulates `query.order_bys` behavior, because FoundationDB + # doesn't have native support for result ordering. @doc """ Returns the ordering function that needs to be applied on a query result. diff --git a/lib/ecto/adapters/foundationdb/layer/pack.ex b/lib/ecto/adapters/foundationdb/layer/pack.ex index 5a20744..5b5e30a 100644 --- a/lib/ecto/adapters/foundationdb/layer/pack.ex +++ b/lib/ecto/adapters/foundationdb/layer/pack.ex @@ -1,16 +1,15 @@ defmodule Ecto.Adapters.FoundationDB.Layer.Pack do - @moduledoc """ - This internal module creates binaries to be used as FDB keys and values. + @moduledoc false + # This internal module creates binaries to be used as FDB keys and values. - Primary writes are stored in - {@adapter_prefix, source, @data_namespace, id} + # Primary writes are stored in + # {@adapter_prefix, source, @data_namespace, id} - Default indexes are stored in - {@adapter_prefix, source, @index_namespace, index_name, length(index_values), [...index_values...], id} + # Default indexes are stored in + # {@adapter_prefix, source, @index_namespace, index_name, length(index_values), [...index_values...], id} - Schema migrations are stored as primary writes and default indexes with - {@migration_prefix, source, ...} - """ + # Schema migrations are stored as primary writes and default indexes with + # {@migration_prefix, source, ...} @adapter_prefix <<0xFD>> @migration_prefix <<0xFE>> diff --git a/lib/ecto/adapters/foundationdb/layer/query.ex b/lib/ecto/adapters/foundationdb/layer/query.ex index 7f1dc6b..d063a90 100644 --- a/lib/ecto/adapters/foundationdb/layer/query.ex +++ b/lib/ecto/adapters/foundationdb/layer/query.ex @@ -1,7 +1,5 @@ defmodule Ecto.Adapters.FoundationDB.Layer.Query do - @moduledoc """ - This internal module handles execution of Ecto Query requests. - """ + @moduledoc false alias Ecto.Adapters.FoundationDB.Exception.Unsupported alias Ecto.Adapters.FoundationDB.Layer.Fields alias Ecto.Adapters.FoundationDB.Layer.Indexer diff --git a/lib/ecto/adapters/foundationdb/layer/tx.ex b/lib/ecto/adapters/foundationdb/layer/tx.ex index c19904d..78ed10c 100644 --- a/lib/ecto/adapters/foundationdb/layer/tx.ex +++ b/lib/ecto/adapters/foundationdb/layer/tx.ex @@ -1,7 +1,5 @@ defmodule Ecto.Adapters.FoundationDB.Layer.Tx do - @moduledoc """ - This internal module handles the execution of `:erlfdb` operations within transactions. - """ + @moduledoc false alias Ecto.Adapters.FoundationDB.Exception.IncorrectTenancy alias Ecto.Adapters.FoundationDB.Exception.Unsupported alias Ecto.Adapters.FoundationDB.Layer.Fields diff --git a/lib/ecto/adapters/foundationdb/migration.ex b/lib/ecto/adapters/foundationdb/migration.ex index 103b512..ca9c4cf 100644 --- a/lib/ecto/adapters/foundationdb/migration.ex +++ b/lib/ecto/adapters/foundationdb/migration.ex @@ -1,7 +1,5 @@ defmodule Ecto.Adapters.FoundationDB.Migration do - @moduledoc """ - Specifies the adapter migrations API. - """ + @moduledoc false alias Ecto.Adapters.FoundationDB.Migration.Index alias Ecto.Adapters.FoundationDB.Schema diff --git a/lib/ecto/adapters/foundationdb/migrator.ex b/lib/ecto/adapters/foundationdb/migrator.ex index ccfc4b4..bbd0f32 100644 --- a/lib/ecto/adapters/foundationdb/migrator.ex +++ b/lib/ecto/adapters/foundationdb/migrator.ex @@ -1,8 +1,6 @@ defmodule Ecto.Adapters.FoundationDB.Migrator do @moduledoc """ - Ecto FoundationDB is configured by default to manage database migrations - triggered by actions taken on Tenants (See `Tenant.open/2`). This module contains - the operations to manage those migrations. + Implement this behaviour to define migrations for `Ecto.Adapters.FoundationDB` """ require Logger @@ -14,6 +12,9 @@ defmodule Ecto.Adapters.FoundationDB.Migrator do alias Ecto.Adapters.FoundationDB.Options alias Ecto.Adapters.FoundationDB.Tenant + @doc """ + + """ @spec up_all(Ecto.Repo.t(), Options.t()) :: :ok def up_all(repo, options \\ []) do options = Keyword.merge(repo.config(), options) diff --git a/lib/ecto/adapters/foundationdb/options.ex b/lib/ecto/adapters/foundationdb/options.ex index c8b8ffe..8493b62 100644 --- a/lib/ecto/adapters/foundationdb/options.ex +++ b/lib/ecto/adapters/foundationdb/options.ex @@ -1,6 +1,6 @@ defmodule Ecto.Adapters.FoundationDB.Options do @moduledoc """ - This internal module handles the options that are available to the application developer. + Options for `Ecto.Adapters.FoundationDB`. """ @type option() :: diff --git a/lib/ecto/adapters/foundationdb/query_plan.ex b/lib/ecto/adapters/foundationdb/query_plan.ex index 6f4224b..2c653f7 100644 --- a/lib/ecto/adapters/foundationdb/query_plan.ex +++ b/lib/ecto/adapters/foundationdb/query_plan.ex @@ -1,9 +1,5 @@ defmodule Ecto.Adapters.FoundationDB.QueryPlan do - @moduledoc """ - This internal module parses Ecto Query into simpler constructs. - - The FoundationDB Adapter does not support the full Query API. - """ + @moduledoc false alias Ecto.Adapters.FoundationDB.Exception.Unsupported alias Ecto.Adapters.FoundationDB.Layer.Fields alias Ecto.Adapters.FoundationDB.QueryPlan.Between diff --git a/lib/ecto/adapters/foundationdb/schema.ex b/lib/ecto/adapters/foundationdb/schema.ex index fec521b..20f7f17 100644 --- a/lib/ecto/adapters/foundationdb/schema.ex +++ b/lib/ecto/adapters/foundationdb/schema.ex @@ -1,7 +1,5 @@ defmodule Ecto.Adapters.FoundationDB.Schema do - @moduledoc """ - This internal module deals with options on the Ecto Schema. - """ + @moduledoc false def get_context!(_source, schema) do %{__meta__: _meta = %{context: context}} = Kernel.struct!(schema) context diff --git a/lib/ecto/adapters/foundationdb/supervisor.ex b/lib/ecto/adapters/foundationdb/supervisor.ex index 545c5cb..01f83c4 100644 --- a/lib/ecto/adapters/foundationdb/supervisor.ex +++ b/lib/ecto/adapters/foundationdb/supervisor.ex @@ -1,8 +1,5 @@ defmodule Ecto.Adapters.FoundationDB.Supervisor do - @moduledoc """ - This is a top-level supervisor for the FoundationDB Adapter under which - children may be added in the future. - """ + @moduledoc false use Supervisor def start_link(init_arg) do diff --git a/mix.exs b/mix.exs index f8227f6..ff25d32 100644 --- a/mix.exs +++ b/mix.exs @@ -1,6 +1,8 @@ defmodule EctoFoundationdb.MixProject do use Mix.Project + @version "0.1.0" + def project do [ app: :ecto_foundationdb, @@ -12,7 +14,21 @@ defmodule EctoFoundationdb.MixProject do aliases: aliases(), dialyzer: [ ignore_warnings: ".dialyzer_ignore.exs" - ] + ], + + # Docs + name: "Ecto.Adapters.FoundationDB", + docs: docs() + ] + end + + defp docs do + [ + main: "Ecto.Adapters.FoundationDB", + source_ref: "v#{@version}", + source_url: "https://github.com/foundationdb-beam/ecto_foundationdb", + filter_modules: + ~r/^Elixir.Ecto.Adapters.FoundationDB(.Layer|.Sandbox|.Migrator|.Database|.Tenant|.Options|.Exception.Unsupported|.Exception.IncorrectTenancy)?$/ ] end @@ -29,14 +45,12 @@ defmodule EctoFoundationdb.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - # {:dep_from_hexpm, "~> 0.3.0"}, {:erlfdb, git: "https://github.com/foundationdb-beam/erlfdb.git", branch: "main"}, {:ecto, "~> 3.10"}, {:jason, "~> 1.4"}, {:credo, "~> 1.6", only: [:dev, :test, :docs]}, {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, - - # Benchmarks + {:ex_doc, "~> 0.16", only: :dev, runtime: false}, {:benchee, "~> 1.0", only: :dev} ] end diff --git a/mix.lock b/mix.lock index 12a1623..2b28dec 100644 --- a/mix.lock +++ b/mix.lock @@ -5,11 +5,17 @@ "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, "ecto": {:hex, :ecto, "3.11.1", "4b4972b717e7ca83d30121b12998f5fcdc62ba0ed4f20fd390f16f3270d85c3e", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebd3d3772cd0dfcd8d772659e41ed527c28b2a8bde4b00fe03e0463da0f1983b"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "erlfdb": {:git, "https://github.com/foundationdb-beam/erlfdb.git", "a251706335aba208d0d68bf8b34a2f571a9bdf3b", [branch: "main"]}, + "ex_doc": {:hex, :ex_doc, "0.31.2", "8b06d0a5ac69e1a54df35519c951f1f44a7b7ca9a5bb7a260cd8a174d6322ece", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "317346c14febaba9ca40fd97b5b5919f7751fb85d399cc8e7e8872049f37e0af"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, }