From 5346377d17fc86325324fbf2766bbc8e040cf560 Mon Sep 17 00:00:00 2001 From: Oleksii Sholik Date: Wed, 11 Mar 2026 00:37:37 +0100 Subject: [PATCH 1/3] chore(sync-service): upgrade Elixir to 1.20.0-rc.3 Upgrade Elixir from 1.19.5 to 1.20.0-rc.3 while keeping Erlang/OTP at 28.4. Fix Elixir 1.20 compatibility warnings: - Remove 7 unused `require` statements detected by stricter checks - Add pin operator (`^`) in bitstring size patterns (5 occurrences) - Rename `Changes.record()` type to `Changes.row()` to avoid overriding new built-in `record/0` type - Remove unreachable `:shutdown` case clause in pool.ex (caught by improved type inference from preceding handle_info clause) - Use `apply/3` in telemetry.ex to avoid type checker flagging compile-time conditional as always-false Also bump postgrex 0.21.1 -> 0.22.0 (with db_connection and telemetry transitive bumps). Co-Authored-By: Claude Opus 4.6 --- .changeset/upgrade-elixir-rc.md | 5 ++ .tool-versions | 2 +- elixir-otp-upgrade-progress.md | 60 +++++++++++++++++++ packages/sync-service/Dockerfile | 2 +- .../lib/electric/connection/manager/pool.ex | 1 - .../lib/electric/replication/changes.ex | 10 ++-- .../replication/shape_log_collector.ex | 1 - .../electric/shape_cache/pure_file_storage.ex | 10 ++-- .../lib/electric/shape_cache/shape_status.ex | 1 - .../shape_status/shape_db/migrator.ex | 2 - .../shape_cache/shape_status_owner.ex | 2 - .../lib/electric/shapes/consumer/move_ins.ex | 2 - .../lib/electric/shapes/consumer/state.ex | 1 - .../electric/stack_supervisor/telemetry.ex | 2 - .../sync-service/lib/electric/telemetry.ex | 2 +- packages/sync-service/mix.lock | 6 +- 16 files changed, 81 insertions(+), 28 deletions(-) create mode 100644 .changeset/upgrade-elixir-rc.md create mode 100644 elixir-otp-upgrade-progress.md diff --git a/.changeset/upgrade-elixir-rc.md b/.changeset/upgrade-elixir-rc.md new file mode 100644 index 0000000000..f781bfe001 --- /dev/null +++ b/.changeset/upgrade-elixir-rc.md @@ -0,0 +1,5 @@ +--- +'@core/sync-service': patch +--- + +Upgrade Elixir to 1.20.0-rc.3 and fix compatibility warnings (unused requires, bitstring pin operators, built-in type conflict, dead code). Also bump postgrex to 0.22.0. diff --git a/.tool-versions b/.tool-versions index c75f812127..4a14e7b1ef 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,5 +1,5 @@ caddy 2.10.0 -elixir 1.19.5 +elixir 1.20.0-rc.3-otp-28 erlang 28.4 nodejs 24.11.1 pnpm 10.12.1 diff --git a/elixir-otp-upgrade-progress.md b/elixir-otp-upgrade-progress.md new file mode 100644 index 0000000000..45a35a7e05 --- /dev/null +++ b/elixir-otp-upgrade-progress.md @@ -0,0 +1,60 @@ +# Elixir/OTP Upgrade Progress + +## Versions + +- **Elixir**: 1.19.5 -> 1.20.0-rc.3 +- **Erlang/OTP**: 28.4 (unchanged, latest stable) +- **Postgrex**: 0.21.1 -> 0.22.0 (to fix BitString pin operator warning in generated code) +- **db_connection**: 2.8.1 -> 2.9.0 (transitive dependency of postgrex) +- **telemetry**: 1.3.0 -> 1.4.1 (transitive dependency of postgrex) + +## Code changes required + +Elixir 1.20 introduces stricter compilation warnings from the set-theoretic type system and other compiler improvements. The following warnings needed to be fixed: + +### 1. Unused `require` statements (7 files) + +Elixir 1.20 now warns about unused `require` statements. Removed from: +- `lib/electric/shape_cache/shape_status/shape_db/migrator.ex` - `require Logger` +- `lib/electric/shape_cache/shape_status_owner.ex` - `require Logger` +- `lib/electric/shapes/consumer/move_ins.ex` - `require Xid` +- `lib/electric/stack_supervisor/telemetry.ex` - `require Logger` +- `lib/electric/replication/shape_log_collector.ex` - `require Electric.Postgres.Lsn` +- `lib/electric/shape_cache/shape_status.ex` - `require Electric.Shapes.Shape` +- `lib/electric/shapes/consumer/state.ex` - `require LogOffset` + +### 2. Pin operator in bitstring size patterns + +Elixir 1.20 requires the pin operator (`^`) for variables used in `size(...)` within bitstring patterns. Fixed in: +- `lib/electric/shape_cache/pure_file_storage.ex` - 5 occurrences across `append_move_in_snapshot_to_log!`, `read_tags`, and `append_move_in_snapshot_to_log_filtered!` + +### 3. Type `record/0` overriding built-in + +Elixir 1.20 introduced `record()` as a built-in type. Renamed `Changes.record()` to `Changes.row()` in: +- `lib/electric/replication/changes.ex` + +### 4. Dead code detected by type inference + +- `lib/electric/connection/manager/pool.ex` - Removed `:shutdown` clause from case expression that could never match (already caught by a preceding `handle_info` clause at line 195) + +### 5. Compile-time conditional always false + +- `lib/electric/telemetry.ex` - Changed `Electric.telemetry_enabled?()` to `apply(Electric, :telemetry_enabled?, [])` to avoid type checker inferring the compile-time constant value + +## Known issues + +### Postgrex generated code warnings + +`Postgrex.Types.define` generates a type module using `Module.create`. Elixir 1.20's compiler flags two functions in the generated module as unused: +- `Elixir.PgInterop.Postgrex.Extensions.PgSnapshot/4` +- `Elixir.PgInterop.Postgrex.Extensions.PgLsn/4` + +These are arity-4 decode helper functions that exist for array/composite type decoding but are never called for these simple binary extension types. This is a postgrex compatibility issue with Elixir 1.20 that can't be suppressed from our side (the module is created via `Module.create` which doesn't preserve `@compile` attributes from pre-defined modules). + +This prevents `mix compile --warnings-as-errors` from passing, but the warnings are purely from generated dependency code, not from Electric's source. + +## Verification + +- `mix compile` - passes (2 warnings from postgrex generated code only) +- `mix format --check-formatted` - passes +- `mix test` - passes (382 doctests, 8 properties, 1443 tests, 0 failures) diff --git a/packages/sync-service/Dockerfile b/packages/sync-service/Dockerfile index 3df7f90457..3adc6432e2 100644 --- a/packages/sync-service/Dockerfile +++ b/packages/sync-service/Dockerfile @@ -1,4 +1,4 @@ -ARG ELIXIR_VERSION=1.19.5 +ARG ELIXIR_VERSION=1.20.0-rc.3 ARG OTP_VERSION=28.4 ARG DEBIAN_VERSION=bookworm-20260223-slim diff --git a/packages/sync-service/lib/electric/connection/manager/pool.ex b/packages/sync-service/lib/electric/connection/manager/pool.ex index 3b313c0838..578c29e386 100644 --- a/packages/sync-service/lib/electric/connection/manager/pool.ex +++ b/packages/sync-service/lib/electric/connection/manager/pool.ex @@ -229,7 +229,6 @@ defmodule Electric.Connection.Manager.Pool do state = case reason do :killed -> state - :shutdown -> state {:shutdown, exit_reason} -> %{state | last_connection_error: exit_reason} reason -> %{state | last_connection_error: reason} end diff --git a/packages/sync-service/lib/electric/replication/changes.ex b/packages/sync-service/lib/electric/replication/changes.ex index cd026a9e2f..1bf19a946e 100644 --- a/packages/sync-service/lib/electric/replication/changes.ex +++ b/packages/sync-service/lib/electric/replication/changes.ex @@ -17,7 +17,7 @@ defmodule Electric.Replication.Changes do @type db_identifier() :: String.t() @type xid() :: Xid.anyxid() @type relation_name() :: {schema :: db_identifier(), table :: db_identifier()} - @type record() :: %{(column_name :: db_identifier()) => column_data :: binary()} + @type row() :: %{(column_name :: db_identifier()) => column_data :: binary()} @type relation_id() :: non_neg_integer @typedoc """ @@ -186,7 +186,7 @@ defmodule Electric.Replication.Changes do @type t() :: %__MODULE__{ relation: Changes.relation_name(), - record: Changes.record(), + record: Changes.row(), log_offset: LogOffset.t(), key: String.t() | nil, last?: boolean(), @@ -210,8 +210,8 @@ defmodule Electric.Replication.Changes do @type t() :: %__MODULE__{ relation: Changes.relation_name(), - old_record: Changes.record() | nil, - record: Changes.record(), + old_record: Changes.row() | nil, + record: Changes.row(), log_offset: LogOffset.t(), key: String.t() | nil, old_key: String.t() | nil, @@ -258,7 +258,7 @@ defmodule Electric.Replication.Changes do @type t() :: %__MODULE__{ relation: Changes.relation_name(), - old_record: Changes.record(), + old_record: Changes.row(), log_offset: LogOffset.t(), key: String.t() | nil, move_tags: [Changes.tag()], diff --git a/packages/sync-service/lib/electric/replication/shape_log_collector.ex b/packages/sync-service/lib/electric/replication/shape_log_collector.ex index e7f423fba5..93ac892df3 100644 --- a/packages/sync-service/lib/electric/replication/shape_log_collector.ex +++ b/packages/sync-service/lib/electric/replication/shape_log_collector.ex @@ -35,7 +35,6 @@ defmodule Electric.Replication.ShapeLogCollector do import Electric.Utils, only: [map_while_ok: 2, map_if_ok: 2] - require Electric.Postgres.Lsn require Electric.Replication.LogOffset require Logger require TransactionFragment diff --git a/packages/sync-service/lib/electric/shape_cache/pure_file_storage.ex b/packages/sync-service/lib/electric/shape_cache/pure_file_storage.ex index 1b0a475782..56481df5c7 100644 --- a/packages/sync-service/lib/electric/shape_cache/pure_file_storage.ex +++ b/packages/sync-service/lib/electric/shape_cache/pure_file_storage.ex @@ -909,8 +909,8 @@ defmodule Electric.ShapeCache.PureFileStorage do with {:meta, <>} <- {:meta, IO.binread(file, 15)}, _tags = read_tags(file, tag_count), - <> <- IO.binread(file, key_size), - <> <- IO.binread(file, json_size) do + <> <- IO.binread(file, key_size), + <> <- IO.binread(file, json_size) do {[{offset, key_size, key, op_type, 0, json_size, json}], {file, LogOffset.increment(offset)}} else @@ -937,7 +937,7 @@ defmodule Electric.ShapeCache.PureFileStorage do defp read_tags(file, tag_count) do for _ <- 1..tag_count//1 do <> = IO.binread(file, 2) - <> = IO.binread(file, tag_size) + <> = IO.binread(file, tag_size) tag end end @@ -962,8 +962,8 @@ defmodule Electric.ShapeCache.PureFileStorage do with {:meta, <>} <- {:meta, IO.binread(file, 15)}, tags = read_tags(file, tag_count), - <> <- IO.binread(file, key_size), - <> <- IO.binread(file, json_size) do + <> <- IO.binread(file, key_size), + <> <- IO.binread(file, json_size) do # Check if this row should be skipped if all_parents_moved_out?(tags, tags_to_skip) or Electric.Shapes.Consumer.MoveIns.should_skip_query_row?( diff --git a/packages/sync-service/lib/electric/shape_cache/shape_status.ex b/packages/sync-service/lib/electric/shape_cache/shape_status.ex index 51e087fa60..5ec652bf9e 100644 --- a/packages/sync-service/lib/electric/shape_cache/shape_status.ex +++ b/packages/sync-service/lib/electric/shape_cache/shape_status.ex @@ -20,7 +20,6 @@ defmodule Electric.ShapeCache.ShapeStatus do import Electric, only: [is_stack_id: 1, is_shape_handle: 1] - require Electric.Shapes.Shape require Logger @type stack_id() :: Electric.stack_id() diff --git a/packages/sync-service/lib/electric/shape_cache/shape_status/shape_db/migrator.ex b/packages/sync-service/lib/electric/shape_cache/shape_status/shape_db/migrator.ex index aef60795fc..8a32c8774c 100644 --- a/packages/sync-service/lib/electric/shape_cache/shape_status/shape_db/migrator.ex +++ b/packages/sync-service/lib/electric/shape_cache/shape_status/shape_db/migrator.ex @@ -3,8 +3,6 @@ defmodule Electric.ShapeCache.ShapeStatus.ShapeDb.Migrator do alias Electric.ShapeCache.ShapeStatus.ShapeDb - require Logger - @optimization_period :timer.minutes(60) def start_link(args) do diff --git a/packages/sync-service/lib/electric/shape_cache/shape_status_owner.ex b/packages/sync-service/lib/electric/shape_cache/shape_status_owner.ex index bc9a3f9501..730405f836 100644 --- a/packages/sync-service/lib/electric/shape_cache/shape_status_owner.ex +++ b/packages/sync-service/lib/electric/shape_cache/shape_status_owner.ex @@ -12,8 +12,6 @@ defmodule Electric.ShapeCache.ShapeStatusOwner do alias Electric.ShapeCache.ShapeStatus - require Logger - @schema NimbleOptions.new!(stack_id: [type: :string, required: true]) def name(stack_id) do diff --git a/packages/sync-service/lib/electric/shapes/consumer/move_ins.ex b/packages/sync-service/lib/electric/shapes/consumer/move_ins.ex index c0c554cb70..a235bf7ffe 100644 --- a/packages/sync-service/lib/electric/shapes/consumer/move_ins.ex +++ b/packages/sync-service/lib/electric/shapes/consumer/move_ins.ex @@ -4,8 +4,6 @@ defmodule Electric.Shapes.Consumer.MoveIns do alias Electric.Postgres.Xid alias Electric.Postgres.SnapshotQuery - require Xid - defstruct waiting_move_ins: %{}, filtering_move_ins: [], touch_tracker: %{}, diff --git a/packages/sync-service/lib/electric/shapes/consumer/state.ex b/packages/sync-service/lib/electric/shapes/consumer/state.ex index 3b51de7855..ff1a31aa79 100644 --- a/packages/sync-service/lib/electric/shapes/consumer/state.ex +++ b/packages/sync-service/lib/electric/shapes/consumer/state.ex @@ -11,7 +11,6 @@ defmodule Electric.Shapes.Consumer.State do alias Electric.ShapeCache.Storage require Logger - require LogOffset @write_unit_txn :txn @write_unit_txn_fragment :txn_fragment diff --git a/packages/sync-service/lib/electric/stack_supervisor/telemetry.ex b/packages/sync-service/lib/electric/stack_supervisor/telemetry.ex index 6f760a85f2..96b30a479d 100644 --- a/packages/sync-service/lib/electric/stack_supervisor/telemetry.ex +++ b/packages/sync-service/lib/electric/stack_supervisor/telemetry.ex @@ -1,6 +1,4 @@ defmodule Electric.StackSupervisor.Telemetry do - require Logger - def configure(config) do # Set shared OpenTelemetry span attributes for the given stack. They are stored in # persistent_term so it doesn't matter which process this function is called from. diff --git a/packages/sync-service/lib/electric/telemetry.ex b/packages/sync-service/lib/electric/telemetry.ex index 13023a4753..3cfb299873 100644 --- a/packages/sync-service/lib/electric/telemetry.ex +++ b/packages/sync-service/lib/electric/telemetry.ex @@ -23,7 +23,7 @@ defmodule Electric.Telemetry do modules = List.wrap(dependencies) |> Enum.map(&Macro.expand(&1, env)) telemetry_code_available? = Enum.all?(modules, &Code.ensure_loaded?/1) - if Electric.telemetry_enabled?() && telemetry_code_available? do + if apply(Electric, :telemetry_enabled?, []) and telemetry_code_available? do if @log_level do Logger.log( @log_level, diff --git a/packages/sync-service/mix.lock b/packages/sync-service/mix.lock index a69bbed739..b77e63d96d 100644 --- a/packages/sync-service/mix.lock +++ b/packages/sync-service/mix.lock @@ -6,7 +6,7 @@ "chatterbox": {:hex, :ts_chatterbox, "0.15.1", "5cac4d15dd7ad61fc3c4415ce4826fc563d4643dee897a558ec4ea0b1c835c9c", [:rebar3], [{:hpack, "~> 0.3.0", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "4f75b91451338bc0da5f52f3480fa6ef6e3a2aeecfc33686d6b3d0a0948f31aa"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "ctx": {:hex, :ctx, "0.6.0", "8ff88b70e6400c4df90142e7f130625b82086077a45364a78d208ed3ed53c7fe", [:rebar3], [], "hexpm", "a14ed2d1b67723dbebbe423b28d7615eb0bdcba6ff28f2d1f1b0a7e1d4aa5fc2"}, - "db_connection": {:hex, :db_connection, "2.8.1", "9abdc1e68c34c6163f6fb96a96532272d13ad7ca45262156ae8b7ec6d9dc4bec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61a3d489b239d76f326e03b98794fb8e45168396c925ef25feb405ed09da8fd"}, + "db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "dialyxir": {:hex, :dialyxir, "1.4.6", "7cca478334bf8307e968664343cbdb432ee95b4b68a9cba95bdabb0ad5bdfd9a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "8cf5615c5cd4c2da6c501faae642839c8405b49f8aa057ad4ae401cb808ef64d"}, "dotenvy": {:hex, :dotenvy, "1.1.0", "316aee89c11a4ec8be3d74a69d17d17ea2e21e633e0cac9f155cf420e237ccb4", [:mix], [], "hexpm", "0519bda67fdfa1c22279c2654b2f292485f0caae7360fe29205f74f28a93df18"}, @@ -43,7 +43,7 @@ "pg_query_ex": {:hex, :pg_query_ex, "0.9.0", "8e34bd2d0e0eb9e8d621c4697032fad4bfba46826950d3b46904a80ab589b43a", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:protox, "~> 2.0", [hex: :protox, repo: "hexpm", optional: false]}], "hexpm", "a3fada1704fa9e2bc11ff846ad545ef9a1d34f46d86206063c37128960f4f5f5"}, "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, - "postgrex": {:hex, :postgrex, "0.21.1", "2c5cc830ec11e7a0067dd4d623c049b3ef807e9507a424985b8dcf921224cd88", [: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", "27d8d21c103c3cc68851b533ff99eef353e6a0ff98dc444ea751de43eb48bdac"}, + "postgrex": {:hex, :postgrex, "0.22.0", "fb027b58b6eab1f6de5396a2abcdaaeb168f9ed4eccbb594e6ac393b02078cbd", [:mix], [{:db_connection, "~> 2.9", [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", "a68c4261e299597909e03e6f8ff5a13876f5caadaddd0d23af0d0a61afcc5d84"}, "protobuf": {:hex, :protobuf, "0.13.0", "7a9d9aeb039f68a81717eb2efd6928fdf44f03d2c0dfdcedc7b560f5f5aae93d", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "21092a223e3c6c144c1a291ab082a7ead32821ba77073b72c68515aa51fef570"}, "protox": {:hex, :protox, "2.0.4", "2a86ae3699696c5d92e15804968ce6a6827a8d9516d0bbabcf16584dec710ae1", [:mix], [], "hexpm", "8ac5a03bb84da4c75d76dc29cd46008081c2068ad0f6f0da4c051093d6e24c01"}, "remote_ip": {:hex, :remote_ip, "1.2.0", "fb078e12a44414f4cef5a75963c33008fe169b806572ccd17257c208a7bc760f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2ff91de19c48149ce19ed230a81d377186e4412552a597d6a5137373e5877cb7"}, @@ -54,7 +54,7 @@ "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"}, "stream_split": {:hex, :stream_split, "0.1.7", "2d3fd1fd21697da7f91926768d65f79409086052c9ec7ae593987388f52425f8", [:mix], [], "hexpm", "1dc072ff507a64404a0ad7af90df97096183fee8eeac7b300320cea7c4679147"}, - "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, "telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.1", "c9755987d7b959b557084e6990990cb96a50d6482c683fb9622a63837f3cd3d8", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5e2c599da4983c4f88a33e9571f1458bf98b0cf6ba930f1dc3a6e8cf45d5afb6"}, "telemetry_metrics_statsd": {:hex, :telemetry_metrics_statsd, "0.7.2", "a70cfaf821cb2f3ac2e767988461179add44762d1db752e74dfa0c93449b2857", [:mix], [{:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "933bb8b176b95d5088404b7137d94926b8dea9a74ef2c95d616f2740f1571c13"}, From 06ba704ab35f207bda41a7cb76099a91532152ef Mon Sep 17 00:00:00 2001 From: Oleksii Sholik Date: Wed, 11 Mar 2026 00:58:31 +0100 Subject: [PATCH 2/3] fixup! chore(sync-service): upgrade Elixir to 1.20.0-rc.3 - Fix unused `require Logger` in electric-telemetry (stack_telemetry.ex) - Fix bitstring pin operator in elixir-client (postgres.ex) - Restore `require Logger` in stack_supervisor/telemetry.ex (needed with MIX_TARGET=application) - Use postgrex from git main to pick up the Elixir 1.20 unused-clause fix (https://github.com/elixir-ecto/postgrex/issues/760) - Remove progress file (review feedback) Co-Authored-By: Claude Opus 4.6 --- elixir-otp-upgrade-progress.md | 60 ------------------- .../lib/electric/telemetry/stack_telemetry.ex | 2 - .../electric/client/ecto_adapter/postgres.ex | 2 +- .../electric/stack_supervisor/telemetry.ex | 2 + packages/sync-service/mix.exs | 4 +- packages/sync-service/mix.lock | 2 +- 6 files changed, 7 insertions(+), 65 deletions(-) delete mode 100644 elixir-otp-upgrade-progress.md diff --git a/elixir-otp-upgrade-progress.md b/elixir-otp-upgrade-progress.md deleted file mode 100644 index 45a35a7e05..0000000000 --- a/elixir-otp-upgrade-progress.md +++ /dev/null @@ -1,60 +0,0 @@ -# Elixir/OTP Upgrade Progress - -## Versions - -- **Elixir**: 1.19.5 -> 1.20.0-rc.3 -- **Erlang/OTP**: 28.4 (unchanged, latest stable) -- **Postgrex**: 0.21.1 -> 0.22.0 (to fix BitString pin operator warning in generated code) -- **db_connection**: 2.8.1 -> 2.9.0 (transitive dependency of postgrex) -- **telemetry**: 1.3.0 -> 1.4.1 (transitive dependency of postgrex) - -## Code changes required - -Elixir 1.20 introduces stricter compilation warnings from the set-theoretic type system and other compiler improvements. The following warnings needed to be fixed: - -### 1. Unused `require` statements (7 files) - -Elixir 1.20 now warns about unused `require` statements. Removed from: -- `lib/electric/shape_cache/shape_status/shape_db/migrator.ex` - `require Logger` -- `lib/electric/shape_cache/shape_status_owner.ex` - `require Logger` -- `lib/electric/shapes/consumer/move_ins.ex` - `require Xid` -- `lib/electric/stack_supervisor/telemetry.ex` - `require Logger` -- `lib/electric/replication/shape_log_collector.ex` - `require Electric.Postgres.Lsn` -- `lib/electric/shape_cache/shape_status.ex` - `require Electric.Shapes.Shape` -- `lib/electric/shapes/consumer/state.ex` - `require LogOffset` - -### 2. Pin operator in bitstring size patterns - -Elixir 1.20 requires the pin operator (`^`) for variables used in `size(...)` within bitstring patterns. Fixed in: -- `lib/electric/shape_cache/pure_file_storage.ex` - 5 occurrences across `append_move_in_snapshot_to_log!`, `read_tags`, and `append_move_in_snapshot_to_log_filtered!` - -### 3. Type `record/0` overriding built-in - -Elixir 1.20 introduced `record()` as a built-in type. Renamed `Changes.record()` to `Changes.row()` in: -- `lib/electric/replication/changes.ex` - -### 4. Dead code detected by type inference - -- `lib/electric/connection/manager/pool.ex` - Removed `:shutdown` clause from case expression that could never match (already caught by a preceding `handle_info` clause at line 195) - -### 5. Compile-time conditional always false - -- `lib/electric/telemetry.ex` - Changed `Electric.telemetry_enabled?()` to `apply(Electric, :telemetry_enabled?, [])` to avoid type checker inferring the compile-time constant value - -## Known issues - -### Postgrex generated code warnings - -`Postgrex.Types.define` generates a type module using `Module.create`. Elixir 1.20's compiler flags two functions in the generated module as unused: -- `Elixir.PgInterop.Postgrex.Extensions.PgSnapshot/4` -- `Elixir.PgInterop.Postgrex.Extensions.PgLsn/4` - -These are arity-4 decode helper functions that exist for array/composite type decoding but are never called for these simple binary extension types. This is a postgrex compatibility issue with Elixir 1.20 that can't be suppressed from our side (the module is created via `Module.create` which doesn't preserve `@compile` attributes from pre-defined modules). - -This prevents `mix compile --warnings-as-errors` from passing, but the warnings are purely from generated dependency code, not from Electric's source. - -## Verification - -- `mix compile` - passes (2 warnings from postgrex generated code only) -- `mix format --check-formatted` - passes -- `mix test` - passes (382 doctests, 8 properties, 1443 tests, 0 failures) diff --git a/packages/electric-telemetry/lib/electric/telemetry/stack_telemetry.ex b/packages/electric-telemetry/lib/electric/telemetry/stack_telemetry.ex index 6413382a94..df5b2a2c7a 100644 --- a/packages/electric-telemetry/lib/electric/telemetry/stack_telemetry.ex +++ b/packages/electric-telemetry/lib/electric/telemetry/stack_telemetry.ex @@ -12,8 +12,6 @@ defmodule ElectricTelemetry.StackTelemetry do alias ElectricTelemetry.Reporters - require Logger - @behaviour ElectricTelemetry.Poller def start_link(opts) do diff --git a/packages/elixir-client/lib/electric/client/ecto_adapter/postgres.ex b/packages/elixir-client/lib/electric/client/ecto_adapter/postgres.ex index 300489557c..e2db87513f 100644 --- a/packages/elixir-client/lib/electric/client/ecto_adapter/postgres.ex +++ b/packages/elixir-client/lib/electric/client/ecto_adapter/postgres.ex @@ -399,7 +399,7 @@ if Code.ensure_loaded?(Ecto) do defp bitstring_literal(value) do size = bit_size(value) - <> = value + <> = value [?b, ?', val |> Integer.to_string(2) |> String.pad_leading(size, ["0"]), ?'] end diff --git a/packages/sync-service/lib/electric/stack_supervisor/telemetry.ex b/packages/sync-service/lib/electric/stack_supervisor/telemetry.ex index 96b30a479d..6f760a85f2 100644 --- a/packages/sync-service/lib/electric/stack_supervisor/telemetry.ex +++ b/packages/sync-service/lib/electric/stack_supervisor/telemetry.ex @@ -1,4 +1,6 @@ defmodule Electric.StackSupervisor.Telemetry do + require Logger + def configure(config) do # Set shared OpenTelemetry span attributes for the given stack. They are stored in # persistent_term so it doesn't matter which process this function is called from. diff --git a/packages/sync-service/mix.exs b/packages/sync-service/mix.exs index bbc2441871..79a8012554 100644 --- a/packages/sync-service/mix.exs +++ b/packages/sync-service/mix.exs @@ -100,7 +100,9 @@ defmodule Electric.MixProject do {:opentelemetry_semantic_conventions, "~> 1.27"}, {:pg_query_ex, "0.9.0"}, {:plug, "~> 1.17"}, - {:postgrex, "~> 0.20"}, + # TODO: Switch back to Hex once postgrex releases a version with the + # Elixir 1.20 unused-clause fix (https://github.com/elixir-ecto/postgrex/issues/760) + {:postgrex, github: "elixir-ecto/postgrex", ref: "e4f79427e99bb0dc86376a769fc966b98edb7dfc"}, {:retry, "~> 0.19"}, {:remote_ip, "~> 1.2"}, {:req, "~> 0.5"}, diff --git a/packages/sync-service/mix.lock b/packages/sync-service/mix.lock index b77e63d96d..d04178db32 100644 --- a/packages/sync-service/mix.lock +++ b/packages/sync-service/mix.lock @@ -43,7 +43,7 @@ "pg_query_ex": {:hex, :pg_query_ex, "0.9.0", "8e34bd2d0e0eb9e8d621c4697032fad4bfba46826950d3b46904a80ab589b43a", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:protox, "~> 2.0", [hex: :protox, repo: "hexpm", optional: false]}], "hexpm", "a3fada1704fa9e2bc11ff846ad545ef9a1d34f46d86206063c37128960f4f5f5"}, "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, - "postgrex": {:hex, :postgrex, "0.22.0", "fb027b58b6eab1f6de5396a2abcdaaeb168f9ed4eccbb594e6ac393b02078cbd", [:mix], [{:db_connection, "~> 2.9", [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", "a68c4261e299597909e03e6f8ff5a13876f5caadaddd0d23af0d0a61afcc5d84"}, + "postgrex": {:git, "https://github.com/elixir-ecto/postgrex.git", "e4f79427e99bb0dc86376a769fc966b98edb7dfc", [ref: "e4f79427e99bb0dc86376a769fc966b98edb7dfc"]}, "protobuf": {:hex, :protobuf, "0.13.0", "7a9d9aeb039f68a81717eb2efd6928fdf44f03d2c0dfdcedc7b560f5f5aae93d", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "21092a223e3c6c144c1a291ab082a7ead32821ba77073b72c68515aa51fef570"}, "protox": {:hex, :protox, "2.0.4", "2a86ae3699696c5d92e15804968ce6a6827a8d9516d0bbabcf16584dec710ae1", [:mix], [], "hexpm", "8ac5a03bb84da4c75d76dc29cd46008081c2068ad0f6f0da4c051093d6e24c01"}, "remote_ip": {:hex, :remote_ip, "1.2.0", "fb078e12a44414f4cef5a75963c33008fe169b806572ccd17257c208a7bc760f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2ff91de19c48149ce19ed230a81d377186e4412552a597d6a5137373e5877cb7"}, From 2121a966b1d74d4516fa2fb88bf2dce875eba7cb Mon Sep 17 00:00:00 2001 From: Oleksii Sholik Date: Wed, 11 Mar 2026 01:01:45 +0100 Subject: [PATCH 3/3] fixup! chore(sync-service): upgrade Elixir to 1.20.0-rc.3 Fix mix.exs formatting. Co-Authored-By: Claude Opus 4.6 --- packages/sync-service/mix.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/sync-service/mix.exs b/packages/sync-service/mix.exs index 79a8012554..97b53f1761 100644 --- a/packages/sync-service/mix.exs +++ b/packages/sync-service/mix.exs @@ -102,7 +102,8 @@ defmodule Electric.MixProject do {:plug, "~> 1.17"}, # TODO: Switch back to Hex once postgrex releases a version with the # Elixir 1.20 unused-clause fix (https://github.com/elixir-ecto/postgrex/issues/760) - {:postgrex, github: "elixir-ecto/postgrex", ref: "e4f79427e99bb0dc86376a769fc966b98edb7dfc"}, + {:postgrex, + github: "elixir-ecto/postgrex", ref: "e4f79427e99bb0dc86376a769fc966b98edb7dfc"}, {:retry, "~> 0.19"}, {:remote_ip, "~> 1.2"}, {:req, "~> 0.5"},