Skip to content

Commit 67d0450

Browse files
committed
feat: Automatically switch to DROP / CREATE trigger if PG is <= v13
1 parent 48af7b0 commit 67d0450

File tree

10 files changed

+120
-63
lines changed

10 files changed

+120
-63
lines changed

config/config.exs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import Config
2+
3+
config :ecto_watch, EctoWatch.DB, mod: EctoWatch.DB.Live
4+
5+
if Mix.env() == :test do
6+
config :ecto_watch, EctoWatch.DB, mod: EctoWatch.DB.Mock
7+
end

guides/introduction/Getting Started.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ To use EctoWatch, you need to add it to your supervision tree and specify watche
1010
repo: MyApp.Repo,
1111
pub_sub: MyApp.PubSub,
1212
# Set to true if using PostgreSQL versions older than 13.3.4
13-
legacy_postgres_support?: false,
1413
watchers: [
1514
{User, :inserted},
1615
{User, :updated},
@@ -80,4 +79,4 @@ Once subscribed, messages can be handled like so (LiveView example are given her
8079

8180
### PostgreSQL Version Compatibility
8281

83-
EctoWatch uses `CREATE OR REPLACE TRIGGER` which was introduced in PostgreSQL 13.3.4. If you're using an older version of PostgreSQL, set the `legacy_postgres_support?: true` option. This will use `DROP TRIGGER IF EXISTS` followed by `CREATE TRIGGER` instead.
82+
EctoWatch uses `CREATE OR REPLACE TRIGGER` which was introduced in PostgreSQL 13. If you're using an older version of PostgreSQL, `ecto_watch` will automatically use `DROP TRIGGER IF EXISTS` followed by `CREATE TRIGGER` instead. This can have [potential problems with permissions](https://github.com/cheerfulstoic/ecto_watch/issues/50), so if you experience those it's recommended to upgrade to at least PostgreSQL 14.

lib/ecto_watch/db.ex

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
defmodule EctoWatch.DB do
2+
@callback max_identifier_length(repo_mod :: atom()) :: integer()
3+
@callback major_version(repo_mod :: atom()) :: integer()
4+
5+
def supports_create_or_replace_trigger?(repo_mod) do
6+
major_version(repo_mod) >= 14
7+
end
8+
9+
# TODO
10+
def max_identifier_length(repo_mod), do: mod().max_identifier_length(repo_mod)
11+
def major_version(repo_mod), do: mod().major_version(repo_mod)
12+
13+
defp mod do
14+
Application.get_env(:ecto_watch, EctoWatch.DB)[:mod]
15+
end
16+
end
17+
18+
defmodule EctoWatch.DB.Live do
19+
@behaviour EctoWatch.DB
20+
21+
def max_identifier_length(repo_mod) do
22+
query_for_value(repo_mod, "SHOW max_identifier_length")
23+
|> String.to_integer()
24+
end
25+
26+
def major_version(repo_mod) do
27+
version_string = query_for_value(repo_mod, "SHOW server_version")
28+
29+
case Integer.parse(version_string) do
30+
{major_version, _} ->
31+
major_version
32+
33+
_ ->
34+
raise "Unable to parse PostgreSQL major version number from version string: #{inspect(version_string)}"
35+
end
36+
end
37+
38+
defp query_for_value(repo_mod, query) do
39+
case Ecto.Adapters.SQL.query!(repo_mod, query, []) do
40+
%Postgrex.Result{rows: [[value]]} ->
41+
value
42+
43+
other ->
44+
raise "Unexpected result when making query `#{query}`: #{inspect(other)}"
45+
end
46+
end
47+
end

lib/ecto_watch/options.ex

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,16 @@ defmodule EctoWatch.Options do
33

44
alias EctoWatch.Options.WatcherOptions
55

6-
defstruct [:repo_mod, :pub_sub_mod, :watchers, :debug?, :legacy_postgres_support?]
6+
defstruct [:repo_mod, :pub_sub_mod, :watchers, :debug?]
77

88
def new(opts) do
99
%__MODULE__{
1010
repo_mod: opts[:repo],
1111
pub_sub_mod: opts[:pub_sub],
1212
debug?: opts[:debug?],
13-
legacy_postgres_support?: opts[:legacy_postgres_support?],
1413
watchers:
1514
Enum.map(opts[:watchers], fn watcher_opts ->
16-
WatcherOptions.new(watcher_opts, opts[:debug?], opts[:legacy_postgres_support?])
15+
WatcherOptions.new(watcher_opts, opts[:debug?])
1716
end)
1817
}
1918
end
@@ -36,13 +35,6 @@ defmodule EctoWatch.Options do
3635
type: :boolean,
3736
required: false,
3837
default: false
39-
],
40-
legacy_postgres_support?: [
41-
type: :boolean,
42-
required: false,
43-
default: false,
44-
doc:
45-
"Set to true to use DROP/CREATE instead of CREATE OR REPLACE for trigger creation (only needed for PostgreSQL versions older than 13.3.4)"
4638
]
4739
]
4840

lib/ecto_watch/options/watcher_options.ex

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ defmodule EctoWatch.Options.WatcherOptions do
99
:label,
1010
:trigger_columns,
1111
:extra_columns,
12-
:debug?,
13-
:legacy_postgres_support?
12+
:debug?
1413
]
1514

1615
def validate_list(list) do
@@ -179,13 +178,6 @@ defmodule EctoWatch.Options.WatcherOptions do
179178
type: :boolean,
180179
required: false,
181180
default: false
182-
],
183-
legacy_postgres_support?: [
184-
type: :boolean,
185-
required: false,
186-
default: false,
187-
doc:
188-
"Set to true to use DROP/CREATE instead of CREATE OR REPLACE for trigger creation (only needed for PostgreSQL versions older than 13.3.4)"
189181
]
190182
]
191183

@@ -231,7 +223,7 @@ defmodule EctoWatch.Options.WatcherOptions do
231223
new({schema_definition, update_type, []}, debug?)
232224
end
233225

234-
def new({schema_definition, update_type, opts}, debug?, legacy_postgres_support? \\ false) do
226+
def new({schema_definition, update_type, opts}, debug?) do
235227
schema_definition = SchemaDefinition.new(schema_definition)
236228

237229
%__MODULE__{
@@ -240,8 +232,7 @@ defmodule EctoWatch.Options.WatcherOptions do
240232
label: opts[:label],
241233
trigger_columns: opts[:trigger_columns] || [],
242234
extra_columns: opts[:extra_columns] || [],
243-
debug?: debug? || opts[:debug?],
244-
legacy_postgres_support?: opts[:legacy_postgres_support?] || legacy_postgres_support?
235+
debug?: debug? || opts[:debug?]
245236
}
246237
end
247238
end

lib/ecto_watch/watcher_server.ex

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ defmodule EctoWatch.WatcherServer do
55
Used internally, but you'll see it in your application supervision tree.
66
"""
77

8+
alias EctoWatch.DB
89
alias EctoWatch.Helpers
910
alias EctoWatch.Options.WatcherOptions
1011

@@ -101,29 +102,29 @@ defmodule EctoWatch.WatcherServer do
101102
[]
102103
)
103104

104-
if options.legacy_postgres_support? do
105+
if DB.supports_create_or_replace_trigger?(repo_mod) do
105106
Ecto.Adapters.SQL.query!(
106107
repo_mod,
107108
"""
108-
DROP TRIGGER IF EXISTS #{details.trigger_name} on \"#{options.schema_definition.schema_prefix}\".\"#{options.schema_definition.table_name}\";
109+
CREATE OR REPLACE TRIGGER #{details.trigger_name}
110+
AFTER #{update_keyword} ON \"#{options.schema_definition.schema_prefix}\".\"#{options.schema_definition.table_name}\" FOR EACH ROW
111+
EXECUTE PROCEDURE \"#{options.schema_definition.schema_prefix}\".#{details.function_name}();
109112
""",
110113
[]
111114
)
112-
115+
else
113116
Ecto.Adapters.SQL.query!(
114117
repo_mod,
115118
"""
116-
CREATE TRIGGER #{details.trigger_name}
117-
AFTER #{update_keyword} ON \"#{options.schema_definition.schema_prefix}\".\"#{options.schema_definition.table_name}\" FOR EACH ROW
118-
EXECUTE PROCEDURE \"#{options.schema_definition.schema_prefix}\".#{details.function_name}();
119+
DROP TRIGGER IF EXISTS #{details.trigger_name} on \"#{options.schema_definition.schema_prefix}\".\"#{options.schema_definition.table_name}\";
119120
""",
120121
[]
121122
)
122-
else
123+
123124
Ecto.Adapters.SQL.query!(
124125
repo_mod,
125126
"""
126-
CREATE OR REPLACE TRIGGER #{details.trigger_name}
127+
CREATE TRIGGER #{details.trigger_name}
127128
AFTER #{update_keyword} ON \"#{options.schema_definition.schema_prefix}\".\"#{options.schema_definition.table_name}\" FOR EACH ROW
128129
EXECUTE PROCEDURE \"#{options.schema_definition.schema_prefix}\".#{details.function_name}();
129130
""",
@@ -266,39 +267,34 @@ defmodule EctoWatch.WatcherServer do
266267
end
267268

268269
defp validate_watcher_details!(watcher_details, watcher_options) do
269-
case Ecto.Adapters.SQL.query!(watcher_details.repo_mod, "SHOW max_identifier_length", []) do
270-
%Postgrex.Result{rows: [[max_identifier_length]]} ->
271-
max_identifier_length = String.to_integer(max_identifier_length)
270+
max_identifier_length =
271+
DB.max_identifier_length(watcher_details.repo_mod)
272272

273-
max_byte_size =
274-
max(
275-
byte_size(watcher_details.function_name),
276-
byte_size(watcher_details.trigger_name)
277-
)
278-
279-
if max_byte_size > max_identifier_length do
280-
difference = max_byte_size - max_identifier_length
273+
max_byte_size =
274+
max(
275+
byte_size(watcher_details.function_name),
276+
byte_size(watcher_details.trigger_name)
277+
)
281278

282-
if watcher_options.label do
283-
raise """
284-
Error for watcher: #{inspect(identifier(watcher_options))}
279+
if max_byte_size > max_identifier_length do
280+
difference = max_byte_size - max_identifier_length
285281

286-
Label is #{difference} character(s) too long to be part of the Postgres trigger name.
287-
"""
288-
else
289-
raise """
290-
Error for watcher: #{inspect(identifier(watcher_options))}
282+
if watcher_options.label do
283+
raise """
284+
Error for watcher: #{inspect(identifier(watcher_options))}
291285
292-
Schema module name is #{difference} character(s) too long for the auto-generated Postgres trigger name.
286+
Label is #{difference} character(s) too long to be part of the Postgres trigger name.
287+
"""
288+
else
289+
raise """
290+
Error for watcher: #{inspect(identifier(watcher_options))}
293291
294-
You may want to use the `label` option
292+
Schema module name is #{difference} character(s) too long for the auto-generated Postgres trigger name.
295293
296-
"""
297-
end
298-
end
294+
You may want to use the `label` option
299295
300-
other ->
301-
raise "Unexpected result when querying for max_identifier_length: #{inspect(other)}"
296+
"""
297+
end
302298
end
303299
end
304300

mix.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ defmodule EctoWatch.MixProject do
4949
{:ecto_sql, ">= 3.0.0"},
5050
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
5151
{:mix_test_watch, "~> 1.0", only: [:dev, :test]},
52-
{:credo, "~> 1.7", only: [:dev, :test], runtime: false}
52+
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
53+
{:mox, "~> 1.2", only: [:dev, :test]}
5354
]
5455
end
5556

mix.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
1414
"makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"},
1515
"mix_test_watch": {:hex, :mix_test_watch, "1.2.0", "1f9acd9e1104f62f280e30fc2243ae5e6d8ddc2f7f4dc9bceb454b9a41c82b42", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "278dc955c20b3fb9a3168b5c2493c2e5cffad133548d307e0a50c7f2cfbf34f6"},
16+
"mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"},
1617
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
18+
"nimble_ownership": {:hex, :nimble_ownership, "1.0.1", "f69fae0cdd451b1614364013544e66e4f5d25f36a2056a9698b793305c5aa3a6", [:mix], [], "hexpm", "3825e461025464f519f3f3e4a1f9b68c47dc151369611629ad08b636b73bb22d"},
1719
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
1820
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
1921
"postgrex": {:hex, :postgrex, "0.18.0", "f34664101eaca11ff24481ed4c378492fed2ff416cd9b06c399e90f321867d7e", [: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", "a042989ba1bc1cca7383ebb9e461398e3f89f868c92ce6671feb7ef132a252d1"},

test/ecto_watch_test.exs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ defmodule EctoWatchTest do
55

66
import ExUnit.CaptureLog
77

8+
import Mox
9+
10+
setup :set_mox_from_context
11+
setup :verify_on_exit!
12+
813
# TODO: Long module names (testing for limits of postgres labels)
914
# TODO: More tests for label option
1015
# TODO: Pass non-lists to `extra_columns`
@@ -74,7 +79,18 @@ defmodule EctoWatchTest do
7479
end
7580
end
7681

82+
defmodule EctoWatchDBStub do
83+
@moduledoc false
84+
85+
@behaviour EctoWatch.DB
86+
87+
def max_identifier_length(_), do: 63
88+
def major_version(_), do: 17
89+
end
90+
7791
setup do
92+
Mox.stub_with(EctoWatch.DB.Mock, EctoWatchDBStub)
93+
7894
start_supervised!(TestRepo)
7995

8096
start_supervised!({Phoenix.PubSub, name: TestPubSub})
@@ -1888,14 +1904,15 @@ defmodule EctoWatchTest do
18881904
end
18891905

18901906
describe "trigger creation" do
1891-
test "uses drop/create trigger when legacy_postgres_support? is true" do
1907+
test "uses drop/create trigger when PG major version is <= 13" do
1908+
expect(EctoWatch.DB.Mock, :major_version, fn _ -> 13 end)
1909+
18921910
log =
18931911
capture_log(fn ->
18941912
start_supervised!(
18951913
{EctoWatch,
18961914
repo: TestRepo,
18971915
pub_sub: TestPubSub,
1898-
legacy_postgres_support?: true,
18991916
watchers: [
19001917
{Thing, :inserted}
19011918
]}
@@ -1916,20 +1933,23 @@ defmodule EctoWatchTest do
19161933
)
19171934
end
19181935

1919-
test "uses create or replace trigger when legacy_postgres_support? is false" do
1936+
test "uses create or replace trigger when PG major version is >= 14" do
1937+
expect(EctoWatch.DB.Mock, :major_version, fn _ -> 14 end)
1938+
19201939
log =
19211940
capture_log(fn ->
19221941
start_supervised!(
19231942
{EctoWatch,
19241943
repo: TestRepo,
19251944
pub_sub: TestPubSub,
1926-
legacy_postgres_support?: false,
19271945
watchers: [
19281946
{Thing, :inserted}
19291947
]}
19301948
)
19311949
end)
19321950

1951+
assert log =~ "CREATE OR REPLACE TRIGGER ew_inserted_for_ectowatchtest_thing_trigger"
1952+
19331953
refute log =~ "DROP TRIGGER IF EXISTS ew_inserted_for_ectowatchtest_thing_trigger"
19341954

19351955
%Postgrex.Result{rows: [["ew_inserted_for_ectowatchtest_thing_trigger"]]} =

test/test_helper.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
Mox.defmock(EctoWatch.DB.Mock, for: EctoWatch.DB)
2+
13
ExUnit.start()

0 commit comments

Comments
 (0)