diff --git a/.formatter.exs b/.formatter.exs index de7099b..bd24f7f 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,4 +1,22 @@ # Used by "mix format" -[ - inputs: ["{mix,.formatter}.exs", "{config,lib,examples,test,test_integration}/**/*.{ex,exs}"] -] + +integration_apps = + Enum.map( + Path.wildcard("test_integration/apps/*"), + &"#{&1}/{config,lib,test,mix}/**/*.{ex,exs}" + ) + +inputs = + [ + "{mix,.formatter}.exs", + "config/*.exs", + "lib/**/*.ex", + "examples/**/*.exs", + "test/**/*.{ex,exs}", + "priv/repo/migrations/**/*.exs", + "priv/repo/*/migrations/**/*.exs" + ] ++ integration_apps + +IO.inspect(inputs) + +[inputs: inputs] diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..c9311cd --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: solnic diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..433817f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,53 @@ +name: CI + +on: + push: + branches: + - main + + pull_request: + branches: + - main + +jobs: + tests: + name: Run tests (Elixir ${{ matrix.elixir-version }}) + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + + matrix: + include: + - elixir-version: "1.18.4" + container: "dev-latest" + - elixir-version: "1.17.3" + container: "dev-1.17" + - elixir-version: "1.16.3" + container: "dev-1.16" + - elixir-version: "1.15.8" + container: "dev-1.15" + + steps: + - uses: actions/checkout@v4 + + - name: Start db service + run: docker compose up -d postgres + + - name: Restore deps and _build cache + uses: actions/cache@v3 + with: + # we exclude tls_certificate_check because its dir has no x bit and tar fails to access + # it when creating a cache tarbal + path: | + _build + deps/* + !deps/tls_certificate_check + key: ${{ runner.os }}-mix-${{ matrix.container }}-${{ hashFiles('mix.lock') }} + restore-keys: ${{ runner.os }}-mix-${{ matrix.container }}- + + - name: Setup + run: docker compose run -e MIX_ENV=test --rm ${{ matrix.container }} bin/setup + + - name: Run tests + run: docker compose run -e MIX_ENV=test --rm ${{ matrix.container }} mix test diff --git a/.gitignore b/.gitignore index 259c852..3bb0caa 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,4 @@ drops_relation-*.tar priv/*.db* priv/repo/*sqlite* -test_integration/apps/*/_build -test_integration/apps/*/deps test_integration/apps/*/priv/repo/*sqlite* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..485b4bf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +ARG ELIXIR_VERSION=1.18.4 +ARG OTP_VERSION=28.0.2 +ARG DISTRO=ubuntu-noble-20250714 + +ARG IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-${DISTRO}" + +FROM ${IMAGE} + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + git \ + postgresql-client-16 \ + libpq-dev \ + sqlite3 + +WORKDIR /workspace/drops-relation + +COPY mix.exs mix.lock ./ + +RUN mix local.hex --force && \ + mix local.rebar --force diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..86aa5b5 --- /dev/null +++ b/bin/setup @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +cd test_integration/apps/pristine + +mix deps.get +mix compile + +cd ../../.. + +cd test_integration/apps/sample + +mix deps.get +mix compile + +mix ecto.setup +mix drops.relation.refresh_cache + +cd ../../.. + +mix deps.get +mix compile + +mix ecto.setup diff --git a/config/config.exs b/config/config.exs index 8c5cc8b..f8b3359 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,7 +1,15 @@ import Config -# Configure ecto repos -config :drops_relation, ecto_repos: [Test.Repos.Sqlite, Test.Repos.Postgres] +# Configure ecto repos - environment specific +case config_env() do + :test -> + config :drops_relation, + ecto_repos: [Test.Repos.Sqlite, Test.Repos.Postgres, MyApp.Repo] + + _ -> + config :drops_relation, + ecto_repos: [MyApp.Repo] +end # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. diff --git a/config/runtime.exs b/config/runtime.exs index b0c9954..a809907 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -2,31 +2,35 @@ import Config env = config_env() -if adapter = System.get_env("ADAPTER") do - config :drops_relation, ecto_repos: [Module.concat(["Test", "Repos", adapter])] -end +# if adapter = System.get_env("ADAPTER") do +# config :drops_relation, ecto_repos: [Module.concat(["Test", "Repos", adapter])] +# end config :logger, :default_handler, config: [file: ~c"log/#{env}.log"] config :drops_relation, Test.Repos.Sqlite, adapter: Ecto.Adapters.SQLite3, database: "priv/repo/#{env}.sqlite", - pool_size: 1, pool: Ecto.Adapters.SQL.Sandbox, - queue_target: 5000, - queue_interval: 1000, - log: :debug, - priv: "priv/repo/sqlite" + priv: "priv/repo/sqlite", + log: :debug config :drops_relation, Test.Repos.Postgres, adapter: Ecto.Adapters.Postgres, username: "postgres", password: "postgres", - hostname: "postgres", + hostname: System.get_env("POSTGRES_HOST", "postgres"), database: "drops_relation_#{env}", - pool_size: 10, pool: Ecto.Adapters.SQL.Sandbox, - queue_target: 5000, - queue_interval: 1000, + priv: "priv/repo/postgres", + log: :debug + +config :drops_relation, MyApp.Repo, + adapter: Ecto.Adapters.Postgres, + username: "postgres", + password: "postgres", + hostname: System.get_env("POSTGRES_HOST", "postgres"), + database: "drops_relation_#{env}_my_app", + pool: Ecto.Adapters.SQL.Sandbox, priv: "priv/repo/postgres", log: :debug diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..25b6959 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,76 @@ +services: + base: &base + build: + context: . + dockerfile: Dockerfile + working_dir: /workspace/drops-relation + command: sleep infinity + depends_on: [postgres] + links: [postgres] + volumes: + - ".:/workspace/drops-relation" + - "drops_relation_deps:/workspace/drops-relation/deps" + + postgres: + image: postgres:latest + environment: + POSTGRES_PASSWORD: postgres + POSTGRES_DATABASE: postgres + POSTGRES_USERNAME: postgres + ports: + - 5432:5432 + command: [ "postgres", "-c", "log_statement=all" ] + volumes: + - "drops_relation_pgdata:/var/lib/postgresql/data" + + dev-latest: &dev-latest + <<: *base + build: + context: . + dockerfile: Dockerfile + args: + ELIXIR_VERSION: 1.18.4 + OTP_VERSION: 28.0.2 + + dev-1.17: + <<: *base + build: + context: . + dockerfile: Dockerfile + args: + ELIXIR_VERSION: 1.17.3 + OTP_VERSION: 27.3.4.2 + + dev-1.16: + <<: *base + build: + context: . + dockerfile: Dockerfile + args: + ELIXIR_VERSION: 1.16.3 + OTP_VERSION: 26.2.5.14 + + dev-1.15: + <<: *base + build: + context: . + dockerfile: Dockerfile + args: + ELIXIR_VERSION: 1.15.8 + OTP_VERSION: 25.3.2.21 + + dev-1.14: + <<: *base + build: + context: . + dockerfile: Dockerfile + args: + ELIXIR_VERSION: 1.14.5 + OTP_VERSION: 24.3.4.17 + + test: + <<: *dev-latest + +volumes: + drops_relation_deps: + drops_relation_pgdata: diff --git a/drops_relation_test b/drops_relation_test deleted file mode 100644 index 8949671..0000000 Binary files a/drops_relation_test and /dev/null differ diff --git a/lib/drops/relation/cache.ex b/lib/drops/relation/cache.ex index 7177b94..50c65d7 100644 --- a/lib/drops/relation/cache.ex +++ b/lib/drops/relation/cache.ex @@ -340,11 +340,11 @@ defmodule Drops.Relation.Cache do end defp encode(data) do - JSON.encode!(data) + Drops.Relation.json().encode!(data) end defp decode(data) do - JSON.decode!(data) + Drops.Relation.json().decode!(data) end defp read_cache_file(cache_file) do diff --git a/lib/drops/relation/relation.ex b/lib/drops/relation/relation.ex index 1950ae9..a46a510 100644 --- a/lib/drops/relation/relation.ex +++ b/lib/drops/relation/relation.ex @@ -191,6 +191,14 @@ defmodule Drops.Relation do end end + if Code.ensure_loaded?(JSON) do + @doc false + def json, do: JSON + else + @doc false + def json, do: Jason + end + @core_plugins [ Drops.Relation.Plugins.Schema, Drops.Relation.Plugins.Queryable, diff --git a/lib/drops/relation/schema.ex b/lib/drops/relation/schema.ex index 6540b67..e46d147 100644 --- a/lib/drops/relation/schema.ex +++ b/lib/drops/relation/schema.ex @@ -87,20 +87,6 @@ defmodule Drops.Relation.Schema do use Serializable - # defimpl JSON.Encoder do - # def encode(schema, opts) do - # JSON.Encoder.encode( - # Map.merge( - # %{ - # __struct__: "Schema" - # }, - # Map.from_struct(schema) - # ), - # opts - # ) - # end - # end - @doc """ Creates a new Schema struct with the provided metadata. diff --git a/lib/drops/relation/schema/serializable.ex b/lib/drops/relation/schema/serializable.ex index e560a5c..7803f20 100644 --- a/lib/drops/relation/schema/serializable.ex +++ b/lib/drops/relation/schema/serializable.ex @@ -27,17 +27,14 @@ defmodule Drops.Relation.Schema.Serializable do defmacro __using__(_opts) do quote location: :keep do - defimpl JSON.Encoder, for: __MODULE__ do - def encode(component, opts) do + defimpl unquote(Drops.Relation.json()).Encoder, for: __MODULE__ do + def encode(component, _opts) do attributes = Dumper.dump(Map.from_struct(component)) - JSON.Encoder.encode( - %{ - __struct__: component.__struct__.name(), - attributes: attributes - }, - opts - ) + Drops.Relation.json().encode!(%{ + __struct__: component.__struct__.name(), + attributes: attributes + }) end end diff --git a/lib/mix/tasks/dev/test.setup.ex b/lib/mix/tasks/dev/test.setup.ex index d4c1853..b64b19f 100644 --- a/lib/mix/tasks/dev/test.setup.ex +++ b/lib/mix/tasks/dev/test.setup.ex @@ -15,7 +15,7 @@ if Mix.env() == :test do case adapter() do nil -> - Test.Repos.start(:all) + Test.Repos.start_all(:auto) adapter -> Test.Repos.start(adapter) diff --git a/mix.exs b/mix.exs index a07e337..045822f 100644 --- a/mix.exs +++ b/mix.exs @@ -12,7 +12,7 @@ defmodule Drops.Relation.MixProject do elixir: "~> 1.14", elixirc_options: [warnings_as_errors: false], start_permanent: Mix.env() == :prod, - deps: deps(), + deps: deps(Mix.env(), System.version()), licenses: [@license], description: ~S""" Provides a convenient query API that wraps Ecto.Schema and delegates to Ecto.Repo functions with automatic schema inference from database tables. @@ -35,12 +35,13 @@ defmodule Drops.Relation.MixProject do def cli do [ preferred_envs: [ - "test.refresh_cache": :test, "ecto.migrate": :test, "ecto.reset": :test, "ecto.drop": :test, "ecto.create": :test, "ecto.setup": :test, + test: :test, + "test.refresh_cache": :test, "test.setup": :test, "test.example": :test, "test.cov.update_tasks": :test, @@ -133,28 +134,42 @@ defmodule Drops.Relation.MixProject do ] end - defp deps do + defp deps(_, version) when version != "all" do + base_deps = deps(:test, "all") + + if Version.match?(version, "< 1.18.0") do + base_deps ++ + [ + {:jason, "~> 1.4"} + ] + else + base_deps + end + end + + defp deps(_, _) do [ {:nimble_options, "~> 1.0"}, - {:drops_inflector, "~> 0.1", github: "solnic/drops_inflector"}, + {:drops_inflector, "~> 0.1"}, {:ecto, "~> 3.10"}, {:ecto_sql, "~> 3.10"}, + {:igniter, "~> 0.6"}, {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, {:excoveralls, "~> 0.18", only: [:dev, :test], runtime: false}, {:doctor, "~> 0.21.0", only: :dev, runtime: false}, {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, - {:igniter, "~> 0.6", optional: true}, - {:ecto_sqlite3, "~> 0.12", only: [:test, :dev], optional: true}, - {:postgrex, "~> 0.17", only: [:test, :dev], optional: true} + {:ecto_sqlite3, "~> 0.12", only: [:test, :dev], optional: true, runtime: false}, + {:postgrex, "~> 0.17", only: [:test, :dev], optional: true, runtime: false} ] end defp aliases do [ "test.refresh_cache": ["test.setup", "drops.relation.refresh_cache"], - "ecto.migrate": ["ecto.migrate", "test.refresh_cache"], + "ecto.migrate": ["test.setup", "ecto.migrate", "test.refresh_cache"], "ecto.rollback": ["ecto.rollback", "test.refresh_cache"], + "ecto.setup": ["ecto.create", "ecto.migrate"], "ecto.reset": ["ecto.drop --force-drop", "ecto.create", "ecto.migrate"], "ecto.dump": ["test.setup", "ecto.dump"], "ecto.load": ["test.setup", "ecto.load", "test.refresh_cache"], diff --git a/mix.lock b/mix.lock index b55a463..ab83c78 100644 --- a/mix.lock +++ b/mix.lock @@ -6,7 +6,7 @@ "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, "doctor": {:hex, :doctor, "0.21.0", "20ef89355c67778e206225fe74913e96141c4d001cb04efdeba1a2a9704f1ab5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "a227831daa79784eb24cdeedfa403c46a4cb7d0eab0e31232ec654314447e4e0"}, - "drops_inflector": {:git, "https://github.com/solnic/drops_inflector.git", "f368ffc030c9e676dd8821432c118a9c22997233", []}, + "drops_inflector": {:hex, :drops_inflector, "0.1.0", "96872d716567cbb9aae017fc2f3722b7916a10fcef7a3bd118137eaf51f24f39", [:mix], [], "hexpm", "564a383fdcb553cc291d889597845fd23d8a3ff026a4da93bdae0e75d893d4e1"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "ecto": {:hex, :ecto, "3.13.2", "7d0c0863f3fc8d71d17fc3ad3b9424beae13f02712ad84191a826c7169484f01", [: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", "669d9291370513ff56e7b7e7081b7af3283d02e046cf3d403053c557894a0b3e"}, "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"}, diff --git a/test/drops/relation/cache_test.exs b/test/drops/relation/cache_test.exs index d8e05f9..49aae00 100644 --- a/test/drops/relation/cache_test.exs +++ b/test/drops/relation/cache_test.exs @@ -5,6 +5,8 @@ defmodule Drops.Relation.CacheTest do alias Drops.Relation.Schema alias Drops.Relation.Schema.{Field, PrimaryKey, ForeignKey, Index} + @json_lib Drops.Relation.json() + # Mock repository for testing defmodule TestRepo do def config do @@ -184,7 +186,7 @@ defmodule Drops.Relation.CacheTest do meta: %{nullable: false, default: nil} } - dumped = JSON.encode!(field) |> JSON.decode!() + dumped = @json_lib.encode!(field) |> @json_lib.decode!() assert dumped["__struct__"] == "Field" assert dumped["attributes"]["name"] == ["atom", "email"] assert dumped["attributes"]["type"] == ["atom", "string"] @@ -203,7 +205,7 @@ defmodule Drops.Relation.CacheTest do meta: %{} } - dumped = JSON.encode!(field) |> JSON.decode!() + dumped = @json_lib.encode!(field) |> @json_lib.decode!() loaded = Drops.Relation.Schema.Field.load(dumped) assert loaded.type == {:array, :string} assert loaded == field @@ -215,7 +217,7 @@ defmodule Drops.Relation.CacheTest do field = %Field{name: :id, type: :id, source: :id, meta: %{}} pk = %PrimaryKey{fields: [field]} - dumped = JSON.encode!(pk) |> JSON.decode!() + dumped = @json_lib.encode!(pk) |> @json_lib.decode!() assert dumped["__struct__"] == "PrimaryKey" assert is_list(dumped["attributes"]["fields"]) @@ -226,7 +228,7 @@ defmodule Drops.Relation.CacheTest do test "handles empty PrimaryKey" do pk = %PrimaryKey{fields: []} - dumped = JSON.encode!(pk) |> JSON.decode!() + dumped = @json_lib.encode!(pk) |> @json_lib.decode!() loaded = Drops.Relation.Schema.PrimaryKey.load(dumped) assert loaded == pk end @@ -240,7 +242,7 @@ defmodule Drops.Relation.CacheTest do references_field: :id } - dumped = JSON.encode!(fk) |> JSON.decode!() + dumped = @json_lib.encode!(fk) |> @json_lib.decode!() assert dumped["__struct__"] == "ForeignKey" assert dumped["attributes"]["field"] == ["atom", "user_id"] assert dumped["attributes"]["references_table"] == "users" @@ -262,7 +264,7 @@ defmodule Drops.Relation.CacheTest do type: :btree } - dumped = JSON.encode!(index) |> JSON.decode!() + dumped = @json_lib.encode!(index) |> @json_lib.decode!() assert dumped["__struct__"] == "Index" assert dumped["attributes"]["name"] == "users_email_index" assert dumped["attributes"]["unique"] == true @@ -295,7 +297,7 @@ defmodule Drops.Relation.CacheTest do indices: indices } - dumped = JSON.encode!(schema) |> JSON.decode!() + dumped = @json_lib.encode!(schema) |> @json_lib.decode!() assert dumped["__struct__"] == "Schema" assert dumped["attributes"]["source"] == "test_table" @@ -312,7 +314,7 @@ defmodule Drops.Relation.CacheTest do indices: [] } - dumped = JSON.encode!(schema) |> JSON.decode!() + dumped = @json_lib.encode!(schema) |> @json_lib.decode!() loaded = Drops.Relation.Schema.load(dumped) assert loaded == schema end @@ -345,7 +347,7 @@ defmodule Drops.Relation.CacheTest do # Read the cache file directly to verify serialization worked if File.exists?(cache_file) do {:ok, content} = File.read(cache_file) - data = JSON.decode!(content) + data = @json_lib.decode!(content) # Verify the schema was serialized with the new protocol format assert data["schema"]["__struct__"] == "Schema" diff --git a/test/drops/relation/generator_test.exs b/test/drops/relation/generator_test.exs index beb674b..3908617 100644 --- a/test/drops/relation/generator_test.exs +++ b/test/drops/relation/generator_test.exs @@ -1,5 +1,5 @@ defmodule Mix.Tasks.Drops.Relation.GeneratorTest do - use Test.DoctestCase, async: true + use Test.DoctestCase, async: false alias Drops.Relation.Generator diff --git a/test/drops/relation/schema/code_compiler_test.exs b/test/drops/relation/schema/code_compiler_test.exs index b1cf045..ec64c6d 100644 --- a/test/drops/relation/schema/code_compiler_test.exs +++ b/test/drops/relation/schema/code_compiler_test.exs @@ -6,7 +6,7 @@ defmodule Drops.Relation.Compilers.CodeCompilerTest do their behavior using Ecto's reflection functions, making the tests more robust than AST comparison. """ - use ExUnit.Case, async: true + use ExUnit.Case, async: false alias Drops.Relation.Compilers.CodeCompiler alias Drops.Relation.Generator diff --git a/test/drops/relation/schema/patcher_test.exs b/test/drops/relation/schema/patcher_test.exs index 569a9f1..56710d3 100644 --- a/test/drops/relation/schema/patcher_test.exs +++ b/test/drops/relation/schema/patcher_test.exs @@ -5,7 +5,7 @@ defmodule Drops.Relation.Schema.PatcherTest do These tests verify that the Patcher can correctly update existing schema modules while preserving custom code. """ - use ExUnit.Case, async: true + use ExUnit.Case, async: false alias Drops.Relation.Schema alias Drops.Relation.Schema.{Field, PrimaryKey, Patcher} diff --git a/test/drops/relation/schema_test.exs b/test/drops/relation/schema_test.exs index 6065cce..d029c28 100644 --- a/test/drops/relation/schema_test.exs +++ b/test/drops/relation/schema_test.exs @@ -1,5 +1,5 @@ defmodule Drops.Relation.SchemaTest do - use Test.RelationCase, async: true + use Test.RelationCase, async: false describe "merge/2" do @describetag adapter: :sqlite diff --git a/test/support/doctest/my_app.ex b/test/support/doctest/my_app.ex index 7be8231..82cffbc 100644 --- a/test/support/doctest/my_app.ex +++ b/test/support/doctest/my_app.ex @@ -9,11 +9,3 @@ defmodule MyApp do Supervisor.start_link([], opts) end end - -defmodule MyApp.Repo do - @moduledoc false - - use Ecto.Repo, - otp_app: :my_app, - adapter: Ecto.Adapters.Postgres -end diff --git a/test/support/doctest_case.ex b/test/support/doctest_case.ex index 21f568f..9dd3486 100644 --- a/test/support/doctest_case.ex +++ b/test/support/doctest_case.ex @@ -7,7 +7,9 @@ defmodule Test.DoctestCase do setup tags do if tags[:test_type] == :doctest do - Test.Repos.start_owner!(MyApp.Repo, shared: not tags[:async]) + :ok = Test.Repos.start_owner!(MyApp.Repo, shared: not tags[:async]) + + {:ok, _} = Drops.Relation.Cache.warm_up(MyApp.Repo, ["users", "posts"]) modules_before = Test.loaded_modules() @@ -16,7 +18,7 @@ defmodule Test.DoctestCase do Test.Fixtures.load(fixtures) on_exit(fn -> - Test.Repos.stop_owner(MyApp.Repo) + :ok = Test.Repos.stop_owner(MyApp.Repo) new_modules = MapSet.difference(Test.loaded_modules(), modules_before) test_module_prefix = to_string(__MODULE__) diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex index 80c4721..99264be 100644 --- a/test/support/fixtures.ex +++ b/test/support/fixtures.ex @@ -1,10 +1,5 @@ defmodule Test.Fixtures do - @moduledoc """ - Provides consistent fixture data for doctests and tests. - - This module loads predefined fixture data into the database to ensure - doctests have consistent, predictable data to work with. - """ + @moduledoc false @doc """ Loads fixture data for the specified tables. @@ -72,19 +67,11 @@ defmodule Test.Fixtures do MyApp.Repo.delete_all(MyApp.Users) MyApp.Repo.insert_all("users", users) - end - @doc """ - Loads post fixtures into the posts table. + reset_sequence("users", "id", 4) + end - Creates 4 posts with predictable data: - - Post with ID 1: "First Post" by John (user_id: 1), published: true, view_count: 100 - - Post with ID 2: "Second Post" by Jane (user_id: 2), published: true, view_count: 50 - - Post with ID 3: "Draft Post" by John (user_id: 1), published: false, view_count: 0 - - Post with ID 4: "Another Post" by Bob (user_id: 3), published: true, view_count: 25 - """ def load_posts do - # Insert fixture data with explicit IDs and timestamps now = DateTime.utc_now() |> DateTime.truncate(:second) posts = [ @@ -132,5 +119,12 @@ defmodule Test.Fixtures do MyApp.Repo.delete_all(MyApp.Posts) MyApp.Repo.insert_all("posts", posts) + + reset_sequence("posts", "id", 5) + end + + defp reset_sequence(table_name, column_name, next_value) do + sequence_name = "#{table_name}_#{column_name}_seq" + MyApp.Repo.query!("SELECT setval('#{sequence_name}', #{next_value})") end end diff --git a/test/support/integration_case.ex b/test/support/integration_case.ex index 0177152..71f0a46 100644 --- a/test/support/integration_case.ex +++ b/test/support/integration_case.ex @@ -1,12 +1,5 @@ defmodule Test.IntegrationCase do - @moduledoc """ - Test case for integration tests that run mix tasks within the sample directory. - - This case provides: - - Automatic MIX_ENV management - - Directory cleanup based on tags - - Helper for running mix tasks in sample context - """ + @moduledoc false use ExUnit.CaseTemplate @@ -19,75 +12,29 @@ defmodule Test.IntegrationCase do end setup tags do - # Store original environment original_cwd = File.cwd!() - original_env = System.get_env("MIX_ENV") - original_adapter = System.get_env("ADAPTER") app = Map.get(tags, :app, "sample") - adapter = Map.get(tags, :adapter, String.to_atom(System.get_env("ADAPTER", "sqlite"))) - app_path = Path.join(@apps_path, app) - # Change to app directory File.cd!(app_path) - # Set MIX_ENV to dev to avoid test database ownership issues - System.put_env("MIX_ENV", "dev") - - # Set ADAPTER environment variable for the test - System.put_env("ADAPTER", Atom.to_string(adapter)) - - # Clean and recompile when switching adapters to avoid compile-time config issues - # Always clean and recompile to ensure the correct adapter configuration - System.cmd("mix", ["deps.clean", "sample", "--build"], env: [{"MIX_ENV", "dev"}]) - - System.cmd("mix", ["compile", "--force"], - env: [{"MIX_ENV", "dev"}, {"ADAPTER", Atom.to_string(adapter)}] - ) - - # Handle file state management files_to_restore = Map.get(tags, :files, []) original_file_contents = backup_files(files_to_restore) - # Clean directories if specified in tags clean_dirs = Map.get(tags, :clean_dirs, []) clean_directories(clean_dirs) - # Clear the cache to ensure clean state between tests clear_cache() on_exit(fn -> - # Restore original working directory File.cd!(original_cwd) - - # Restore original MIX_ENV - if original_env do - System.put_env("MIX_ENV", original_env) - else - System.delete_env("MIX_ENV") - end - - # Restore original ADAPTER - if original_adapter do - System.put_env("ADAPTER", original_adapter) - else - System.delete_env("ADAPTER") - end - - # Change back to app directory for cleanup File.cd!(app_path) - # Restore original file contents restore_files(files_to_restore, original_file_contents) - - # Clean up directories after test clean_directories(clean_dirs) - - # Clear cache after test clear_cache() - # Restore original working directory again File.cd!(original_cwd) end) @@ -112,14 +59,7 @@ defmodule Test.IntegrationCase do args = String.split(task_string, " ") [task_name | task_args] = args - env = [{"MIX_ENV", "dev"}] - - # Pass through ADAPTER environment variable if set - env = - case System.get_env("ADAPTER") do - nil -> env - adapter -> [{"ADAPTER", adapter} | env] - end + env = [{"MIX_ENV", "test"}] System.cmd( "mix", @@ -203,8 +143,7 @@ defmodule Test.IntegrationCase do end defp clear_cache do - # Clear the drops_relation cache directory for dev environment - cache_dir = Path.join(File.cwd!(), "tmp/cache/dev/drops_relation_schema") + cache_dir = Path.join(File.cwd!(), "tmp/cache/test/drops_relation_schema") if File.exists?(cache_dir) do File.rm_rf!(cache_dir) diff --git a/test/support/relation_case.ex b/test/support/relation_case.ex index 1836aab..48f956a 100644 --- a/test/support/relation_case.ex +++ b/test/support/relation_case.ex @@ -16,14 +16,17 @@ defmodule Test.RelationCase do if tags[:test_type] != :doctest do adapter = Map.get(tags, :adapter, String.to_atom(System.get_env("ADAPTER", "sqlite"))) - setup_sandbox(tags, adapter) + :ok = Test.Repos.start_owner!(adapter, shared: not tags[:async]) context = Enum.reduce(Map.get(tags, :relations, []), %{}, fn name, context -> Map.put(context, name, create_relation(name, adapter: adapter)) end) - on_exit(fn -> Test.cleanup_relation_modules(Map.values(context)) end) + on_exit(fn -> + :ok = Test.Repos.stop_owner(adapter) + Test.cleanup_relation_modules(Map.values(context)) + end) {:ok, Map.merge(context, %{adapter: adapter, repo: repo(adapter)})} else @@ -91,11 +94,6 @@ defmodule Test.RelationCase do relation_module end - def setup_sandbox(tags, adapter) do - Test.Repos.start_owner!(adapter, shared: not tags[:async]) - on_exit(fn -> Test.Repos.stop_owner(adapter) end) - end - def repo(:sqlite), do: Test.Repos.Sqlite def repo(:postgres), do: Test.Repos.Postgres diff --git a/test/support/repos.ex b/test/support/repos.ex index 8bca8a7..669da9a 100644 --- a/test/support/repos.ex +++ b/test/support/repos.ex @@ -3,26 +3,47 @@ defmodule Test.Repos do @adapters [:sqlite, :postgres] - def start(repo_or_adapter, mode \\ :auto) + def start_all(mode) do + Enum.each(@adapters, &start(&1, mode)) + start(MyApp.Repo, mode) + end + + def start(adapter, mode \\ :auto) - def start(:all, mode), do: Enum.each(@adapters, &start(&1, mode)) def start(:sqlite, mode), do: start(Test.Repos.Sqlite, mode) def start(:postgres, mode), do: start(Test.Repos.Postgres, mode) def start(repo, mode) do - {:ok, pid} = repo.start_link() - env = Application.get_env(:drops_relation, :env, :test) - Ecto.Adapters.SQL.Sandbox.mode(repo, mode || mode(env)) - :persistent_term.put({:repos, repo}, pid) - end + case Process.whereis(repo) do + nil -> + {:ok, _pid} = repo.start_link() + + :ok = Ecto.Adapters.SQL.Sandbox.mode(repo, mode) - def stop(repo) do - pid = :persistent_term.get({:repos, repo, :owner}) - Ecto.Adapters.SQL.Sandbox.stop_owner(pid) + _pid -> + :ok + end end - defp mode(:dev), do: :auto - defp mode(:test), do: :manual + def stop_owner(:sqlite), do: stop_owner(Test.Repos.Sqlite) + def stop_owner(:postgres), do: stop_owner(Test.Repos.Postgres) + + def stop_owner(repo) do + case owner_pid(repo) do + nil -> + :ok + + pid -> + try do + Ecto.Adapters.SQL.Sandbox.stop_owner(pid) + rescue + _ -> :ok + end + + :persistent_term.erase({:repos, repo, :owner}) + :ok + end + end def start_owner!(repo, opts \\ []) @@ -30,33 +51,33 @@ defmodule Test.Repos do def start_owner!(:postgres, opts), do: start_owner!(Test.Repos.Postgres, opts) def start_owner!(repo, opts) do - case ensure_started(repo) do - {:ok, _pid} -> - try do - pid = Ecto.Adapters.SQL.Sandbox.start_owner!(repo, opts) + retry_count = Keyword.get(opts, :retry, 0) + max_retries = Keyword.get(opts, :max_retries, 3) - :persistent_term.put({:repos, repo, :owner}, pid) - rescue - error -> - if opts[:retry] <= 3 do - start_owner!(repo, Keyword.put(opts, :retry, opts[:retry] || 0 + 1)) + try do + pid = Ecto.Adapters.SQL.Sandbox.start_owner!(repo, opts) + :persistent_term.put({:repos, repo, :owner}, pid) + :ok + rescue + error -> + case error do + %MatchError{term: {:error, {{:badmatch, :already_shared}, _}}} -> + :ok + + _ -> + if retry_count < max_retries do + cleanup_owner_state(repo) + Process.sleep(50 * (retry_count + 1)) + + retry_opts = Keyword.put(opts, :retry, retry_count + 1) + start_owner!(repo, retry_opts) else reraise error, __STACKTRACE__ end end - - {:error, error} -> - raise "Failed to start repo #{repo}: #{inspect(error)}" end end - def stop_owner(:sqlite), do: stop_owner(Test.Repos.Sqlite) - def stop_owner(:postgres), do: stop_owner(Test.Repos.Postgres) - - def stop_owner(repo) do - Ecto.Adapters.SQL.Sandbox.stop_owner(:persistent_term.get({:repos, repo, :owner})) - end - def with_owner(repo, fun) do try do start_owner!(repo, shared: false) @@ -65,9 +86,7 @@ defmodule Test.Repos do error -> reraise error, __STACKTRACE__ after - if repo_pid(repo) do - stop_owner(repo) - end + stop_owner(repo) end end @@ -75,26 +94,24 @@ defmodule Test.Repos do Enum.each(Application.get_env(:drops_relation, :ecto_repos), &fun.(&1)) end - defp ensure_started(repo) do - case Process.whereis(repo) do - nil -> - try do - :ok = Test.Repos.start(repo) + defp owner_pid(repo) do + :persistent_term.get({:repos, repo, :owner}, nil) + end - {:ok, repo_pid(repo)} - rescue - error -> - {:error, error} - end + defp cleanup_owner_state(repo) do + try do + case owner_pid(repo) do + nil -> + :ok - pid -> - {:ok, pid} + _pid -> + :persistent_term.erase({:repos, repo, :owner}) + :ok + end + rescue + _ -> :ok end end - - defp repo_pid(repo) do - :persistent_term.get({:repos, repo}) - end end defmodule Test.Repos.Sqlite do @@ -102,6 +119,7 @@ defmodule Test.Repos.Sqlite do use Ecto.Repo, otp_app: :drops_relation, + pool: Ecto.Adapters.SQL.Sandbox, adapter: Ecto.Adapters.SQLite3 end @@ -110,5 +128,15 @@ defmodule Test.Repos.Postgres do use Ecto.Repo, otp_app: :drops_relation, + pool: Ecto.Adapters.SQL.Sandbox, + adapter: Ecto.Adapters.Postgres +end + +defmodule MyApp.Repo do + @moduledoc false + + use Ecto.Repo, + otp_app: :drops_relation, + pool: Ecto.Adapters.SQL.Sandbox, adapter: Ecto.Adapters.Postgres end diff --git a/test/test_helper.exs b/test/test_helper.exs index 74c896b..7613ae2 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -4,24 +4,14 @@ Code.require_file("support/relation_case.ex", __DIR__) Code.require_file("support/integration_case.ex", __DIR__) # Doctest setup -Code.require_file("support/doctest/my_app.ex", __DIR__) Code.require_file("support/fixtures.ex", __DIR__) -Application.put_env(:my_app, :ecto_repos, [MyApp.Repo]) -Application.put_env(:my_app, :drops, relation: [repo: MyApp.Repo]) - -Application.put_env(:my_app, MyApp.Repo, - adapter: Ecto.Adapters.Postgres, - username: "postgres", - password: "postgres", - hostname: "postgres", - database: "drops_relation_test", - pool: Ecto.Adapters.SQL.Sandbox, - priv: "priv/repo/postgres" -) +Test.Repos.start_all(:manual) Drops.Relation.Cache.clear_all() +Application.put_env(:my_app, :drops, relation: [repo: MyApp.Repo]) + Test.Repos.with_owner(MyApp.Repo, fn repo -> {:ok, _} = Drops.Relation.Cache.warm_up(repo, ["users", "posts"]) end) diff --git a/test_integration/apps/pristine/mix.exs b/test_integration/apps/pristine/mix.exs index c9e29bf..c18be81 100644 --- a/test_integration/apps/pristine/mix.exs +++ b/test_integration/apps/pristine/mix.exs @@ -7,7 +7,9 @@ defmodule Pristine.MixProject do version: "0.1.0", elixir: "~> 1.14", start_permanent: Mix.env() == :prod, - deps: deps() + deps: deps(), + build_path: "../../../_build/__apps__/pristine", + deps_path: "../../../deps/__apps__/pristine" ] end diff --git a/test_integration/apps/pristine/mix.lock b/test_integration/apps/pristine/mix.lock index ef63f8b..b4260b7 100644 --- a/test_integration/apps/pristine/mix.lock +++ b/test_integration/apps/pristine/mix.lock @@ -1,7 +1,7 @@ %{ "db_connection": {:hex, :db_connection, "2.8.0", "64fd82cfa6d8e25ec6660cea73e92a4cbc6a18b31343910427b702838c4b33b2", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "008399dae5eee1bf5caa6e86d204dcb44242c82b1ed5e22c881f2c34da201b15"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, - "drops_inflector": {:git, "https://github.com/solnic/drops_inflector.git", "b3a3e5d76768e3910d682ca4a972154b1cb4dc2f", []}, + "drops_inflector": {:hex, :drops_inflector, "0.1.0", "96872d716567cbb9aae017fc2f3722b7916a10fcef7a3bd118137eaf51f24f39", [:mix], [], "hexpm", "564a383fdcb553cc291d889597845fd23d8a3ff026a4da93bdae0e75d893d4e1"}, "ecto": {:hex, :ecto, "3.13.2", "7d0c0863f3fc8d71d17fc3ad3b9424beae13f02712ad84191a826c7169484f01", [: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", "669d9291370513ff56e7b7e7081b7af3283d02e046cf3d403053c557894a0b3e"}, "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"}, "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"}, diff --git a/test_integration/apps/sample/config/dev.exs b/test_integration/apps/sample/config/dev.exs new file mode 100644 index 0000000..a846df5 --- /dev/null +++ b/test_integration/apps/sample/config/dev.exs @@ -0,0 +1,13 @@ +# Stub file for igniter, otherwise this happens: + +# Mix task failed with output: Generating schemas for tables: comments, posts, users +# Creating or updating schema: Sample.Schemas.Comments +# ** (File.Error) could not read file "config/dev.exs": no such file or directory +# (elixir 1.15.8) lib/file.ex:358: File.read!/1 +# (rewrite 1.1.2) lib/rewrite/source.ex:127: Rewrite.Source.read!/2 +# (rewrite 1.1.2) lib/rewrite/source/ex.ex:137: Rewrite.Source.Ex.read!/2 +# (igniter 0.6.10) lib/igniter.ex:648: Igniter.include_existing_file/3 +# (igniter 0.6.10) lib/igniter.ex:1514: Igniter.format/3 +# (elixir 1.15.8) lib/enum.ex:2510: Enum."-reduce/3-lists^foldl/2-0-"/3 +# (drops_relation 0.0.1) lib/mix/tasks/drops.relation.gen_schemas.ex:35: Mix.Tasks.Drops.Relation.GenSchemas.run/1 +# (mix 1.15.8) lib/mix/task.ex:455: anonymous fn/3 in Mix.Task.run_task/5 diff --git a/test_integration/apps/sample/config/runtime.exs b/test_integration/apps/sample/config/runtime.exs index c010347..963e5af 100644 --- a/test_integration/apps/sample/config/runtime.exs +++ b/test_integration/apps/sample/config/runtime.exs @@ -27,7 +27,7 @@ case adapter do adapter: Ecto.Adapters.Postgres, username: "postgres", password: "postgres", - hostname: "postgres", - database: "sample_app_#{env}", + hostname: System.get_env("POSTGRES_HOST", "postgres"), + database: "drops_relation_#{env}_sample_app", log: :debug end diff --git a/test_integration/apps/sample/mix.exs b/test_integration/apps/sample/mix.exs index 5539695..3818bd1 100644 --- a/test_integration/apps/sample/mix.exs +++ b/test_integration/apps/sample/mix.exs @@ -9,7 +9,8 @@ defmodule Sample.MixProject do start_permanent: Mix.env() == :prod, deps: deps(), aliases: aliases(), - elixirc_options: [warnings_as_errors: false, no_warn_undefined: :all] + build_path: "../../../_build/__apps__/sample", + deps_path: "../../../deps/__apps__/sample" ] end @@ -23,6 +24,7 @@ defmodule Sample.MixProject do def aliases do [ "ecto.migrate": ["ecto.migrate", "drops.relation.refresh_cache"], + "ecto.setup": ["ecto.create", "ecto.migrate"], "ecto.rollback": ["ecto.rollback", "drops.relation.refresh_cache"], "ecto.load": ["ecto.load", "drops.relation.refresh_cache"], "ecto.reset": ["ecto.drop", "ecto.create", "ecto.migrate"] @@ -35,8 +37,7 @@ defmodule Sample.MixProject do {:ecto_sqlite3, "~> 0.17"}, {:postgrex, "~> 0.17"}, {:drops_relation, path: "../../.."}, - {:jason, "~> 1.4"}, - {:igniter, "~> 0.6", optional: true} + {:igniter, "~> 0.6"} ] end end diff --git a/test_integration/apps/sample/mix.lock b/test_integration/apps/sample/mix.lock index 6779257..cf44f61 100644 --- a/test_integration/apps/sample/mix.lock +++ b/test_integration/apps/sample/mix.lock @@ -2,16 +2,16 @@ "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, "db_connection": {:hex, :db_connection, "2.8.0", "64fd82cfa6d8e25ec6660cea73e92a4cbc6a18b31343910427b702838c4b33b2", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "008399dae5eee1bf5caa6e86d204dcb44242c82b1ed5e22c881f2c34da201b15"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, - "drops_inflector": {:git, "https://github.com/solnic/drops_inflector.git", "f368ffc030c9e676dd8821432c118a9c22997233", []}, + "drops_inflector": {:hex, :drops_inflector, "0.1.0", "96872d716567cbb9aae017fc2f3722b7916a10fcef7a3bd118137eaf51f24f39", [:mix], [], "hexpm", "564a383fdcb553cc291d889597845fd23d8a3ff026a4da93bdae0e75d893d4e1"}, "ecto": {:hex, :ecto, "3.13.2", "7d0c0863f3fc8d71d17fc3ad3b9424beae13f02712ad84191a826c7169484f01", [: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", "669d9291370513ff56e7b7e7081b7af3283d02e046cf3d403053c557894a0b3e"}, "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"}, "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.21.0", "8531f5044fb08289b3aacd21e383a9fb187e5a78981b9ed6d0929a78a25c2341", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "9c3e90ea33099ca0ddd160c8d9eaf80d7d4a9b110d325fa6ed0409858a714606"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, - "exqlite": {:hex, :exqlite, "0.32.1", "3fff269335457c202c850510cf5fda099f71784110377528a42f0ce7b5513af9", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "171e7bb46c4d2f0b6ccc8a40e336033d56dfaac594ae1f198ab592ba12794767"}, + "exqlite": {:hex, :exqlite, "0.33.0", "2cc96c4227fbb2d0864716def736dff18afb9949b1eaa74630822a0865b4b342", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8a7c2792e567bbebb4dafe96f6397f1c527edd7039d74f508a603817fbad2844"}, "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"}, "glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, - "igniter": {:hex, :igniter, "0.6.10", "896d75fc48ed493ff22accd111fe2e34747163d26c5f374267bad1ef4a6c5076", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "9a3abc56e94f362730a3023dfe0ac2ced1186f95fa1ccf4cc30df0c8ce0fc276"}, + "igniter": {:hex, :igniter, "0.6.25", "e2774a4605c2bc9fc38f689232604aea0fc925c7966ae8e928fd9ea2fa9d300c", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "b1916e1e45796d5c371c7671305e81277231617eb58b1c120915aba237fbce6a"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, @@ -19,7 +19,7 @@ "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "owl": {:hex, :owl, "0.12.2", "65906b525e5c3ef51bab6cba7687152be017aebe1da077bb719a5ee9f7e60762", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "6398efa9e1fea70a04d24231e10dcd66c1ac1aa2da418d20ef5357ec61de2880"}, "postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"}, - "req": {:hex, :req, "0.5.14", "521b449fa0bf275e6d034c05f29bec21789a0d6cd6f7a1c326c7bee642bf6e07", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "b7b15692071d556c73432c7797aa7e96b51d1a2db76f746b976edef95c930021"}, + "req": {:hex, :req, "0.5.15", "662020efb6ea60b9f0e0fac9be88cd7558b53fe51155a2d9899de594f9906ba9", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "a6513a35fad65467893ced9785457e91693352c70b58bbc045b47e5eb2ef0c53"}, "rewrite": {:hex, :rewrite, "1.1.2", "f5a5d10f5fed1491a6ff48e078d4585882695962ccc9e6c779bae025d1f92eda", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "7f8b94b1e3528d0a47b3e8b7bfeca559d2948a65fa7418a9ad7d7712703d39d4"}, "sourceror": {:hex, :sourceror, "1.10.0", "38397dedbbc286966ec48c7af13e228b171332be1ad731974438c77791945ce9", [:mix], [], "hexpm", "29dbdfc92e04569c9d8e6efdc422fc1d815f4bd0055dc7c51b8800fb75c4b3f1"}, "spitfire": {:hex, :spitfire, "0.2.1", "29e154873f05444669c7453d3d931820822cbca5170e88f0f8faa1de74a79b47", [:mix], [], "hexpm", "6eeed75054a38341b2e1814d41bb0a250564092358de2669fdb57ff88141d91b"},