From cef10036fbcd53160663df7fc7c15d481b3679db Mon Sep 17 00:00:00 2001 From: Juraj Paluba Date: Sat, 20 Sep 2025 13:31:49 +0200 Subject: [PATCH 1/6] refactor: get rid of umbrella Umbrella is suitable for projects that compile only certain applications when releasing. Even though it may seem that dbreplex is such project, it is not. Every release will contian all publishers/subscribers. Another reasons for switching from the umbrella structure are nicely summarized here: https://elixirforum.com/t/migrating-off-of-an-umbrella-app-structure/53735/6 --- .github/workflows/nightly-integration.yml | 3 - apps/core/.formatter.exs | 4 - apps/core/.gitignore | 23 -- apps/core/README.md | 39 --- apps/core/mix.exs | 33 --- apps/core/test/test_helper.exs | 1 - apps/file_publisher/.formatter.exs | 4 - apps/file_publisher/.gitignore | 23 -- apps/file_publisher/test/test_helper.exs | 1 - apps/main_app/.formatter.exs | 4 - apps/main_app/.gitignore | 23 -- apps/main_app/README.md | 8 - apps/main_app/mix.exs | 39 --- apps/pg_subscriber/.formatter.exs | 4 - apps/pg_subscriber/.gitignore | 23 -- apps/pg_subscriber/README.md | 17 -- apps/pg_subscriber/mix.exs | 32 --- apps/pg_subscriber/test/assets/pg_dump.sql | 258 ------------------ apps/pg_subscriber/test/test_helper.exs | 1 - .../example => example}/docker-compose.yml | 0 .../example => example}/init.sql | 0 .../example => example}/pg_hba.conf | 0 .../example => example}/postgresql.conf | 0 {apps/file_publisher => lib}/README.md | 0 {apps/core/lib => lib}/core.ex | 0 .../core/lib => lib}/core/messages/column.ex | 0 .../core/lib => lib}/core/messages/delete.ex | 0 .../core/lib => lib}/core/messages/insert.ex | 0 .../core/messages/message_protocol.ex | 0 .../core/lib => lib}/core/messages/update.ex | 0 .../lib => lib}/core/publisher_contract.ex | 0 .../lib/main_app.ex => lib/dbreplex.ex | 2 +- .../lib => lib}/file_publisher.ex | 0 .../lib => lib}/file_publisher/serializer.ex | 0 {apps/file_publisher => lib}/mix.exs | 0 .../lib => lib}/pg_subscriber/column.ex | 0 .../lib => lib}/pg_subscriber/column_meta.ex | 0 .../lib => lib}/pg_subscriber/handler.ex | 0 .../pg_subscriber/messages/delete.ex | 0 .../pg_subscriber/messages/insert.ex | 0 .../messages/message_behaviour.ex | 0 .../pg_subscriber/messages/relation.ex | 0 .../pg_subscriber/messages/update.ex | 0 .../pg_subscriber/pg_dump_parser.ex | 0 .../lib => lib}/pg_subscriber/pg_repl.ex | 0 .../pg_subscriber/relation_store.ex | 0 .../lib => lib}/pg_subscriber/tuple_data.ex | 0 .../lib => lib}/pg_subscriber/utils.ex | 0 {apps/pg_subscriber/lib => lib}/subscriber.ex | 0 mix.exs | 13 +- .../expected_file_publisher_content.txt | 0 {apps/main_app/test => test}/assets/init.sql | 0 .../main_app/test => test}/assets/pg_dump.sql | 0 {apps/core/test => test}/core_test.exs | 0 .../dbreplex_test.exs | 4 +- .../test => test}/file_publisher_test.exs | 0 {apps/pg_subscriber/test => test}/handler.exs | 0 .../test => test}/pg_dump_parser_test.exs | 4 +- {apps/main_app/test => test}/test_helper.exs | 0 .../test => test}/tuple_data_test.exs | 0 60 files changed, 16 insertions(+), 547 deletions(-) delete mode 100644 apps/core/.formatter.exs delete mode 100644 apps/core/.gitignore delete mode 100644 apps/core/README.md delete mode 100644 apps/core/mix.exs delete mode 100644 apps/core/test/test_helper.exs delete mode 100644 apps/file_publisher/.formatter.exs delete mode 100644 apps/file_publisher/.gitignore delete mode 100644 apps/file_publisher/test/test_helper.exs delete mode 100644 apps/main_app/.formatter.exs delete mode 100644 apps/main_app/.gitignore delete mode 100644 apps/main_app/README.md delete mode 100644 apps/main_app/mix.exs delete mode 100644 apps/pg_subscriber/.formatter.exs delete mode 100644 apps/pg_subscriber/.gitignore delete mode 100644 apps/pg_subscriber/README.md delete mode 100644 apps/pg_subscriber/mix.exs delete mode 100644 apps/pg_subscriber/test/assets/pg_dump.sql delete mode 100644 apps/pg_subscriber/test/test_helper.exs rename {apps/pg_subscriber/example => example}/docker-compose.yml (100%) rename {apps/pg_subscriber/example => example}/init.sql (100%) rename {apps/pg_subscriber/example => example}/pg_hba.conf (100%) rename {apps/pg_subscriber/example => example}/postgresql.conf (100%) rename {apps/file_publisher => lib}/README.md (100%) rename {apps/core/lib => lib}/core.ex (100%) rename {apps/core/lib => lib}/core/messages/column.ex (100%) rename {apps/core/lib => lib}/core/messages/delete.ex (100%) rename {apps/core/lib => lib}/core/messages/insert.ex (100%) rename {apps/core/lib => lib}/core/messages/message_protocol.ex (100%) rename {apps/core/lib => lib}/core/messages/update.ex (100%) rename {apps/core/lib => lib}/core/publisher_contract.ex (100%) rename apps/main_app/lib/main_app.ex => lib/dbreplex.ex (95%) rename {apps/file_publisher/lib => lib}/file_publisher.ex (100%) rename {apps/file_publisher/lib => lib}/file_publisher/serializer.ex (100%) rename {apps/file_publisher => lib}/mix.exs (100%) rename {apps/pg_subscriber/lib => lib}/pg_subscriber/column.ex (100%) rename {apps/pg_subscriber/lib => lib}/pg_subscriber/column_meta.ex (100%) rename {apps/pg_subscriber/lib => lib}/pg_subscriber/handler.ex (100%) rename {apps/pg_subscriber/lib => lib}/pg_subscriber/messages/delete.ex (100%) rename {apps/pg_subscriber/lib => lib}/pg_subscriber/messages/insert.ex (100%) rename {apps/pg_subscriber/lib => lib}/pg_subscriber/messages/message_behaviour.ex (100%) rename {apps/pg_subscriber/lib => lib}/pg_subscriber/messages/relation.ex (100%) rename {apps/pg_subscriber/lib => lib}/pg_subscriber/messages/update.ex (100%) rename {apps/pg_subscriber/lib => lib}/pg_subscriber/pg_dump_parser.ex (100%) rename {apps/pg_subscriber/lib => lib}/pg_subscriber/pg_repl.ex (100%) rename {apps/pg_subscriber/lib => lib}/pg_subscriber/relation_store.ex (100%) rename {apps/pg_subscriber/lib => lib}/pg_subscriber/tuple_data.ex (100%) rename {apps/pg_subscriber/lib => lib}/pg_subscriber/utils.ex (100%) rename {apps/pg_subscriber/lib => lib}/subscriber.ex (100%) rename {apps/main_app/test => test}/assets/expected_file_publisher_content.txt (100%) rename {apps/main_app/test => test}/assets/init.sql (100%) rename {apps/main_app/test => test}/assets/pg_dump.sql (100%) rename {apps/core/test => test}/core_test.exs (100%) rename apps/main_app/test/main_app_test.exs => test/dbreplex_test.exs (97%) rename {apps/file_publisher/test => test}/file_publisher_test.exs (100%) rename {apps/pg_subscriber/test => test}/handler.exs (100%) rename {apps/pg_subscriber/test => test}/pg_dump_parser_test.exs (95%) rename {apps/main_app/test => test}/test_helper.exs (100%) rename {apps/pg_subscriber/test => test}/tuple_data_test.exs (100%) diff --git a/.github/workflows/nightly-integration.yml b/.github/workflows/nightly-integration.yml index 738af33..c59421f 100644 --- a/.github/workflows/nightly-integration.yml +++ b/.github/workflows/nightly-integration.yml @@ -10,9 +10,6 @@ jobs: integration_test: name: Integration Tests runs-on: ubuntu-latest - defaults: - run: - working-directory: ./apps/main_app env: MIX_ENV: test strategy: diff --git a/apps/core/.formatter.exs b/apps/core/.formatter.exs deleted file mode 100644 index d2cda26..0000000 --- a/apps/core/.formatter.exs +++ /dev/null @@ -1,4 +0,0 @@ -# Used by "mix format" -[ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] -] diff --git a/apps/core/.gitignore b/apps/core/.gitignore deleted file mode 100644 index 17ce19a..0000000 --- a/apps/core/.gitignore +++ /dev/null @@ -1,23 +0,0 @@ -# The directory Mix will write compiled artifacts to. -/_build/ - -# If you run "mix test --cover", coverage assets end up here. -/cover/ - -# The directory Mix downloads your dependencies sources to. -/deps/ - -# Where third-party dependencies like ExDoc output generated docs. -/doc/ - -# If the VM crashes, it generates a dump, let's ignore it too. -erl_crash.dump - -# Also ignore archive artifacts (built via "mix archive.build"). -*.ez - -# Ignore package tarball (built via "mix hex.build"). -core-*.tar - -# Temporary files, for example, from tests. -/tmp/ diff --git a/apps/core/README.md b/apps/core/README.md deleted file mode 100644 index 4ea34af..0000000 --- a/apps/core/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# Core - -**Core** is the shared library for defining the internal data representation and contracts used across the replication system. It provides: - -- Canonical schemas for replication messages received from databases (e.g., PostgreSQL, MySQL). -- Contracts (interfaces) that publisher apps can implement to handle replicated messages. -- Utilities for encoding, decoding, and working with messages in a uniform way. - -## Installation - -This app is designed to be used within the umbrella. - -## Usage - -### Using the message structs - -```elixir -alias Core.Messages.Insert - -%Insert{ - table: "users", - columns: %{"id" => 1, "name" => "Alice"} -} -``` - -### Implementing the publisher contract -In your custom publisher app, you can implement the Core.PublisherContract behaviour: -```elixir -defmodule MyCustomPublisher do - @behaviour Core.PublisherContract - - @impl true - def handle_message(message) do - # Do something with the message, e.g., send to a queue, update a cache, or update a database - IO.inspect(message, label: "Received replication message") - :ok - end -end -``` diff --git a/apps/core/mix.exs b/apps/core/mix.exs deleted file mode 100644 index 3a01864..0000000 --- a/apps/core/mix.exs +++ /dev/null @@ -1,33 +0,0 @@ -defmodule Core.MixProject do - use Mix.Project - - def project do - [ - app: :core, - version: "0.1.0", - build_path: "../../_build", - config_path: "../../config/config.exs", - deps_path: "../../deps", - lockfile: "../../mix.lock", - elixir: "~> 1.18", - start_permanent: Mix.env() == :prod, - deps: deps() - ] - end - - # Run "mix help compile.app" to learn about applications. - def application do - [ - extra_applications: [:logger] - ] - end - - # Run "mix help deps" to learn about dependencies. - defp deps do - [ - # {:dep_from_hexpm, "~> 0.3.0"}, - # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}, - # {:sibling_app_in_umbrella, in_umbrella: true} - ] - end -end diff --git a/apps/core/test/test_helper.exs b/apps/core/test/test_helper.exs deleted file mode 100644 index 869559e..0000000 --- a/apps/core/test/test_helper.exs +++ /dev/null @@ -1 +0,0 @@ -ExUnit.start() diff --git a/apps/file_publisher/.formatter.exs b/apps/file_publisher/.formatter.exs deleted file mode 100644 index d2cda26..0000000 --- a/apps/file_publisher/.formatter.exs +++ /dev/null @@ -1,4 +0,0 @@ -# Used by "mix format" -[ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] -] diff --git a/apps/file_publisher/.gitignore b/apps/file_publisher/.gitignore deleted file mode 100644 index 2c0df54..0000000 --- a/apps/file_publisher/.gitignore +++ /dev/null @@ -1,23 +0,0 @@ -# The directory Mix will write compiled artifacts to. -/_build/ - -# If you run "mix test --cover", coverage assets end up here. -/cover/ - -# The directory Mix downloads your dependencies sources to. -/deps/ - -# Where third-party dependencies like ExDoc output generated docs. -/doc/ - -# If the VM crashes, it generates a dump, let's ignore it too. -erl_crash.dump - -# Also ignore archive artifacts (built via "mix archive.build"). -*.ez - -# Ignore package tarball (built via "mix hex.build"). -file_publisher-*.tar - -# Temporary files, for example, from tests. -/tmp/ diff --git a/apps/file_publisher/test/test_helper.exs b/apps/file_publisher/test/test_helper.exs deleted file mode 100644 index 869559e..0000000 --- a/apps/file_publisher/test/test_helper.exs +++ /dev/null @@ -1 +0,0 @@ -ExUnit.start() diff --git a/apps/main_app/.formatter.exs b/apps/main_app/.formatter.exs deleted file mode 100644 index d2cda26..0000000 --- a/apps/main_app/.formatter.exs +++ /dev/null @@ -1,4 +0,0 @@ -# Used by "mix format" -[ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] -] diff --git a/apps/main_app/.gitignore b/apps/main_app/.gitignore deleted file mode 100644 index 329b773..0000000 --- a/apps/main_app/.gitignore +++ /dev/null @@ -1,23 +0,0 @@ -# The directory Mix will write compiled artifacts to. -/_build/ - -# If you run "mix test --cover", coverage assets end up here. -/cover/ - -# The directory Mix downloads your dependencies sources to. -/deps/ - -# Where third-party dependencies like ExDoc output generated docs. -/doc/ - -# If the VM crashes, it generates a dump, let's ignore it too. -erl_crash.dump - -# Also ignore archive artifacts (built via "mix archive.build"). -*.ez - -# Ignore package tarball (built via "mix hex.build"). -main_app-*.tar - -# Temporary files, for example, from tests. -/tmp/ diff --git a/apps/main_app/README.md b/apps/main_app/README.md deleted file mode 100644 index 629b26b..0000000 --- a/apps/main_app/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# MainApp - -MainApp is the orchestrator that runs configured `Subscriber` and `Publisher`. - -If everything is properly configured, run the application: -```bash -mix run --no-halt -``` diff --git a/apps/main_app/mix.exs b/apps/main_app/mix.exs deleted file mode 100644 index f830dd7..0000000 --- a/apps/main_app/mix.exs +++ /dev/null @@ -1,39 +0,0 @@ -defmodule MainApp.MixProject do - use Mix.Project - - def project do - [ - app: :main_app, - version: "0.1.0", - build_path: "../../_build", - config_path: "../../config/config.exs", - deps_path: "../../deps", - lockfile: "../../mix.lock", - elixir: "~> 1.18", - start_permanent: Mix.env() == :prod, - deps: deps() - ] - end - - # Run "mix help compile.app" to learn about applications. - def application do - [ - extra_applications: [:logger], - mod: {MainApp, []} - ] - end - - # Run "mix help deps" to learn about dependencies. - defp deps do - [ - # Core - {:core, in_umbrella: true}, - - # Subscribers - {:pg_subscriber, in_umbrella: true}, - - # Publishers - {:file_publisher, in_umbrella: true} - ] - end -end diff --git a/apps/pg_subscriber/.formatter.exs b/apps/pg_subscriber/.formatter.exs deleted file mode 100644 index d2cda26..0000000 --- a/apps/pg_subscriber/.formatter.exs +++ /dev/null @@ -1,4 +0,0 @@ -# Used by "mix format" -[ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] -] diff --git a/apps/pg_subscriber/.gitignore b/apps/pg_subscriber/.gitignore deleted file mode 100644 index 071657a..0000000 --- a/apps/pg_subscriber/.gitignore +++ /dev/null @@ -1,23 +0,0 @@ -# The directory Mix will write compiled artifacts to. -/_build/ - -# If you run "mix test --cover", coverage assets end up here. -/cover/ - -# The directory Mix downloads your dependencies sources to. -/deps/ - -# Where third-party dependencies like ExDoc output generated docs. -/doc/ - -# If the VM crashes, it generates a dump, let's ignore it too. -erl_crash.dump - -# Also ignore archive artifacts (built via "mix archive.build"). -*.ez - -# Ignore package tarball (built via "mix hex.build"). -pg_subscriptor-*.tar - -# Temporary files, for example, from tests. -/tmp/ diff --git a/apps/pg_subscriber/README.md b/apps/pg_subscriber/README.md deleted file mode 100644 index 2493296..0000000 --- a/apps/pg_subscriber/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# PgSubscriptor - -## Howw to run -Currently the `pg_subscriptor` app is connecting to the Postgres instance running on `127.0.0.1:5432`. It expects that -the database is `postgres`, user `postgres`, and password `postgres`. You need to allow `replication` connection in -`pg_hba` file (checkout [example](./example) directory). - -The Postgres instance must be configured with `wal_level >= logical` and you need to install -[wal2json plugin](https://github.com/eulerto/wal2json) on the instance server. Note, you need to install the plugin -for the Postgres version 16. - -Upon start, the client opens connection to the Postgres database, configures so called `REPLICATION SLOT`, starts the -replication and listens on incoming changes. Simply execute insert/delete statement (you can use [pg_randomizer](https://github.com/revolko/pg-randomizer)) and incoming changes will be printed to the console in json format. - - -## Discussion -I am not sure if `wal2json` is the best plugin to use. By default, Postgres has `pgoutput` plugin for replication, but that yeilded not readable binary result. If we are able to figure out the binary encoding, `pgoutput` might be more efficient than using `json` format. diff --git a/apps/pg_subscriber/mix.exs b/apps/pg_subscriber/mix.exs deleted file mode 100644 index 2dc4540..0000000 --- a/apps/pg_subscriber/mix.exs +++ /dev/null @@ -1,32 +0,0 @@ -defmodule PgSubscriber.MixProject do - use Mix.Project - - def project do - [ - app: :pg_subscriber, - version: "0.1.0", - build_path: "../../_build", - config_path: "../../config/config.exs", - deps_path: "../../deps", - lockfile: "../../mix.lock", - elixir: "~> 1.18", - start_permanent: Mix.env() == :prod, - deps: deps() - ] - end - - # Run "mix help compile.app" to learn about applications. - def application do - [ - extra_applications: [:logger] - ] - end - - # Run "mix help deps" to learn about dependencies. - defp deps do - [ - {:core, in_umbrella: true}, - {:postgrex, "~> 0.20.0"} - ] - end -end diff --git a/apps/pg_subscriber/test/assets/pg_dump.sql b/apps/pg_subscriber/test/assets/pg_dump.sql deleted file mode 100644 index e1f26ee..0000000 --- a/apps/pg_subscriber/test/assets/pg_dump.sql +++ /dev/null @@ -1,258 +0,0 @@ --- --- PostgreSQL database dump --- - --- Dumped from database version 17.5 (Debian 17.5-1.pgdg120+1) --- Dumped by pg_dump version 17.5 (Debian 17.5-1.pgdg120+1) - -SET statement_timeout = 0; -SET lock_timeout = 0; -SET idle_in_transaction_session_timeout = 0; -SET transaction_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SELECT pg_catalog.set_config('search_path', '', false); -SET check_function_bodies = false; -SET xmloption = content; -SET client_min_messages = warning; -SET row_security = off; - -SET default_tablespace = ''; - -SET default_table_access_method = heap; - --- --- Name: distributors; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.distributors ( - did integer NOT NULL, - name character varying(255) NOT NULL -); - -ALTER TABLE ONLY public.distributors REPLICA IDENTITY FULL; - - -ALTER TABLE public.distributors OWNER TO postgres; - --- --- Name: films; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.films ( - code integer NOT NULL, - title character varying(255) NOT NULL -); - -ALTER TABLE ONLY public.films REPLICA IDENTITY FULL; - - -ALTER TABLE public.films OWNER TO postgres; - --- --- Name: test_foreign; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.test_foreign ( - id integer NOT NULL, - test_primary_id integer NOT NULL -); - - -ALTER TABLE public.test_foreign OWNER TO postgres; - --- --- Name: test_foreign_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres --- - -CREATE SEQUENCE public.test_foreign_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - -ALTER SEQUENCE public.test_foreign_id_seq OWNER TO postgres; - --- --- Name: test_foreign_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres --- - -ALTER SEQUENCE public.test_foreign_id_seq OWNED BY public.test_foreign.id; - - --- --- Name: test_primary; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.test_primary ( - first_name character varying(255), - last_name character varying(255) NOT NULL, - id integer NOT NULL -); - - -ALTER TABLE public.test_primary OWNER TO postgres; - --- --- Name: test_primary_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres --- - -CREATE SEQUENCE public.test_primary_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - -ALTER SEQUENCE public.test_primary_id_seq OWNER TO postgres; - --- --- Name: test_primary_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres --- - -ALTER SEQUENCE public.test_primary_id_seq OWNED BY public.test_primary.id; - - --- --- Name: test_foreign id; Type: DEFAULT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.test_foreign ALTER COLUMN id SET DEFAULT nextval('public.test_foreign_id_seq'::regclass); - - --- --- Name: test_primary id; Type: DEFAULT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.test_primary ALTER COLUMN id SET DEFAULT nextval('public.test_primary_id_seq'::regclass); - - --- --- Data for Name: distributors; Type: TABLE DATA; Schema: public; Owner: postgres --- - -INSERT INTO public.distributors (did, name) VALUES (55, 'f033ab37c30201f73f142449d037028d'); -INSERT INTO public.distributors (did, name) VALUES (11, 'ea5d2f1c4608232e07d3aa3d998e5135'); -INSERT INTO public.distributors (did, name) VALUES (23, '17e62166fc8586dfa4d1bc0e1742c08b'); -INSERT INTO public.distributors (did, name) VALUES (1, 'e4da3b7fbbce2345d7772b0674a318d5'); -INSERT INTO public.distributors (did, name) VALUES (94, 'e2ef524fbf3d9fe611d5a8e90fefdc9c'); -INSERT INTO public.distributors (did, name) VALUES (10, 'a1d0c6e83f027327d8461063f4ac58a6'); -INSERT INTO public.distributors (did, name) VALUES (17, '26657d5ff9020d2abefe558796b99584'); -INSERT INTO public.distributors (did, name) VALUES (33, 'ac627ab1ccbdb62ec96e702f07f6425b'); -INSERT INTO public.distributors (did, name) VALUES (46, '8e296a067a37563370ded05f5a3bf3ec'); -INSERT INTO public.distributors (did, name) VALUES (29, 'c7e1249ffc03eb9ded908c236bd1996d'); - - --- --- Data for Name: films; Type: TABLE DATA; Schema: public; Owner: postgres --- - -INSERT INTO public.films (code, title) VALUES (74, 'd3d9446802a44259755d38e6d163e820'); -INSERT INTO public.films (code, title) VALUES (64, 'e2ef524fbf3d9fe611d5a8e90fefdc9c'); -INSERT INTO public.films (code, title) VALUES (36, 'ac627ab1ccbdb62ec96e702f07f6425b'); -INSERT INTO public.films (code, title) VALUES (71, '34173cb38f07f89ddbebc2ac9128303f'); -INSERT INTO public.films (code, title) VALUES (31, '093f65e080a295f8076b1c5722a46aa2'); -INSERT INTO public.films (code, title) VALUES (85, '19ca14e7ea6328a42e0eb13d585e4c22'); -INSERT INTO public.films (code, title) VALUES (74, '98f13708210194c475687be6106a3b84'); -INSERT INTO public.films (code, title) VALUES (69, 'f899139df5e1059396431415e770c6dd'); -INSERT INTO public.films (code, title) VALUES (65, '6364d3f0f495b6ab9dcf8d3b5c6e0b01'); -INSERT INTO public.films (code, title) VALUES (38, '8f14e45fceea167a5a36dedd4bea2543'); - - --- --- Data for Name: test_foreign; Type: TABLE DATA; Schema: public; Owner: postgres --- - - - --- --- Data for Name: test_primary; Type: TABLE DATA; Schema: public; Owner: postgres --- - -INSERT INTO public.test_primary (first_name, last_name, id) VALUES ('ja', 'on', 1); - - --- --- Name: test_foreign_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres --- - -SELECT pg_catalog.setval('public.test_foreign_id_seq', 1, false); - - --- --- Name: test_primary_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres --- - -SELECT pg_catalog.setval('public.test_primary_id_seq', 1, true); - - --- --- Name: test_foreign test_foreign_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.test_foreign - ADD CONSTRAINT test_foreign_pkey PRIMARY KEY (id); - - --- --- Name: test_primary test_primary_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.test_primary - ADD CONSTRAINT test_primary_pkey PRIMARY KEY (id); - - --- --- Name: test_foreign test_foreign_test_primary_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.test_foreign - ADD CONSTRAINT test_foreign_test_primary_id_fkey FOREIGN KEY (test_primary_id) REFERENCES public.test_primary(id) ON DELETE CASCADE; - - --- --- Name: postgrex_example; Type: PUBLICATION; Schema: -; Owner: postgres --- - -CREATE PUBLICATION postgrex_example WITH (publish = 'insert, update, delete, truncate'); - - -ALTER PUBLICATION postgrex_example OWNER TO postgres; - --- --- Name: postgrex_example distributors; Type: PUBLICATION TABLE; Schema: public; Owner: postgres --- - -ALTER PUBLICATION postgrex_example ADD TABLE ONLY public.distributors; - - --- --- Name: postgrex_example films; Type: PUBLICATION TABLE; Schema: public; Owner: postgres --- - -ALTER PUBLICATION postgrex_example ADD TABLE ONLY public.films; - - --- --- Name: postgrex_example test_foreign; Type: PUBLICATION TABLE; Schema: public; Owner: postgres --- - -ALTER PUBLICATION postgrex_example ADD TABLE ONLY public.test_foreign; - - --- --- Name: postgrex_example test_primary; Type: PUBLICATION TABLE; Schema: public; Owner: postgres --- - -ALTER PUBLICATION postgrex_example ADD TABLE ONLY public.test_primary; - - --- --- PostgreSQL database dump complete --- - diff --git a/apps/pg_subscriber/test/test_helper.exs b/apps/pg_subscriber/test/test_helper.exs deleted file mode 100644 index 869559e..0000000 --- a/apps/pg_subscriber/test/test_helper.exs +++ /dev/null @@ -1 +0,0 @@ -ExUnit.start() diff --git a/apps/pg_subscriber/example/docker-compose.yml b/example/docker-compose.yml similarity index 100% rename from apps/pg_subscriber/example/docker-compose.yml rename to example/docker-compose.yml diff --git a/apps/pg_subscriber/example/init.sql b/example/init.sql similarity index 100% rename from apps/pg_subscriber/example/init.sql rename to example/init.sql diff --git a/apps/pg_subscriber/example/pg_hba.conf b/example/pg_hba.conf similarity index 100% rename from apps/pg_subscriber/example/pg_hba.conf rename to example/pg_hba.conf diff --git a/apps/pg_subscriber/example/postgresql.conf b/example/postgresql.conf similarity index 100% rename from apps/pg_subscriber/example/postgresql.conf rename to example/postgresql.conf diff --git a/apps/file_publisher/README.md b/lib/README.md similarity index 100% rename from apps/file_publisher/README.md rename to lib/README.md diff --git a/apps/core/lib/core.ex b/lib/core.ex similarity index 100% rename from apps/core/lib/core.ex rename to lib/core.ex diff --git a/apps/core/lib/core/messages/column.ex b/lib/core/messages/column.ex similarity index 100% rename from apps/core/lib/core/messages/column.ex rename to lib/core/messages/column.ex diff --git a/apps/core/lib/core/messages/delete.ex b/lib/core/messages/delete.ex similarity index 100% rename from apps/core/lib/core/messages/delete.ex rename to lib/core/messages/delete.ex diff --git a/apps/core/lib/core/messages/insert.ex b/lib/core/messages/insert.ex similarity index 100% rename from apps/core/lib/core/messages/insert.ex rename to lib/core/messages/insert.ex diff --git a/apps/core/lib/core/messages/message_protocol.ex b/lib/core/messages/message_protocol.ex similarity index 100% rename from apps/core/lib/core/messages/message_protocol.ex rename to lib/core/messages/message_protocol.ex diff --git a/apps/core/lib/core/messages/update.ex b/lib/core/messages/update.ex similarity index 100% rename from apps/core/lib/core/messages/update.ex rename to lib/core/messages/update.ex diff --git a/apps/core/lib/core/publisher_contract.ex b/lib/core/publisher_contract.ex similarity index 100% rename from apps/core/lib/core/publisher_contract.ex rename to lib/core/publisher_contract.ex diff --git a/apps/main_app/lib/main_app.ex b/lib/dbreplex.ex similarity index 95% rename from apps/main_app/lib/main_app.ex rename to lib/dbreplex.ex index f29f4e0..0852510 100644 --- a/apps/main_app/lib/main_app.ex +++ b/lib/dbreplex.ex @@ -1,4 +1,4 @@ -defmodule MainApp do +defmodule DBReplex do @moduledoc false use Application diff --git a/apps/file_publisher/lib/file_publisher.ex b/lib/file_publisher.ex similarity index 100% rename from apps/file_publisher/lib/file_publisher.ex rename to lib/file_publisher.ex diff --git a/apps/file_publisher/lib/file_publisher/serializer.ex b/lib/file_publisher/serializer.ex similarity index 100% rename from apps/file_publisher/lib/file_publisher/serializer.ex rename to lib/file_publisher/serializer.ex diff --git a/apps/file_publisher/mix.exs b/lib/mix.exs similarity index 100% rename from apps/file_publisher/mix.exs rename to lib/mix.exs diff --git a/apps/pg_subscriber/lib/pg_subscriber/column.ex b/lib/pg_subscriber/column.ex similarity index 100% rename from apps/pg_subscriber/lib/pg_subscriber/column.ex rename to lib/pg_subscriber/column.ex diff --git a/apps/pg_subscriber/lib/pg_subscriber/column_meta.ex b/lib/pg_subscriber/column_meta.ex similarity index 100% rename from apps/pg_subscriber/lib/pg_subscriber/column_meta.ex rename to lib/pg_subscriber/column_meta.ex diff --git a/apps/pg_subscriber/lib/pg_subscriber/handler.ex b/lib/pg_subscriber/handler.ex similarity index 100% rename from apps/pg_subscriber/lib/pg_subscriber/handler.ex rename to lib/pg_subscriber/handler.ex diff --git a/apps/pg_subscriber/lib/pg_subscriber/messages/delete.ex b/lib/pg_subscriber/messages/delete.ex similarity index 100% rename from apps/pg_subscriber/lib/pg_subscriber/messages/delete.ex rename to lib/pg_subscriber/messages/delete.ex diff --git a/apps/pg_subscriber/lib/pg_subscriber/messages/insert.ex b/lib/pg_subscriber/messages/insert.ex similarity index 100% rename from apps/pg_subscriber/lib/pg_subscriber/messages/insert.ex rename to lib/pg_subscriber/messages/insert.ex diff --git a/apps/pg_subscriber/lib/pg_subscriber/messages/message_behaviour.ex b/lib/pg_subscriber/messages/message_behaviour.ex similarity index 100% rename from apps/pg_subscriber/lib/pg_subscriber/messages/message_behaviour.ex rename to lib/pg_subscriber/messages/message_behaviour.ex diff --git a/apps/pg_subscriber/lib/pg_subscriber/messages/relation.ex b/lib/pg_subscriber/messages/relation.ex similarity index 100% rename from apps/pg_subscriber/lib/pg_subscriber/messages/relation.ex rename to lib/pg_subscriber/messages/relation.ex diff --git a/apps/pg_subscriber/lib/pg_subscriber/messages/update.ex b/lib/pg_subscriber/messages/update.ex similarity index 100% rename from apps/pg_subscriber/lib/pg_subscriber/messages/update.ex rename to lib/pg_subscriber/messages/update.ex diff --git a/apps/pg_subscriber/lib/pg_subscriber/pg_dump_parser.ex b/lib/pg_subscriber/pg_dump_parser.ex similarity index 100% rename from apps/pg_subscriber/lib/pg_subscriber/pg_dump_parser.ex rename to lib/pg_subscriber/pg_dump_parser.ex diff --git a/apps/pg_subscriber/lib/pg_subscriber/pg_repl.ex b/lib/pg_subscriber/pg_repl.ex similarity index 100% rename from apps/pg_subscriber/lib/pg_subscriber/pg_repl.ex rename to lib/pg_subscriber/pg_repl.ex diff --git a/apps/pg_subscriber/lib/pg_subscriber/relation_store.ex b/lib/pg_subscriber/relation_store.ex similarity index 100% rename from apps/pg_subscriber/lib/pg_subscriber/relation_store.ex rename to lib/pg_subscriber/relation_store.ex diff --git a/apps/pg_subscriber/lib/pg_subscriber/tuple_data.ex b/lib/pg_subscriber/tuple_data.ex similarity index 100% rename from apps/pg_subscriber/lib/pg_subscriber/tuple_data.ex rename to lib/pg_subscriber/tuple_data.ex diff --git a/apps/pg_subscriber/lib/pg_subscriber/utils.ex b/lib/pg_subscriber/utils.ex similarity index 100% rename from apps/pg_subscriber/lib/pg_subscriber/utils.ex rename to lib/pg_subscriber/utils.ex diff --git a/apps/pg_subscriber/lib/subscriber.ex b/lib/subscriber.ex similarity index 100% rename from apps/pg_subscriber/lib/subscriber.ex rename to lib/subscriber.ex diff --git a/mix.exs b/mix.exs index 0610efd..0cbe656 100644 --- a/mix.exs +++ b/mix.exs @@ -3,13 +3,21 @@ defmodule DbSubscriptor.MixProject do def project do [ - apps_path: "apps", + app: :dbreplex, version: "0.1.0", start_permanent: Mix.env() == :prod, + name: "DBReplex", deps: deps() ] end + def application do + [ + extra_applications: [:logger], + mod: {DBReplex, []} + ] + end + # Dependencies listed here are available only for this # project and cannot be accessed from applications inside # the apps folder. @@ -17,7 +25,8 @@ defmodule DbSubscriptor.MixProject do # Run "mix help deps" for examples and options. defp deps do [ - {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false} + {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, + {:postgrex, "~> 0.20.0"} ] end end diff --git a/apps/main_app/test/assets/expected_file_publisher_content.txt b/test/assets/expected_file_publisher_content.txt similarity index 100% rename from apps/main_app/test/assets/expected_file_publisher_content.txt rename to test/assets/expected_file_publisher_content.txt diff --git a/apps/main_app/test/assets/init.sql b/test/assets/init.sql similarity index 100% rename from apps/main_app/test/assets/init.sql rename to test/assets/init.sql diff --git a/apps/main_app/test/assets/pg_dump.sql b/test/assets/pg_dump.sql similarity index 100% rename from apps/main_app/test/assets/pg_dump.sql rename to test/assets/pg_dump.sql diff --git a/apps/core/test/core_test.exs b/test/core_test.exs similarity index 100% rename from apps/core/test/core_test.exs rename to test/core_test.exs diff --git a/apps/main_app/test/main_app_test.exs b/test/dbreplex_test.exs similarity index 97% rename from apps/main_app/test/main_app_test.exs rename to test/dbreplex_test.exs index 88b6361..4866eb0 100644 --- a/apps/main_app/test/main_app_test.exs +++ b/test/dbreplex_test.exs @@ -1,7 +1,7 @@ -defmodule MainAppTest do +defmodule DBReplexTest do alias PgSubscriber.PgDumpParser use ExUnit.Case - doctest MainApp + doctest DBReplex @file_publisher_target "/tmp/dbreplex-integration.txt" @expected_file_publisher_content "./test/assets/expected_file_publisher_content.txt" diff --git a/apps/file_publisher/test/file_publisher_test.exs b/test/file_publisher_test.exs similarity index 100% rename from apps/file_publisher/test/file_publisher_test.exs rename to test/file_publisher_test.exs diff --git a/apps/pg_subscriber/test/handler.exs b/test/handler.exs similarity index 100% rename from apps/pg_subscriber/test/handler.exs rename to test/handler.exs diff --git a/apps/pg_subscriber/test/pg_dump_parser_test.exs b/test/pg_dump_parser_test.exs similarity index 95% rename from apps/pg_subscriber/test/pg_dump_parser_test.exs rename to test/pg_dump_parser_test.exs index 0edad71..d7f85a2 100644 --- a/apps/pg_subscriber/test/pg_dump_parser_test.exs +++ b/test/pg_dump_parser_test.exs @@ -13,12 +13,12 @@ defmodule PgSubscriber.PgDumpParserTest do test "filter insert", state do inserts = PgDumpParser.filter_inserts(state.pg_dump) - assert(length(inserts) === 21) + assert(length(inserts) === 4) end test "convert to core many inserts", state do inserts = PgDumpParser.filter_inserts(state.pg_dump) |> PgDumpParser.to_core_insert() - assert(length(inserts) === 21) + assert(length(inserts) === 4) assert( Enum.all?(inserts, fn %type{} = insert -> diff --git a/apps/main_app/test/test_helper.exs b/test/test_helper.exs similarity index 100% rename from apps/main_app/test/test_helper.exs rename to test/test_helper.exs diff --git a/apps/pg_subscriber/test/tuple_data_test.exs b/test/tuple_data_test.exs similarity index 100% rename from apps/pg_subscriber/test/tuple_data_test.exs rename to test/tuple_data_test.exs From 64337c58301fec633c468f44b1ea882c8b0ec69b Mon Sep 17 00:00:00 2001 From: Juraj Paluba Date: Sat, 20 Sep 2025 14:37:08 +0200 Subject: [PATCH 2/6] refactor(subscribers): create subscribers folder And move pgsubscriber in the new folder. --- README.md | 6 +++--- example.exs | 2 +- .../postgres}/column.ex | 2 +- .../postgres}/column_meta.ex | 2 +- .../postgres}/handler.ex | 12 ++++++------ .../postgres}/messages/delete.ex | 16 ++++++++-------- .../postgres}/messages/insert.ex | 14 +++++++------- .../postgres}/messages/message_behaviour.ex | 2 +- .../postgres}/messages/relation.ex | 10 +++++----- .../postgres}/messages/update.ex | 16 ++++++++-------- .../postgres}/pg_dump_parser.ex | 2 +- .../postgres}/pg_repl.ex | 4 ++-- .../postgres}/relation_store.ex | 6 +++--- .../postgres/supervisor.ex} | 10 +++++----- .../postgres}/tuple_data.ex | 4 ++-- .../postgres}/utils.ex | 2 +- test/dbreplex_test.exs | 8 ++++---- test/handler.exs | 4 ++-- test/pg_dump_parser_test.exs | 4 ++-- test/tuple_data_test.exs | 4 ++-- 20 files changed, 65 insertions(+), 65 deletions(-) rename lib/{pg_subscriber => subscribers/postgres}/column.ex (97%) rename lib/{pg_subscriber => subscribers/postgres}/column_meta.ex (88%) rename lib/{pg_subscriber => subscribers/postgres}/handler.ex (85%) rename lib/{pg_subscriber => subscribers/postgres}/messages/delete.ex (83%) rename lib/{pg_subscriber => subscribers/postgres}/messages/insert.ex (78%) rename lib/{pg_subscriber => subscribers/postgres}/messages/message_behaviour.ex (81%) rename lib/{pg_subscriber => subscribers/postgres}/messages/relation.ex (87%) rename lib/{pg_subscriber => subscribers/postgres}/messages/update.ex (91%) rename lib/{pg_subscriber => subscribers/postgres}/pg_dump_parser.ex (98%) rename lib/{pg_subscriber => subscribers/postgres}/pg_repl.ex (93%) rename lib/{pg_subscriber => subscribers/postgres}/relation_store.ex (89%) rename lib/{subscriber.ex => subscribers/postgres/supervisor.ex} (54%) rename lib/{pg_subscriber => subscribers/postgres}/tuple_data.ex (90%) rename lib/{pg_subscriber => subscribers/postgres}/utils.ex (96%) diff --git a/README.md b/README.md index 269da35..e910fe6 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,12 @@ Either start the application with iex console: iex -S mix run --no-halt ``` -Or use [example.exs](./example.exs) script to quickly startup PgSubscriber with FilePublisher: +Or use [example.exs](./example.exs) script to quickly startup Subscribers.Postgres with FilePublisher: ```bash iex -S mix run example.exs ``` -`PgSubscriber` requires running Postgres database. Checkout [postgres example](./apps/pg_subscriber/example/) for a quick setup. +`Subscribers.Postgres` requires running Postgres database. Checkout [postgres example](./apps/pg_subscriber/example/) for a quick setup. Make sure that `FilePublisher` points to the existing location of your system (file does not need to exist, but parent directories must). @@ -26,7 +26,7 @@ FilePublisher the statement is saved to a file). ## Subscribers -### PgSubscriber :rocket: +### Subscribers.Postgres :rocket: Subscriber for Postgres database. The subscriber creates logical replication connection to Postgres database. The replication connection makes database to push replication messages to the subscriber (push-based communication). diff --git a/example.exs b/example.exs index c82cd1d..dbb1a9f 100644 --- a/example.exs +++ b/example.exs @@ -1,6 +1,6 @@ DynamicSupervisor.start_child( MainApp.DynamicSupervisor, - {PgSubscriber, + {Subscribers.Postgres, [ repl: [host: "localhost", username: "postgres", database: "postgres", password: "postgres"], handler: [] diff --git a/lib/pg_subscriber/column.ex b/lib/subscribers/postgres/column.ex similarity index 97% rename from lib/pg_subscriber/column.ex rename to lib/subscribers/postgres/column.ex index b477de6..28fe102 100644 --- a/lib/pg_subscriber/column.ex +++ b/lib/subscribers/postgres/column.ex @@ -1,4 +1,4 @@ -defmodule PgSubscriber.Column do +defmodule Subscribers.Postgres.Column do alias __MODULE__ @type kind :: ?n | ?u | ?t | ?b diff --git a/lib/pg_subscriber/column_meta.ex b/lib/subscribers/postgres/column_meta.ex similarity index 88% rename from lib/pg_subscriber/column_meta.ex rename to lib/subscribers/postgres/column_meta.ex index bf9538b..157c4b1 100644 --- a/lib/pg_subscriber/column_meta.ex +++ b/lib/subscribers/postgres/column_meta.ex @@ -1,4 +1,4 @@ -defmodule PgSubscriber.ColumnMeta do +defmodule Subscribers.Postgres.ColumnMeta do @moduledoc """ Struct containing metadata about Postgres columns. """ diff --git a/lib/pg_subscriber/handler.ex b/lib/subscribers/postgres/handler.ex similarity index 85% rename from lib/pg_subscriber/handler.ex rename to lib/subscribers/postgres/handler.ex index 5b7a94a..7a60f3d 100644 --- a/lib/pg_subscriber/handler.ex +++ b/lib/subscribers/postgres/handler.ex @@ -1,13 +1,13 @@ -defmodule PgSubscriber.Handler do +defmodule Subscribers.Postgres.Handler do use GenServer require Logger - alias PgSubscriber.Messages.Insert, as: PgInsert - alias PgSubscriber.RelationStore - alias PgSubscriber.Messages.Relation, as: PgRelation - alias PgSubscriber.Messages.Delete, as: PgDelete + alias Subscribers.Postgres.Messages.Insert, as: PgInsert + alias Subscribers.Postgres.RelationStore + alias Subscribers.Postgres.Messages.Relation, as: PgRelation + alias Subscribers.Postgres.Messages.Delete, as: PgDelete alias Core.Messages.MessageProtocol - alias PgSubscriber.Messages.Update, as: PgUpdate + alias Subscribers.Postgres.Messages.Update, as: PgUpdate def start_link(default) do GenServer.start_link(__MODULE__, default, name: __MODULE__) diff --git a/lib/pg_subscriber/messages/delete.ex b/lib/subscribers/postgres/messages/delete.ex similarity index 83% rename from lib/pg_subscriber/messages/delete.ex rename to lib/subscribers/postgres/messages/delete.ex index 454ebc7..2fb3419 100644 --- a/lib/pg_subscriber/messages/delete.ex +++ b/lib/subscribers/postgres/messages/delete.ex @@ -1,12 +1,12 @@ -defmodule PgSubscriber.Messages.Delete do +defmodule Subscribers.Postgres.Messages.Delete do @moduledoc """ Helper module providing utility functions for handling of DELETE messages. """ require Logger - alias PgSubscriber.Column - alias PgSubscriber.TupleData - alias PgSubscriber.Messages.MessageBehaviour - alias PgSubscriber.Utils + alias Subscribers.Postgres.Column + alias Subscribers.Postgres.TupleData + alias Subscribers.Postgres.Messages.MessageBehaviour + alias Subscribers.Postgres.Utils @behaviour MessageBehaviour @@ -51,11 +51,11 @@ defmodule PgSubscriber.Messages.Delete do end end -defimpl Core.Messages.MessageProtocol, for: PgSubscriber.Messages.Delete do - alias PgSubscriber.RelationStore +defimpl Core.Messages.MessageProtocol, for: Subscribers.Postgres.Messages.Delete do + alias Subscribers.Postgres.RelationStore alias Core.Messages.Delete, as: CoreDelete alias Core.Messages.Column, as: CoreColumn - alias PgSubscriber.Messages.Delete, as: PgDelete + alias Subscribers.Postgres.Messages.Delete, as: PgDelete def to_core_message(%PgDelete{ relation_oid: relation_oid, diff --git a/lib/pg_subscriber/messages/insert.ex b/lib/subscribers/postgres/messages/insert.ex similarity index 78% rename from lib/pg_subscriber/messages/insert.ex rename to lib/subscribers/postgres/messages/insert.ex index e8ec226..8120b1a 100644 --- a/lib/pg_subscriber/messages/insert.ex +++ b/lib/subscribers/postgres/messages/insert.ex @@ -1,12 +1,12 @@ -defmodule PgSubscriber.Messages.Insert do +defmodule Subscribers.Postgres.Messages.Insert do @moduledoc """ Represents Postgres INSERT operation in the database replication stream. """ require Logger - alias PgSubscriber.Messages.MessageBehaviour - alias PgSubscriber.Column - alias PgSubscriber.Utils - alias PgSubscriber.TupleData + alias Subscribers.Postgres.Messages.MessageBehaviour + alias Subscribers.Postgres.Column + alias Subscribers.Postgres.Utils + alias Subscribers.Postgres.TupleData @behaviour MessageBehaviour @@ -39,8 +39,8 @@ defmodule PgSubscriber.Messages.Insert do end end -defimpl Core.Messages.MessageProtocol, for: PgSubscriber.Messages.Insert do - alias PgSubscriber.RelationStore +defimpl Core.Messages.MessageProtocol, for: Subscribers.Postgres.Messages.Insert do + alias Subscribers.Postgres.RelationStore alias Core.Messages.Insert def to_core_message(insert) do diff --git a/lib/pg_subscriber/messages/message_behaviour.ex b/lib/subscribers/postgres/messages/message_behaviour.ex similarity index 81% rename from lib/pg_subscriber/messages/message_behaviour.ex rename to lib/subscribers/postgres/messages/message_behaviour.ex index 0a71606..4fb2667 100644 --- a/lib/pg_subscriber/messages/message_behaviour.ex +++ b/lib/subscribers/postgres/messages/message_behaviour.ex @@ -1,4 +1,4 @@ -defmodule PgSubscriber.Messages.MessageBehaviour do +defmodule Subscribers.Postgres.Messages.MessageBehaviour do @doc """ Converts binary data to the Postgres-specific representation of a message (struct). """ diff --git a/lib/pg_subscriber/messages/relation.ex b/lib/subscribers/postgres/messages/relation.ex similarity index 87% rename from lib/pg_subscriber/messages/relation.ex rename to lib/subscribers/postgres/messages/relation.ex index fec7c44..ef4a38d 100644 --- a/lib/pg_subscriber/messages/relation.ex +++ b/lib/subscribers/postgres/messages/relation.ex @@ -1,13 +1,13 @@ -defmodule PgSubscriber.Messages.Relation do +defmodule Subscribers.Postgres.Messages.Relation do @moduledoc """ Helper module providing utility functions for handling of RELATION messages. """ require Logger - alias PgSubscriber.Messages.Relation - alias PgSubscriber.Messages.MessageBehaviour - alias PgSubscriber.Utils - alias PgSubscriber.ColumnMeta + alias Subscribers.Postgres.Messages.Relation + alias Subscribers.Postgres.Messages.MessageBehaviour + alias Subscribers.Postgres.Utils + alias Subscribers.Postgres.ColumnMeta @behaviour MessageBehaviour diff --git a/lib/pg_subscriber/messages/update.ex b/lib/subscribers/postgres/messages/update.ex similarity index 91% rename from lib/pg_subscriber/messages/update.ex rename to lib/subscribers/postgres/messages/update.ex index 44fcd12..c719d85 100644 --- a/lib/pg_subscriber/messages/update.ex +++ b/lib/subscribers/postgres/messages/update.ex @@ -1,11 +1,11 @@ -defmodule PgSubscriber.Messages.Update do +defmodule Subscribers.Postgres.Messages.Update do @moduledoc """ Helper module providing utility functions for proper work with UPDATE messages. """ - alias PgSubscriber.Messages.MessageBehaviour - alias PgSubscriber.Column - alias PgSubscriber.TupleData - alias PgSubscriber.Utils + alias Subscribers.Postgres.Messages.MessageBehaviour + alias Subscribers.Postgres.Column + alias Subscribers.Postgres.TupleData + alias Subscribers.Postgres.Utils require Logger @behaviour MessageBehaviour @@ -82,12 +82,12 @@ defmodule PgSubscriber.Messages.Update do end end -defimpl Core.Messages.MessageProtocol, for: PgSubscriber.Messages.Update do +defimpl Core.Messages.MessageProtocol, for: Subscribers.Postgres.Messages.Update do require Logger alias Core.Messages.Update alias Core.Messages.Column - alias PgSubscriber.RelationStore - alias PgSubscriber.Messages.Update, as: PgpUpdate + alias Subscribers.Postgres.RelationStore + alias Subscribers.Postgres.Messages.Update, as: PgpUpdate def to_core_message(%PgpUpdate{ relation_oid: relation_oid, diff --git a/lib/pg_subscriber/pg_dump_parser.ex b/lib/subscribers/postgres/pg_dump_parser.ex similarity index 98% rename from lib/pg_subscriber/pg_dump_parser.ex rename to lib/subscribers/postgres/pg_dump_parser.ex index 864ad11..9009e17 100644 --- a/lib/pg_subscriber/pg_dump_parser.ex +++ b/lib/subscribers/postgres/pg_dump_parser.ex @@ -1,4 +1,4 @@ -defmodule PgSubscriber.PgDumpParser do +defmodule Subscribers.Postgres.PgDumpParser do @moduledoc """ Utility module providing functions for parsing pg_dump output. """ diff --git a/lib/pg_subscriber/pg_repl.ex b/lib/subscribers/postgres/pg_repl.ex similarity index 93% rename from lib/pg_subscriber/pg_repl.ex rename to lib/subscribers/postgres/pg_repl.ex index 64f6d10..2fa61ad 100644 --- a/lib/pg_subscriber/pg_repl.ex +++ b/lib/subscribers/postgres/pg_repl.ex @@ -1,4 +1,4 @@ -defmodule PgSubscriber.Repl do +defmodule Subscribers.Postgres.Repl do use Postgrex.ReplicationConnection def start_link(opts) do @@ -32,7 +32,7 @@ defmodule PgSubscriber.Repl do @impl true # https://www.postgresql.org/docs/14/protocol-replication.html def handle_data(<>, state) do - GenServer.cast(PgSubscriber.Handler, {:handle, rest}) + GenServer.cast(Subscribers.Postgres.Handler, {:handle, rest}) {:noreply, state} end diff --git a/lib/pg_subscriber/relation_store.ex b/lib/subscribers/postgres/relation_store.ex similarity index 89% rename from lib/pg_subscriber/relation_store.ex rename to lib/subscribers/postgres/relation_store.ex index b2d2960..ab1f244 100644 --- a/lib/pg_subscriber/relation_store.ex +++ b/lib/subscribers/postgres/relation_store.ex @@ -1,4 +1,4 @@ -defmodule PgSubscriber.RelationStore do +defmodule Subscribers.Postgres.RelationStore do @moduledoc """ Agent storing information about known relations. The replication client receives RELATION message everytime @@ -9,8 +9,8 @@ defmodule PgSubscriber.RelationStore do use Agent - alias PgSubscriber.Utils - alias PgSubscriber.Messages.Relation + alias Subscribers.Postgres.Utils + alias Subscribers.Postgres.Messages.Relation @spec start_link(map()) :: Agent.on_start() def start_link(init_state) do diff --git a/lib/subscriber.ex b/lib/subscribers/postgres/supervisor.ex similarity index 54% rename from lib/subscriber.ex rename to lib/subscribers/postgres/supervisor.ex index 1ddcc90..155c262 100644 --- a/lib/subscriber.ex +++ b/lib/subscribers/postgres/supervisor.ex @@ -1,6 +1,6 @@ -defmodule PgSubscriber do +defmodule Subscribers.Postgres do @moduledoc """ - Supervisor for Postgres subscriber. + Entry point for Postgres subscriber. """ use Supervisor @@ -10,9 +10,9 @@ defmodule PgSubscriber do def init(repl: repl_config, handler: handler_config) do children = [ - {PgSubscriber.RelationStore, %{}}, - {PgSubscriber.Handler, handler_config}, - {PgSubscriber.Repl, repl_config} + {Subscribers.Postgres.RelationStore, %{}}, + {Subscribers.Postgres.Handler, handler_config}, + {Subscribers.Postgres.Repl, repl_config} ] Supervisor.init(children, strategy: :one_for_one) diff --git a/lib/pg_subscriber/tuple_data.ex b/lib/subscribers/postgres/tuple_data.ex similarity index 90% rename from lib/pg_subscriber/tuple_data.ex rename to lib/subscribers/postgres/tuple_data.ex index aedf941..eaf2162 100644 --- a/lib/pg_subscriber/tuple_data.ex +++ b/lib/subscribers/postgres/tuple_data.ex @@ -1,6 +1,6 @@ -defmodule PgSubscriber.TupleData do +defmodule Subscribers.Postgres.TupleData do alias __MODULE__ - alias PgSubscriber.Column + alias Subscribers.Postgres.Column @type t :: %__MODULE__{ num_of_cols: integer, diff --git a/lib/pg_subscriber/utils.ex b/lib/subscribers/postgres/utils.ex similarity index 96% rename from lib/pg_subscriber/utils.ex rename to lib/subscribers/postgres/utils.ex index 5c034e8..cdbf4f6 100644 --- a/lib/pg_subscriber/utils.ex +++ b/lib/subscribers/postgres/utils.ex @@ -1,4 +1,4 @@ -defmodule PgSubscriber.Utils do +defmodule Subscribers.Postgres.Utils do @moduledoc """ Collection of PG helper functions. """ diff --git a/test/dbreplex_test.exs b/test/dbreplex_test.exs index 4866eb0..aea9dbe 100644 --- a/test/dbreplex_test.exs +++ b/test/dbreplex_test.exs @@ -1,5 +1,5 @@ defmodule DBReplexTest do - alias PgSubscriber.PgDumpParser + alias Subscribers.Postgres.PgDumpParser use ExUnit.Case doctest DBReplex @@ -7,7 +7,7 @@ defmodule DBReplexTest do @expected_file_publisher_content "./test/assets/expected_file_publisher_content.txt" @tag integration: true - test "PgSubscriber to FilePublisher" do + test "Subscribers.Postgres to FilePublisher" do # initialization {:ok, file_publisher} = DynamicSupervisor.start_child( @@ -18,7 +18,7 @@ defmodule DBReplexTest do {:ok, pg_subscriber} = DynamicSupervisor.start_child( MainApp.DynamicSupervisor, - {PgSubscriber, + {Subscribers.Postgres, [ repl: [ host: "localhost", @@ -32,7 +32,7 @@ defmodule DBReplexTest do [{_, pg_handler, _, _}] = Enum.filter(Supervisor.which_children(pg_subscriber), fn {module, pid, _, _} -> - module == PgSubscriber.Handler + module == Subscribers.Postgres.Handler end) # test diff --git a/test/handler.exs b/test/handler.exs index a1fe3cd..c57af5b 100644 --- a/test/handler.exs +++ b/test/handler.exs @@ -1,6 +1,6 @@ -defmodule PgSubscriber.HandlerTest do +defmodule Subscribers.Postgres.HandlerTest do use ExUnit.Case - alias PgSubscriber.Handler + alias Subscribers.Postgres.Handler doctest Handler test "insert message" do diff --git a/test/pg_dump_parser_test.exs b/test/pg_dump_parser_test.exs index d7f85a2..b389a52 100644 --- a/test/pg_dump_parser_test.exs +++ b/test/pg_dump_parser_test.exs @@ -1,6 +1,6 @@ -defmodule PgSubscriber.PgDumpParserTest do +defmodule Subscribers.Postgres.PgDumpParserTest do use ExUnit.Case - alias PgSubscriber.PgDumpParser + alias Subscribers.Postgres.PgDumpParser alias Core.Messages.Insert doctest PgDumpParser diff --git a/test/tuple_data_test.exs b/test/tuple_data_test.exs index 6017996..ef43635 100644 --- a/test/tuple_data_test.exs +++ b/test/tuple_data_test.exs @@ -1,7 +1,7 @@ defmodule TupleDataTest do use ExUnit.Case - alias PgSubscriber.TupleData - alias PgSubscriber.Column + alias Subscribers.Postgres.TupleData + alias Subscribers.Postgres.Column doctest TupleData From b000f60deb7a9568630dec80698b22cafe15a9de Mon Sep 17 00:00:00 2001 From: Juraj Paluba Date: Sat, 20 Sep 2025 14:50:06 +0200 Subject: [PATCH 3/6] refactor(publishers): create publishers folder And move FilePublisher to the new folder. --- README.md | 8 ++--- example.exs | 2 +- lib/README.md | 3 -- lib/mix.exs | 31 ------------------- .../file/publisher.ex} | 8 ++--- .../file}/serializer.ex | 10 +++--- test/dbreplex_test.exs | 4 +-- test/file_publisher_test.exs | 8 ++--- 8 files changed, 20 insertions(+), 54 deletions(-) delete mode 100644 lib/README.md delete mode 100644 lib/mix.exs rename lib/{file_publisher.ex => publishers/file/publisher.ex} (84%) rename lib/{file_publisher => publishers/file}/serializer.ex (75%) diff --git a/README.md b/README.md index e910fe6..d075286 100644 --- a/README.md +++ b/README.md @@ -11,18 +11,18 @@ Either start the application with iex console: iex -S mix run --no-halt ``` -Or use [example.exs](./example.exs) script to quickly startup Subscribers.Postgres with FilePublisher: +Or use [example.exs](./example.exs) script to quickly startup Subscribers.Postgres with Publishers.File: ```bash iex -S mix run example.exs ``` `Subscribers.Postgres` requires running Postgres database. Checkout [postgres example](./apps/pg_subscriber/example/) for a quick setup. -Make sure that `FilePublisher` points to the existing location of your system (file does not need to exist, but parent +Make sure that `Publishers.File` points to the existing location of your system (file does not need to exist, but parent directories must). Finally, connect to a subscriber and execute INSERT/UPDATE/DELETE statement. The statement will be logged and saved by all publishers (in case of -FilePublisher the statement is saved to a file). +Publishers.File the statement is saved to a file). ## Subscribers @@ -52,7 +52,7 @@ connection makes database to push replication messages to the subscriber (push-b ## Publishers -### FilePublisher :rocket: +### Publishers.File :rocket: Publisher storing replication messages to files. #### Features diff --git a/example.exs b/example.exs index dbb1a9f..de4bb14 100644 --- a/example.exs +++ b/example.exs @@ -9,5 +9,5 @@ DynamicSupervisor.start_child( DynamicSupervisor.start_child( MainApp.DynamicSupervisor, - {FilePublisher, ["/tmp/replication.log"]} + {Publishers.File, ["/tmp/replication.log"]} ) diff --git a/lib/README.md b/lib/README.md deleted file mode 100644 index 804f1f2..0000000 --- a/lib/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# FilePublisher - -**FilePublisher** is a library responsible for persisting replicated database change events to the filesystem. diff --git a/lib/mix.exs b/lib/mix.exs deleted file mode 100644 index 0a2741a..0000000 --- a/lib/mix.exs +++ /dev/null @@ -1,31 +0,0 @@ -defmodule FilePublisher.MixProject do - use Mix.Project - - def project do - [ - app: :file_publisher, - version: "0.1.0", - build_path: "../../_build", - config_path: "../../config/config.exs", - deps_path: "../../deps", - lockfile: "../../mix.lock", - elixir: "~> 1.18", - start_permanent: Mix.env() == :prod, - deps: deps() - ] - end - - # Run "mix help compile.app" to learn about applications. - def application do - [ - extra_applications: [:logger] - ] - end - - # Run "mix help deps" to learn about dependencies. - defp deps do - [ - {:core, in_umbrella: true} - ] - end -end diff --git a/lib/file_publisher.ex b/lib/publishers/file/publisher.ex similarity index 84% rename from lib/file_publisher.ex rename to lib/publishers/file/publisher.ex index 89d0809..38cb293 100644 --- a/lib/file_publisher.ex +++ b/lib/publishers/file/publisher.ex @@ -1,11 +1,11 @@ -defmodule FilePublisher do +defmodule Publishers.File do @moduledoc """ - Documentation for `FilePublisher`. + Entry point for `Publishers.File`. """ alias Core.Messages.Delete alias Core.Messages.Update alias Core.Messages.Insert - alias FilePublisher.Serializer + alias Publishers.File.Serializer use GenServer require Logger @@ -23,7 +23,7 @@ defmodule FilePublisher do @impl true def init(file_path) do - Registry.register(PublisherRegistry, :publishers, {FilePublisher, :handle_message}) + Registry.register(PublisherRegistry, :publishers, {Publishers.File, :handle_message}) {:ok, file} = File.open(file_path, [:append]) {:ok, %{file: file}} end diff --git a/lib/file_publisher/serializer.ex b/lib/publishers/file/serializer.ex similarity index 75% rename from lib/file_publisher/serializer.ex rename to lib/publishers/file/serializer.ex index 125cd5e..64decb8 100644 --- a/lib/file_publisher/serializer.ex +++ b/lib/publishers/file/serializer.ex @@ -1,6 +1,6 @@ -defprotocol FilePublisher.Serializer do +defprotocol Publishers.File.Serializer do @moduledoc """ - Protocol defining serialization for FilePublisher. + Protocol defining serialization for Publishers.File. """ @doc """ @@ -10,7 +10,7 @@ defprotocol FilePublisher.Serializer do def serialize(message) end -defimpl FilePublisher.Serializer, for: Core.Messages.Insert do +defimpl Publishers.File.Serializer, for: Core.Messages.Insert do def serialize(%Core.Messages.Insert{} = message) do "INSERT INTO #{message.table_name} VALUES " <> (Enum.map(message.columns, fn col -> "#{col.value}" end) @@ -18,7 +18,7 @@ defimpl FilePublisher.Serializer, for: Core.Messages.Insert do end end -defimpl FilePublisher.Serializer, for: Core.Messages.Update do +defimpl Publishers.File.Serializer, for: Core.Messages.Update do def serialize(%Core.Messages.Update{} = message) do "UPDATE #{message.table_name} VALUES " <> (Enum.map(message.columns, fn col -> "#{col.value}" end) |> Enum.join(" ")) <> @@ -27,7 +27,7 @@ defimpl FilePublisher.Serializer, for: Core.Messages.Update do end end -defimpl FilePublisher.Serializer, for: Core.Messages.Delete do +defimpl Publishers.File.Serializer, for: Core.Messages.Delete do def serialize(%Core.Messages.Delete{} = message) do "DELETE #{message.table_name} WHERE " <> (Enum.map(message.where, fn col -> "#{col.name}=#{col.value}" end) |> Enum.join(" ")) diff --git a/test/dbreplex_test.exs b/test/dbreplex_test.exs index aea9dbe..9d4f3ba 100644 --- a/test/dbreplex_test.exs +++ b/test/dbreplex_test.exs @@ -7,12 +7,12 @@ defmodule DBReplexTest do @expected_file_publisher_content "./test/assets/expected_file_publisher_content.txt" @tag integration: true - test "Subscribers.Postgres to FilePublisher" do + test "Subscribers.Postgres to Publishers.File" do # initialization {:ok, file_publisher} = DynamicSupervisor.start_child( MainApp.DynamicSupervisor, - {FilePublisher, [@file_publisher_target]} + {Publishers.File, [@file_publisher_target]} ) {:ok, pg_subscriber} = diff --git a/test/file_publisher_test.exs b/test/file_publisher_test.exs index 56f0d68..5a38346 100644 --- a/test/file_publisher_test.exs +++ b/test/file_publisher_test.exs @@ -1,10 +1,10 @@ -defmodule FilePublisherTest do +defmodule Publishers.FileTest do alias Core.Messages.Insert use ExUnit.Case - doctest FilePublisher + doctest Publishers.File test "receive INSERT message" do - {:ok, pid} = FilePublisher.start_link("/tmp/replication.log") + {:ok, pid} = Publishers.File.start_link("/tmp/replication.log") save_message(pid, 5) Process.sleep(100) end @@ -18,7 +18,7 @@ defmodule FilePublisherTest do columns: [%{kind: 1, value: "Juraj"}] } - FilePublisher.handle_message(pid, message) + Publishers.File.handle_message(pid, message) save_message(pid, num - 1) end end From 53cdfbfeff0c052c8a41c47a164c4cfd8312b028 Mon Sep 17 00:00:00 2001 From: Juraj Paluba Date: Sat, 20 Sep 2025 14:59:39 +0200 Subject: [PATCH 4/6] refactor(lib): cleanup unused files --- lib/core.ex | 18 ------------------ test/core_test.exs | 8 -------- 2 files changed, 26 deletions(-) delete mode 100644 lib/core.ex delete mode 100644 test/core_test.exs diff --git a/lib/core.ex b/lib/core.ex deleted file mode 100644 index 1c53942..0000000 --- a/lib/core.ex +++ /dev/null @@ -1,18 +0,0 @@ -defmodule Core do - @moduledoc """ - Documentation for `Core`. - """ - - @doc """ - Hello world. - - ## Examples - - iex> Core.hello() - :world - - """ - def hello do - :world - end -end diff --git a/test/core_test.exs b/test/core_test.exs deleted file mode 100644 index 661aff1..0000000 --- a/test/core_test.exs +++ /dev/null @@ -1,8 +0,0 @@ -defmodule CoreTest do - use ExUnit.Case - doctest Core - - test "greets the world" do - assert Core.hello() == :world - end -end From 8104fccb93fafcc880c24b967000b958d3dca426 Mon Sep 17 00:00:00 2001 From: Juraj Paluba Date: Wed, 24 Sep 2025 17:23:46 +0200 Subject: [PATCH 5/6] refactor(gitignore): ignore elixir_ls files --- .formatter.exs | 3 +-- .gitignore | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.formatter.exs b/.formatter.exs index 33ad900..6269b73 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,5 +1,4 @@ # Used by "mix format" [ - inputs: ["mix.exs", "config/*.exs", "example.exs"], - subdirectories: ["apps/*"] + inputs: ["mix.exs", "example.exs", "{config,lib,test}/**/*.{ex,exs}", ".formatter.exs"] ] diff --git a/.gitignore b/.gitignore index dd7ba8f..728a1b9 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ erl_crash.dump # expert LSP .expert/ + +# elixir LSP +.elixir_ls/ From ecc6fe87cf2ffc148c65cc6562141c548a8cef5e Mon Sep 17 00:00:00 2001 From: Juraj Paluba Date: Wed, 24 Sep 2025 17:26:11 +0200 Subject: [PATCH 6/6] feat: define basic boundary structure --- lib/core/core.ex | 8 ++++++++ lib/dbreplex.ex | 1 + lib/publishers/file/publisher.ex | 1 + lib/subscribers/postgres/supervisor.ex | 1 + mix.exs | 6 ++++-- mix.lock | 1 + 6 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 lib/core/core.ex diff --git a/lib/core/core.ex b/lib/core/core.ex new file mode 100644 index 0000000..3913ac2 --- /dev/null +++ b/lib/core/core.ex @@ -0,0 +1,8 @@ +defmodule Core do + @moduledoc """ + The root module of the Core lib. + Used to define boundary. + """ + use Boundary, + exports: [Messages.{Column, Insert, Update, Delete, MessageProtocol}, PublisherContract] +end diff --git a/lib/dbreplex.ex b/lib/dbreplex.ex index 0852510..0f6feee 100644 --- a/lib/dbreplex.ex +++ b/lib/dbreplex.ex @@ -2,6 +2,7 @@ defmodule DBReplex do @moduledoc false use Application + use Boundary @impl true def start(_type, _args) do diff --git a/lib/publishers/file/publisher.ex b/lib/publishers/file/publisher.ex index 38cb293..a568985 100644 --- a/lib/publishers/file/publisher.ex +++ b/lib/publishers/file/publisher.ex @@ -7,6 +7,7 @@ defmodule Publishers.File do alias Core.Messages.Insert alias Publishers.File.Serializer + use Boundary, deps: [Core] use GenServer require Logger diff --git a/lib/subscribers/postgres/supervisor.ex b/lib/subscribers/postgres/supervisor.ex index 155c262..04668a9 100644 --- a/lib/subscribers/postgres/supervisor.ex +++ b/lib/subscribers/postgres/supervisor.ex @@ -2,6 +2,7 @@ defmodule Subscribers.Postgres do @moduledoc """ Entry point for Postgres subscriber. """ + use Boundary, deps: [Core] use Supervisor def start_link(init_arg) do diff --git a/mix.exs b/mix.exs index 0cbe656..ca7fcde 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,8 @@ defmodule DbSubscriptor.MixProject do version: "0.1.0", start_permanent: Mix.env() == :prod, name: "DBReplex", - deps: deps() + deps: deps(), + compilers: [:boundary] ++ Mix.compilers() ] end @@ -26,7 +27,8 @@ defmodule DbSubscriptor.MixProject do defp deps do [ {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, - {:postgrex, "~> 0.20.0"} + {:postgrex, "~> 0.20.0"}, + {:boundary, "~> 0.10", runtime: false} ] end end diff --git a/mix.lock b/mix.lock index 2c72a68..865e8e7 100644 --- a/mix.lock +++ b/mix.lock @@ -1,4 +1,5 @@ %{ + "boundary": {:hex, :boundary, "0.10.4", "5fec5d2736c12f9bfe1720c3a2bd8c48c3547c24d6002ebf8e087570afd5bd2f", [:mix], [], "hexpm", "8baf6f23987afdb1483033ed0bde75c9c703613c22ed58d5f23bf948f203247c"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "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"},