Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/log-accessible-tables.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@core/sync-service': patch
---

Log all tables accessible to Electric at startup. This helps operators understand which tables the connected database user has SELECT permission on and aids in debugging permission issues.
30 changes: 30 additions & 0 deletions packages/sync-service/lib/electric/connection/manager.ex
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ defmodule Electric.Connection.Manager do
:max_shapes,
:persistent_kv,
purge_all_shapes?: false,
logged_accessible_tables?: false,
# PIDs of the database connection pools
pool_pids: %{admin: nil, snapshot: nil},
validated_connection_opts: %{replication: nil, pool: nil},
Expand All @@ -118,6 +119,7 @@ defmodule Electric.Connection.Manager do
end

use GenServer, shutdown: :infinity
alias Electric.Postgres.Configuration
alias Electric.Postgres.LockBreakerConnection
alias Electric.Connection.Manager.ConnectionBackoff
alias Electric.Connection.Manager.ConnectionResolver
Expand Down Expand Up @@ -483,6 +485,14 @@ defmodule Electric.Connection.Manager do
)
end

state =
if state.logged_accessible_tables? do
state
else
log_accessible_tables(state)
%{state | logged_accessible_tables?: true}
end

repl_sup_opts = [
stack_id: state.stack_id,
shape_cache_opts: state.shape_cache_opts,
Expand Down Expand Up @@ -1234,6 +1244,26 @@ defmodule Electric.Connection.Manager do
)
end

defp log_accessible_tables(state) do
pool = pool_name(state.stack_id, :admin)

case Configuration.run_handling_db_connection_errors(fn ->
Configuration.list_accessible_tables!(pool)
end) do
{:error, reason} ->
Logger.warning(
"Failed to list accessible tables for stack #{state.stack_id}: #{inspect(reason)}"
)

tables when is_list(tables) ->
table_names = Enum.map(tables, &Electric.Utils.relation_to_sql/1)

Logger.info(
"#{length(table_names)} tables are accessible to Electric: #{inspect(table_names)}"
)
end
end

defp should_retry_connection?(
%State{
current_phase: :connection_setup,
Expand Down
26 changes: 26 additions & 0 deletions packages/sync-service/lib/electric/postgres/configuration.ex
Original file line number Diff line number Diff line change
Expand Up @@ -382,4 +382,30 @@ defmodule Electric.Postgres.Configuration do

@spec trim_relation_with_replica(relation_with_replica()) :: Electric.oid_relation()
defp trim_relation_with_replica({oid, relation, _replident}), do: {oid, relation}

@doc """
List all tables accessible to Electric (tables the connected user has SELECT permission on).

Returns a list of `{schema, table}` tuples for ordinary and partitioned tables,
excluding system schemas (pg_catalog, information_schema).
"""
@spec list_accessible_tables!(Postgrex.conn()) :: list(Electric.relation())
def list_accessible_tables!(conn) do
%Postgrex.Result{rows: rows} =
Postgrex.query!(
conn,
"""
SELECT n.nspname, c.relname
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r', 'p')
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
AND has_table_privilege(c.oid, 'SELECT')
ORDER BY n.nspname, c.relname
""",
[]
)

Enum.map(rows, fn [schema, table] -> {schema, table} end)
end
end