Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(electric): Clean up templated SQL routines #511

Merged
merged 9 commits into from
Jan 8, 2024
3 changes: 2 additions & 1 deletion components/electric/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ COPY mix.* /app/
RUN mix deps.get
RUN mix deps.compile

COPY config/*runtime.exs /app/config/
COPY lib /app/lib/
COPY priv /app/priv
COPY src /app/src/
COPY config/*runtime.exs /app/config/

ARG ELECTRIC_VERSION=local
ARG MAKE_RELEASE_TASK=release
Expand Down
20 changes: 11 additions & 9 deletions components/electric/lib/electric/postgres/extension.ex
Original file line number Diff line number Diff line change
Expand Up @@ -287,14 +287,14 @@ defmodule Electric.Postgres.Extension do

@spec define_functions(conn) :: :ok
def define_functions(conn) do
Enum.each(Functions.list(), fn {name, sql} ->
case :epgsql.squery(conn, sql) do
{:ok, [], []} ->
Logger.debug("Successfully (re)defined SQL function/procedure '#{name}'")
:ok

error ->
raise "Failed to define function '#{name}' with error: #{inspect(error)}"
Enum.each(Functions.list(), fn {path, sql} ->
conn
|> :epgsql.squery(sql)
|> List.wrap()
|> Enum.find(&(not match?({:ok, [], []}, &1)))
|> case do
nil -> Logger.debug("Successfully (re)defined SQL routine from '#{path}'")
error -> raise "Failed to define SQL routine from '#{path}' with error: #{inspect(error)}"
end
end)
end
Expand Down Expand Up @@ -357,7 +357,9 @@ defmodule Electric.Postgres.Extension do
end)
end)

:ok = define_functions(txconn)
if module == __MODULE__ do
:ok = define_functions(txconn)
end

{:ok, newly_applied_versions}
end)
Expand Down
58 changes: 30 additions & 28 deletions components/electric/lib/electric/postgres/extension/functions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,26 @@ defmodule Electric.Postgres.Extension.Functions do
This module organizes SQL functions that are to be defined in Electric's internal database schema.
"""

alias Electric.Postgres.Extension
# Import all functions from the Extension module to make them available for calling inside SQL function templates when
# those templates are being evaludated by EEx.
import Electric.Postgres.Extension, warn: false

require EEx

sql_files =
"functions/*.sql.eex"
|> Path.expand(__DIR__)
|> Path.wildcard()
@template_dir "priv/sql_function_templates"

function_names =
for path <- sql_files do
@external_resource path
template_dir_path = Application.app_dir(:electric, @template_dir)
sql_template_paths = Path.wildcard(template_dir_path <> "/**/*.sql.eex")

function_paths =
for path <- sql_template_paths do
relpath = Path.relative_to(path, template_dir_path)
name = path |> Path.basename(".sql.eex") |> String.to_atom()
_ = EEx.function_from_file(:def, name, path, [:assigns])

name
{relpath, name}
end

function_names = for {_relpath, name} <- function_paths, do: name

fn_name_type =
Enum.reduce(function_names, fn name, code ->
quote do
Expand All @@ -29,22 +31,23 @@ defmodule Electric.Postgres.Extension.Functions do
end)

@typep name :: unquote(fn_name_type)
@typep sql :: String.t()
@type function_list :: [{name, sql}]
@typep sql :: binary
@type function_list :: [{Path.t(), sql}]

@function_paths function_paths
@function_names function_names

@doc """
Get a list of `{name, SQL}` pairs where the the SQL code contains the definition of a function (or multiple functions).
Get a list of `{name, SQL}` pairs where the SQL code contains the definition of a function (or multiple functions).

Every function in the list is defined as `CREATE OR REPLACE FUNCTION`.
"""
# NOTE(alco): Eventually, we're hoping to move all function definitions out of migrations and define them all
# here. See VAX-1016 for details.
@spec list :: function_list
def list do
for name <- @function_names do
{name, by_name(name)}
for {relpath, _name} <- @function_paths do
{relpath, eval_template(relpath)}
end
end

Expand All @@ -56,19 +59,18 @@ defmodule Electric.Postgres.Extension.Functions do
"""
@spec by_name(name) :: sql
def by_name(name) when name in @function_names do
apply(__MODULE__, name, [assigns()])
{relpath, ^name} = List.keyfind(@function_paths, name, 1)
eval_template(relpath)
end

# This map of assigns is the same for all function templates.
defp assigns do
%{
schema: Extension.schema(),
ddl_table: Extension.ddl_table(),
txid_type: Extension.txid_type(),
txts_type: Extension.txts_type(),
version_table: Extension.version_table(),
electrified_tracking_table: Extension.electrified_tracking_table(),
publication_name: Extension.publication_name()
}
defp eval_template(relpath) do
# This hack is necessary to get a meaningful error when EEx fails to evaluate the template.
# Without it, errors will say that it is lib/electric/postgres/extension/functions.ex that failed to compile.
env = %{__ENV__ | file: relpath}

Path.join(Application.app_dir(:electric, @template_dir), relpath)
|> EEx.compile_file(file: relpath)
|> Code.eval_quoted([], env)
|> elem(0)
end
end

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ defmodule Electric.Postgres.Extension.Migrations.Migration_20230328113927 do
""",
##################
"""
CREATE PUBLICATION "#{publication_name}";
CREATE PUBLICATION "#{publication_name}";
""",
Extension.add_table_to_publication_sql(ddl_table)
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@ defmodule Electric.Postgres.Extension.Migrations.Migration_20230512000000_confli
def up(schema) do
[
@contents["electric_tag_type_and_operators"],
@contents["utility_functions"],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing that this is ok and referencing functions that don't exist is not a problem -- if so then good to get rid of the duplication

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It depends on the context. If you have a function that calls another function, it is fine for the former to be defined before the latter.

On the other hand, if you have a trigger definition that references a function, the function has to be defined prior to the evaluation of the trigger def.

# This function definition is included here because it is referenced in the definition of
# "trigger_function_installers" below it.
Extension.Functions.by_name(:perform_reordered_op_installer_function),
Extension.Functions.by_name(:__session_replication_role),
@contents["trigger_function_installers"],
@contents["shadow_table_creation_and_update"]
# We need to actually run shadow table creation/updates, but that's handled in the next migration.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
defmodule Electric.Postgres.Extension.Migrations.Migration_20230605141256_ElectrifyFunction do
alias Electric.Postgres.Extension

require EEx

@behaviour Extension.Migration

sql_template = Path.expand("20230605141256_electrify_function/electrify.sql.eex", __DIR__)

@external_resource sql_template

@impl true
def version, do: 2023_06_05_14_12_56

Expand All @@ -25,12 +19,10 @@ defmodule Electric.Postgres.Extension.Migrations.Migration_20230605141256_Electr
oid oid NOT NULL,
created_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_table_name UNIQUE (schema_name, table_name)
);
""",
"""
CREATE INDEX electrified_tracking_table_name_idx ON #{electrified_tracking_table} (schema_name, table_name);
CREATE INDEX electrified_tracking_table_name_oid ON #{electrified_tracking_table} (oid);
)
""",
"CREATE INDEX electrified_tracking_table_name_idx ON #{electrified_tracking_table} (schema_name, table_name)",
"CREATE INDEX electrified_tracking_table_name_oid ON #{electrified_tracking_table} (oid)",
Extension.add_table_to_publication_sql(electrified_tracking_table)
]
end
Expand Down
Loading
Loading