From f020a1f5a94fb51d90a9079e0c4c72bbdf0eca9a Mon Sep 17 00:00:00 2001 From: "Andrei V. Lepikhov" Date: Thu, 9 Apr 2026 13:36:26 +0200 Subject: [PATCH 1/3] Add spock.sub_alter_options() for bulk subscription option changes Introduce a new SQL-callable function spock.sub_alter_options() that accepts a subscription name and a JSONB object to modify multiple subscription settings in a single call. Recognised keys are "forward_origins" (array of origin names), "apply_delay" (interval string), and "skip_schema" (array of schema names). Unrecognised keys raise an error. Only the keys present in the JSONB object are modified; omitted settings retain their current values. This avoids the need for callers to issue multiple sub_alter_* calls when changing several options at once, and provides an extensible interface for future options without adding new C functions. Add TAP test (018_forward_origins.pl) that validates the forward_origins use case across a 3-node chain (n1 -> n2 -> n3): confirms that with forwarding disabled, n1 inserts do not reach n3, then enables forwarding via sub_alter_options and verifies n1 inserts now propagate through. --- sql/spock--5.0.6--6.0.0-devel.sql | 8 ++ sql/spock--6.0.0-devel.sql | 8 ++ src/spock_functions.c | 107 +++++++++++++++++++++++ tests/tap/schedule | 1 + tests/tap/t/018_forward_origins.pl | 135 +++++++++++++++++++++++++++++ 5 files changed, 259 insertions(+) create mode 100644 tests/tap/t/018_forward_origins.pl diff --git a/sql/spock--5.0.6--6.0.0-devel.sql b/sql/spock--5.0.6--6.0.0-devel.sql index 23492975..700774bc 100644 --- a/sql/spock--5.0.6--6.0.0-devel.sql +++ b/sql/spock--5.0.6--6.0.0-devel.sql @@ -414,3 +414,11 @@ BEGIN CALL spock.wait_for_sync_event(result, origin_id, lsn, timeout, wait_if_disabled); END; $$ LANGUAGE plpgsql; + +CREATE FUNCTION spock.sub_alter_options( + subscription_name name, + options jsonb +) +RETURNS boolean +AS 'MODULE_PATHNAME', 'spock_alter_subscription_options' +LANGUAGE C STRICT VOLATILE; diff --git a/sql/spock--6.0.0-devel.sql b/sql/spock--6.0.0-devel.sql index 882082e2..37bf1060 100644 --- a/sql/spock--6.0.0-devel.sql +++ b/sql/spock--6.0.0-devel.sql @@ -262,6 +262,14 @@ RETURNS boolean STRICT VOLATILE LANGUAGE c AS 'MODULE_PATHNAME', 'spock_alter_su CREATE FUNCTION spock.sub_alter_skiplsn(subscription_name name, lsn pg_lsn) RETURNS boolean STRICT VOLATILE LANGUAGE c AS 'MODULE_PATHNAME', 'spock_alter_subscription_skip_lsn'; +CREATE FUNCTION spock.sub_alter_options( + subscription_name name, + options jsonb +) +RETURNS boolean +AS 'MODULE_PATHNAME', 'spock_alter_subscription_options' +LANGUAGE C STRICT VOLATILE; + CREATE FUNCTION spock.sub_show_status( subscription_name name DEFAULT NULL, OUT subscription_name text, diff --git a/src/spock_functions.c b/src/spock_functions.c index c4b92117..6f9ec9e3 100644 --- a/src/spock_functions.c +++ b/src/spock_functions.c @@ -72,6 +72,7 @@ #include "utils/fmgroids.h" #include "utils/inval.h" #include "utils/json.h" +#include "utils/jsonb.h" #include "utils/guc.h" #if PG_VERSION_NUM >= 160000 #include "utils/guc_hooks.h" @@ -122,6 +123,7 @@ PG_FUNCTION_INFO_V1(spock_alter_subscription_enable); PG_FUNCTION_INFO_V1(spock_alter_subscription_add_replication_set); PG_FUNCTION_INFO_V1(spock_alter_subscription_remove_replication_set); PG_FUNCTION_INFO_V1(spock_alter_subscription_skip_lsn); +PG_FUNCTION_INFO_V1(spock_alter_subscription_options); PG_FUNCTION_INFO_V1(spock_alter_subscription_synchronize); PG_FUNCTION_INFO_V1(spock_alter_subscription_resynchronize_table); @@ -924,6 +926,111 @@ spock_alter_subscription_skip_lsn(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } +/* + * Update multiple subscription options at once mentioned at the input JSONB. + * + * Recognised keys: "forward_origins", "apply_delay", and "skip_schema". + * + * Any unrecognised key or wrong JSON type raises an ERROR immediately. + * + * Note: options that affect the replication stream (forward_origins, + * apply_delay) take effect only after the apply worker reconnects to the + * publisher. Call sub_disable() followed by sub_enable() to force an + * immediate restart. + */ +Datum +spock_alter_subscription_options(PG_FUNCTION_ARGS) +{ + char *sub_name = NameStr(*PG_GETARG_NAME(0)); + Jsonb *options = PG_GETARG_JSONB_P(1); + SpockSubscription *sub = get_subscription_by_name(sub_name, false); + JsonbIterator *it; + JsonbIteratorToken r; + JsonbValue v; + + /* XXX: Only used for locking purposes. */ + (void) get_local_node(true, false); + + it = JsonbIteratorInit(&options->root); + + r = JsonbIteratorNext(&it, &v, false); + if (r != WJB_BEGIN_OBJECT) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("subscription options must be a JSON object"))); + + while ((r = JsonbIteratorNext(&it, &v, false)) != WJB_END_OBJECT) + { + char *key; + + if (r != WJB_KEY) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("unexpected token while parsing subscription options"))); + + key = pnstrdup(v.val.string.val, v.val.string.len); + + if (strcmp(key, "forward_origins") == 0 || + strcmp(key, "skip_schema") == 0) + { + List *result = NIL; + + r = JsonbIteratorNext(&it, &v, false); + if (r != WJB_BEGIN_ARRAY) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("option \"%s\" must be a JSON array of strings", + key))); + + while ((r = JsonbIteratorNext(&it, &v, false)) != WJB_END_ARRAY) + { + if (r != WJB_ELEM || v.type != jbvString) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("option \"%s\" must contain only strings", + key))); + /* TODO: further commits should perform a precheck of the input */ + result = lappend(result, + pnstrdup(v.val.string.val, v.val.string.len)); + } + + if (strcmp(key, "forward_origins") == 0) + sub->forward_origins = result; + else + sub->skip_schema = result; + } + else if (strcmp(key, "apply_delay") == 0) + { + char *delay_str; + + r = JsonbIteratorNext(&it, &v, false); + if (r != WJB_VALUE || v.type != jbvString) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("option \"apply_delay\" must be a string"))); + + delay_str = pnstrdup(v.val.string.val, v.val.string.len); + sub->apply_delay = DatumGetIntervalP( + DirectFunctionCall3(interval_in, + CStringGetDatum(delay_str), + ObjectIdGetDatum(InvalidOid), + Int32GetDatum(-1))); + } + else + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("unrecognized subscription option \"%s\"", key), + errhint("Valid options are: \"forward_origins\", " + "\"apply_delay\", \"skip_schema\"."))); + } + } + + alter_subscription(sub); + + PG_RETURN_BOOL(true); +} + /* * Synchronize all the missing tables. */ diff --git a/tests/tap/schedule b/tests/tap/schedule index 4648b4bf..ce1ac1d9 100644 --- a/tests/tap/schedule +++ b/tests/tap/schedule @@ -43,3 +43,4 @@ test: 015_forward_origin_advance test: 016_crash_recovery_progress test: 016_sub_disable_missing_relation test: 017_zodan_3n_timeout +test: 018_forward_origins diff --git a/tests/tap/t/018_forward_origins.pl b/tests/tap/t/018_forward_origins.pl new file mode 100644 index 00000000..75323086 --- /dev/null +++ b/tests/tap/t/018_forward_origins.pl @@ -0,0 +1,135 @@ +#!/usr/bin/perl +# ============================================================================= +# Test: 018_forward_origins.pl - Verify sub_alter_options changes forward_origins +# ============================================================================= +# This test verifies that spock.sub_alter_options() correctly changes the +# forward_origins setting and that the change takes effect after the apply +# worker is restarted. +# +# Topology: +# n1 -> n2 -> n3 +# forward_origins='' (default) on both subscriptions initially +# +# Test flow: +# Phase 1 (forwarding disabled on n3): +# - Insert on n1 => n2 gets it, n3 does NOT +# - Insert on n2 => n3 gets it (n2-local data always forwarded) +# Phase 2 (forwarding enabled on n3 via sub_alter_options): +# - alter_subscription() sends SIGTERM at commit; manager restarts worker +# - Insert on n1 => n3 now gets it (forwarding active) +# ============================================================================= + +use strict; +use warnings; +use Test::More tests => 19; +use lib '.'; +use SpockTest qw(create_cluster destroy_cluster get_test_config scalar_query psql_or_bail wait_for_sub_status); + +# Poll $node_num until the scalar result of $query equals $expected, +# or until $timeout seconds elapse. Returns 1 on success, 0 on timeout. +sub wait_for_count { + my ($node_num, $query, $expected, $timeout) = @_; + $timeout //= 30; + for (1 .. $timeout) { + my $got = scalar_query($node_num, $query); + return 1 if defined $got && $got eq $expected; + sleep(1); + } + return 0; +} + +# ============================================================================= +# SETUP: Create 3-node cluster +# ============================================================================= + +create_cluster(3, 'Create 3-node cluster'); + +my $config = get_test_config(); +my $node_ports = $config->{node_ports}; +my $dbname = $config->{db_name}; +my $host = $config->{host}; + +my $conn_n1 = "host=$host port=$node_ports->[0] dbname=$dbname"; +my $conn_n2 = "host=$host port=$node_ports->[1] dbname=$dbname"; + +# ============================================================================= +# SETUP: Test table and subscriptions +# ============================================================================= + +# Create the table on n1 first so that synchronize_structure copies it to +# n2 and n3 when their subscriptions are established below. +psql_or_bail(1, "CREATE TABLE test_fwd (id integer primary key, origin text)"); + +# n2 subscribes to n1. synchronize_structure=true copies the table schema; +# synchronize_data=true copies any existing rows. forward_origins defaults +# to '{}' (empty): n1 has no upstream anyway, so this has no practical effect. +psql_or_bail(2, "SELECT spock.sub_create('sub_n1_n2', '$conn_n1', ARRAY['default'], true, true)"); +pass('Created subscription n2->n1'); +ok(wait_for_sub_status(2, 'sub_n1_n2', 'replicating', 60), 'Subscription n1->n2 is replicating'); + +# n3 subscribes to n2. forward_origins defaults to '{}' (empty) — n1 +# transactions are NOT forwarded to n3 yet. +psql_or_bail(3, "SELECT spock.sub_create('sub_n2_n3', '$conn_n2', ARRAY['default'], true, true)"); +pass('Created subscription n3->n2 with forward_origins empty'); +ok(wait_for_sub_status(3, 'sub_n2_n3', 'replicating', 60), 'Subscription n2->n3 is replicating'); + +wait_for_count(2, "SELECT COUNT(*) FROM pg_tables WHERE schemaname='public' AND tablename='test_fwd'", '1', 30); +wait_for_count(3, "SELECT COUNT(*) FROM pg_tables WHERE schemaname='public' AND tablename='test_fwd'", '1', 30); +pass('Test table present on all nodes'); + +# ============================================================================= +# PHASE 1: Verify that n3 only sees n2-local inserts +# ============================================================================= + +# Insert on n1 — forwarding is disabled on n3, so n3 should not receive it. +psql_or_bail(1, "INSERT INTO test_fwd (id, origin) VALUES (1, 'from_n1_before_fwd')"); + +ok(wait_for_count(2, "SELECT COUNT(*) FROM test_fwd WHERE origin = 'from_n1_before_fwd'", '1', 30), + 'n2 received n1 insert'); + +# Capture the current WAL position on n2, then block on n3 until it has +# consumed n2's stream past that point. A forwarded n1 row would have +# arrived before this LSN — if it hasn't arrived by now, it never will. +my $sync_lsn = scalar_query(2, "SELECT spock.sync_event()"); +psql_or_bail(3, "CALL spock.wait_for_sync_event(NULL, 'n2', '$sync_lsn', 30)"); + +my $n3_no_fwd = scalar_query(3, "SELECT COUNT(*) FROM test_fwd WHERE origin = 'from_n1_before_fwd'"); +is($n3_no_fwd, '0', 'n3 did not receive n1 insert (forwarding disabled)'); + +# Insert directly on n2 — n2-local changes are always forwarded to n3. +psql_or_bail(2, "INSERT INTO test_fwd (id, origin) VALUES (2, 'from_n2_direct')"); + +ok(wait_for_count(3, "SELECT COUNT(*) FROM test_fwd WHERE origin = 'from_n2_direct'", '1', 30), + 'n3 received n2-local insert'); + +# ============================================================================= +# PHASE 2: Enable forwarding via sub_alter_options, restart apply worker +# ============================================================================= + +psql_or_bail(3, "SELECT spock.sub_alter_options('sub_n2_n3'::name, '{\"forward_origins\": [\"all\"]}'::jsonb)"); +pass('Enabled forward_origins=all on n3 via sub_alter_options'); + +# alter_subscription() sends SIGTERM to the apply worker at commit; the +# manager restarts it automatically with the updated settings. +wait_for_sub_status(3, 'sub_n2_n3', 'down', 3); +ok(wait_for_sub_status(3, 'sub_n2_n3', 'replicating', 60), + 'Apply worker restarted with new forward_origins setting'); + +# Insert on n1 — n3 should now receive it via n2 (forwarding active). +# Walk the sync chain n1 -> n2 -> n3 to guarantee the row arrived before +# we check: if n3 has exactly 2 rows it means the new n1 insert arrived AND +# the pre-forwarding n1 insert (id=1) still did not. +psql_or_bail(1, "INSERT INTO test_fwd (id, origin) VALUES (3, 'from_n1_after_fwd')"); +$sync_lsn = scalar_query(1, "SELECT spock.sync_event()"); +psql_or_bail(2, "CALL spock.wait_for_sync_event(NULL, 'n1', '$sync_lsn', 30)"); +$sync_lsn = scalar_query(2, "SELECT spock.sync_event()"); +psql_or_bail(3, "CALL spock.wait_for_sync_event(NULL, 'n2', '$sync_lsn', 30)"); + +my $n3_count = scalar_query(3, "SELECT COUNT(*) FROM test_fwd"); +ok($n3_count eq '2', 'n3 has exactly 2 rows: n2-direct + forwarded n1 insert'); + +# ============================================================================= +# CLEANUP +# ============================================================================= + +destroy_cluster('Cleanup'); From 1ce2bd4219f0aef90a6998b7b19cff8b4aa6b2ec Mon Sep 17 00:00:00 2001 From: "Andrei V. Lepikhov" Date: Thu, 9 Apr 2026 16:17:10 +0200 Subject: [PATCH 2/3] spock.sub_alter_options: validate input, skip no-op restarts, add regression tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Harden the sub_alter_options() implementation in three ways: * No-op optimisation: track a `changed` flag through the JSONB parsing loop. alter_subscription() — which sends SIGTERM to the apply worker at commit — is only called when at least one option was actually modified. An empty object '{}' now returns false without disturbing the apply worker. * Return value: the function now returns false when nothing changed and true when at least one subscription field was updated, giving callers a reliable signal about whether a worker restart is coming. * Input validation: forward_origins elements are checked against the only supported sentinel value "all". Any other string raises INVALID_PARAMETER_VALUE with a hint, replacing the previous TODO comment. Add tests/regress/sql/alter_options.sql covering: baseline state snapshot, no-op '{}', each option individually (forward_origins, apply_delay, skip_schema), a multi-option call, baseline restore, and all expected error paths (non-object JSON, unknown key, wrong value types, invalid interval, non-existent subscription, invalid forward_origins value). --- Makefile | 2 +- src/spock_functions.c | 26 +- tests/regress/expected/alter_options.out | 389 +++++++++++++++++++++++ tests/regress/sql/alter_options.sql | 231 ++++++++++++++ 4 files changed, 642 insertions(+), 6 deletions(-) create mode 100644 tests/regress/expected/alter_options.out create mode 100644 tests/regress/sql/alter_options.sql diff --git a/Makefile b/Makefile index 772daa4b..9cee2024 100644 --- a/Makefile +++ b/Makefile @@ -54,7 +54,7 @@ REGRESS = preseed infofuncs init_fail init preseed_check basic conflict_secondar excluded_schema conflict_stat \ toasted replication_set exception_row_capture matview bidirectional primary_key \ interfaces foreign_key copy sequence triggers parallel functions row_filter \ - row_filter_sampling att_list column_filter apply_delay \ + row_filter_sampling att_list column_filter apply_delay alter_options \ extended node_origin_cascade multiple_upstreams tuple_origin autoddl \ sync_event sync_table generated_columns spill_transaction read_only drop diff --git a/src/spock_functions.c b/src/spock_functions.c index 6f9ec9e3..e1ad86cd 100644 --- a/src/spock_functions.c +++ b/src/spock_functions.c @@ -947,6 +947,7 @@ spock_alter_subscription_options(PG_FUNCTION_ARGS) JsonbIterator *it; JsonbIteratorToken r; JsonbValue v; + bool changed = false; /* XXX: Only used for locking purposes. */ (void) get_local_node(true, false); @@ -984,20 +985,33 @@ spock_alter_subscription_options(PG_FUNCTION_ARGS) while ((r = JsonbIteratorNext(&it, &v, false)) != WJB_END_ARRAY) { + char *elem; + if (r != WJB_ELEM || v.type != jbvString) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("option \"%s\" must contain only strings", key))); - /* TODO: further commits should perform a precheck of the input */ - result = lappend(result, - pnstrdup(v.val.string.val, v.val.string.len)); + + elem = pnstrdup(v.val.string.val, v.val.string.len); + + /* forward_origins only accepts the sentinel value "all" */ + if (strcmp(key, "forward_origins") == 0 && + strcmp(elem, "all") != 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid value \"%s\" for option \"forward_origins\"", + elem), + errhint("The only supported value is \"all\"."))); + + result = lappend(result, elem); } if (strcmp(key, "forward_origins") == 0) sub->forward_origins = result; else sub->skip_schema = result; + changed = true; } else if (strcmp(key, "apply_delay") == 0) { @@ -1015,6 +1029,7 @@ spock_alter_subscription_options(PG_FUNCTION_ARGS) CStringGetDatum(delay_str), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1))); + changed = true; } else { @@ -1026,9 +1041,10 @@ spock_alter_subscription_options(PG_FUNCTION_ARGS) } } - alter_subscription(sub); + if (changed) + alter_subscription(sub); - PG_RETURN_BOOL(true); + PG_RETURN_BOOL(changed); } /* diff --git a/tests/regress/expected/alter_options.out b/tests/regress/expected/alter_options.out new file mode 100644 index 00000000..98b82969 --- /dev/null +++ b/tests/regress/expected/alter_options.out @@ -0,0 +1,389 @@ +-- Tests for spock.sub_alter_options(). +-- +-- For each supported option we verify: +-- * the function accepts a valid value and returns true; +-- * the change is visible in the spock.subscription catalog (server view); +-- * the change is visible via spock.sub_show_status() (client/user view); +-- * the apply worker restarts and reaches 'replicating' again. +-- We also check all expected error paths. +SELECT * FROM spock_regress_variables() +\gset +\c :subscriber_dsn +-- Block until statement_timeout fires if subscription never reaches +-- 'replicating'; applied for the whole session from here on. +SET statement_timeout = '30s'; +-- Helper: block until the named subscription is replicating (or +-- statement_timeout fires). +CREATE OR REPLACE FUNCTION wait_replicating(sub_name name) RETURNS void AS $$ +BEGIN + WHILE EXISTS ( + SELECT 1 FROM spock.sub_show_status() + WHERE subscription_name = sub_name + AND status != 'replicating' + ) LOOP END LOOP; +END; +$$ LANGUAGE plpgsql; +-- ========================================================================= +-- Baseline: record the initial state before any changes. +-- ========================================================================= +-- Server view (catalog): +SELECT sub_name, sub_forward_origins, sub_apply_delay, sub_skip_schema +FROM spock.subscription +WHERE sub_name = 'test_subscription'; + sub_name | sub_forward_origins | sub_apply_delay | sub_skip_schema +-------------------+---------------------+-----------------+----------------- + test_subscription | | @ 0 | +(1 row) + +-- Client view (public API): +SELECT subscription_name, status, forward_origins +FROM spock.sub_show_status() +WHERE subscription_name = 'test_subscription'; + subscription_name | status | forward_origins +-------------------+-------------+----------------- + test_subscription | replicating | +(1 row) + +-- ========================================================================= +-- Empty object — no-op: nothing changes, apply worker is NOT signalled. +-- ========================================================================= +SELECT spock.sub_alter_options('test_subscription', '{}'::jsonb); + sub_alter_options +------------------- + f +(1 row) + +-- Returns false: nothing changed, alter_subscription() is skipped, apply +-- worker is never killed and the subscription stays in 'replicating'. +SELECT sub_name, sub_forward_origins, sub_apply_delay, sub_skip_schema +FROM spock.subscription +WHERE sub_name = 'test_subscription'; + sub_name | sub_forward_origins | sub_apply_delay | sub_skip_schema +-------------------+---------------------+-----------------+----------------- + test_subscription | | @ 0 | +(1 row) + +-- ========================================================================= +-- forward_origins +-- ========================================================================= +-- Enable forwarding of all origins. +SELECT spock.sub_alter_options('test_subscription', '{"forward_origins": ["all"]}'); + sub_alter_options +------------------- + t +(1 row) + +SELECT pg_sleep(1); + pg_sleep +---------- + +(1 row) + +SELECT wait_replicating('test_subscription'); + wait_replicating +------------------ + +(1 row) + +-- Server view — forward_origins should now be '{all}'. +SELECT sub_name, sub_forward_origins +FROM spock.subscription +WHERE sub_name = 'test_subscription'; + sub_name | sub_forward_origins +-------------------+--------------------- + test_subscription | {all} +(1 row) + +-- Client view — forward_origins column in sub_show_status. +SELECT subscription_name, status, forward_origins +FROM spock.sub_show_status() +WHERE subscription_name = 'test_subscription'; + subscription_name | status | forward_origins +-------------------+-------------+----------------- + test_subscription | replicating | {all} +(1 row) + +-- Clear forward_origins back to empty. +SELECT spock.sub_alter_options('test_subscription', + '{"forward_origins": []}'); + sub_alter_options +------------------- + t +(1 row) + +SELECT pg_sleep(1); + pg_sleep +---------- + +(1 row) + +SELECT wait_replicating('test_subscription'); + wait_replicating +------------------ + +(1 row) + +-- Server view — forward_origins should be NULL (empty list stored as NULL). +SELECT sub_name, sub_forward_origins +FROM spock.subscription +WHERE sub_name = 'test_subscription'; + sub_name | sub_forward_origins +-------------------+--------------------- + test_subscription | +(1 row) + +-- Client view — forward_origins should be empty / NULL. +SELECT subscription_name, status, forward_origins +FROM spock.sub_show_status() +WHERE subscription_name = 'test_subscription'; + subscription_name | status | forward_origins +-------------------+-------------+----------------- + test_subscription | replicating | +(1 row) + +-- ========================================================================= +-- apply_delay +-- ========================================================================= +-- Set a non-zero delay. +SELECT spock.sub_alter_options('test_subscription', + '{"apply_delay": "2 seconds"}'); + sub_alter_options +------------------- + t +(1 row) + +SELECT pg_sleep(1); + pg_sleep +---------- + +(1 row) + +SELECT wait_replicating('test_subscription'); + wait_replicating +------------------ + +(1 row) + +-- Server view — apply_delay should now be '00:00:02'. +SELECT sub_name, sub_apply_delay +FROM spock.subscription +WHERE sub_name = 'test_subscription'; + sub_name | sub_apply_delay +-------------------+----------------- + test_subscription | @ 2 secs +(1 row) + +-- sub_show_status does not expose apply_delay; verify status is still healthy. +SELECT subscription_name, status +FROM spock.sub_show_status() +WHERE subscription_name = 'test_subscription'; + subscription_name | status +-------------------+------------- + test_subscription | replicating +(1 row) + +-- Reset delay to zero. +SELECT spock.sub_alter_options('test_subscription', + '{"apply_delay": "0"}'); + sub_alter_options +------------------- + t +(1 row) + +SELECT pg_sleep(1); + pg_sleep +---------- + +(1 row) + +SELECT wait_replicating('test_subscription'); + wait_replicating +------------------ + +(1 row) + +SELECT sub_name, sub_apply_delay +FROM spock.subscription +WHERE sub_name = 'test_subscription'; + sub_name | sub_apply_delay +-------------------+----------------- + test_subscription | @ 0 +(1 row) + +-- ========================================================================= +-- skip_schema +-- ========================================================================= +-- Exclude a schema from apply. +SELECT spock.sub_alter_options('test_subscription', + '{"skip_schema": ["myschema"]}'); + sub_alter_options +------------------- + t +(1 row) + +SELECT pg_sleep(1); + pg_sleep +---------- + +(1 row) + +SELECT wait_replicating('test_subscription'); + wait_replicating +------------------ + +(1 row) + +-- Server view — skip_schema should be '{myschema}'. +SELECT sub_name, sub_skip_schema +FROM spock.subscription +WHERE sub_name = 'test_subscription'; + sub_name | sub_skip_schema +-------------------+----------------- + test_subscription | {myschema} +(1 row) + +-- sub_show_status does not expose skip_schema; verify status remains healthy. +SELECT subscription_name, status +FROM spock.sub_show_status() +WHERE subscription_name = 'test_subscription'; + subscription_name | status +-------------------+------------- + test_subscription | replicating +(1 row) + +-- Clear the excluded schema list. +SELECT spock.sub_alter_options('test_subscription', + '{"skip_schema": []}'); + sub_alter_options +------------------- + t +(1 row) + +SELECT pg_sleep(1); + pg_sleep +---------- + +(1 row) + +SELECT wait_replicating('test_subscription'); + wait_replicating +------------------ + +(1 row) + +SELECT sub_name, sub_skip_schema +FROM spock.subscription +WHERE sub_name = 'test_subscription'; + sub_name | sub_skip_schema +-------------------+----------------- + test_subscription | +(1 row) + +-- ========================================================================= +-- Multiple options in a single call +-- ========================================================================= +SELECT spock.sub_alter_options('test_subscription', + '{"forward_origins": ["all"], "apply_delay": "500ms", "skip_schema": ["pg_catalog"]}'); + sub_alter_options +------------------- + t +(1 row) + +SELECT pg_sleep(1); + pg_sleep +---------- + +(1 row) + +SELECT wait_replicating('test_subscription'); + wait_replicating +------------------ + +(1 row) + +-- Server view — all three columns should reflect the new values. +SELECT sub_name, sub_forward_origins, sub_apply_delay, sub_skip_schema +FROM spock.subscription +WHERE sub_name = 'test_subscription'; + sub_name | sub_forward_origins | sub_apply_delay | sub_skip_schema +-------------------+---------------------+-----------------+----------------- + test_subscription | {all} | @ 0.5 secs | {pg_catalog} +(1 row) + +-- Client view. +SELECT subscription_name, status, forward_origins +FROM spock.sub_show_status() +WHERE subscription_name = 'test_subscription'; + subscription_name | status | forward_origins +-------------------+-------------+----------------- + test_subscription | replicating | {all} +(1 row) + +-- Restore original state (empty forward_origins, zero delay, no skip_schema). +SELECT spock.sub_alter_options('test_subscription', + '{"forward_origins": [], "apply_delay": "0", "skip_schema": []}'); + sub_alter_options +------------------- + t +(1 row) + +SELECT pg_sleep(1); + pg_sleep +---------- + +(1 row) + +SELECT wait_replicating('test_subscription'); + wait_replicating +------------------ + +(1 row) + +-- Confirm baseline is restored. +SELECT sub_name, sub_forward_origins, sub_apply_delay, sub_skip_schema +FROM spock.subscription +WHERE sub_name = 'test_subscription'; + sub_name | sub_forward_origins | sub_apply_delay | sub_skip_schema +-------------------+---------------------+-----------------+----------------- + test_subscription | | @ 0 | +(1 row) + +-- ========================================================================= +-- Error cases +-- ========================================================================= +\set VERBOSITY terse +-- options argument is not a JSON object. +SELECT spock.sub_alter_options('test_subscription', '"not_an_object"'); +ERROR: subscription options must be a JSON object +-- options argument is a JSON array, not an object. +SELECT spock.sub_alter_options('test_subscription', '["all"]'); +ERROR: subscription options must be a JSON object +-- Unrecognised key. +SELECT spock.sub_alter_options('test_subscription', '{"no_such_option": "x"}'); +ERROR: unrecognized subscription option "no_such_option" +-- forward_origins: value is a string, not an array. +SELECT spock.sub_alter_options('test_subscription', '{"forward_origins": "all"}'); +ERROR: option "forward_origins" must be a JSON array of strings +-- forward_origins: value other than "all" is rejected. +SELECT spock.sub_alter_options('test_subscription', '{"forward_origins": ["meaningless"]}'); +ERROR: invalid value "meaningless" for option "forward_origins" +-- forward_origins: array contains a non-string element. +SELECT spock.sub_alter_options('test_subscription', '{"forward_origins": [1, 2]}'); +ERROR: option "forward_origins" must contain only strings +-- apply_delay: value is a number, not a string. +SELECT spock.sub_alter_options('test_subscription', '{"apply_delay": 5}'); +ERROR: option "apply_delay" must be a string +-- apply_delay: value is not a valid interval string. +SELECT spock.sub_alter_options('test_subscription', '{"apply_delay": "not_an_interval"}'); +ERROR: invalid input syntax for type interval: "not_an_interval" +-- skip_schema: value is a string, not an array. +SELECT spock.sub_alter_options('test_subscription', '{"skip_schema": "public"}'); +ERROR: option "skip_schema" must be a JSON array of strings +-- Non-existent subscription. +SELECT spock.sub_alter_options('no_such_subscription', '{"forward_origins": []}'); +ERROR: subscriber no_such_subscription not found +\set VERBOSITY default +-- ========================================================================= +-- Cleanup +-- ========================================================================= +DROP FUNCTION wait_replicating(name); diff --git a/tests/regress/sql/alter_options.sql b/tests/regress/sql/alter_options.sql new file mode 100644 index 00000000..78180564 --- /dev/null +++ b/tests/regress/sql/alter_options.sql @@ -0,0 +1,231 @@ +-- Tests for spock.sub_alter_options(). +-- +-- For each supported option we verify: +-- * the function accepts a valid value and returns true; +-- * the change is visible in the spock.subscription catalog (server view); +-- * the change is visible via spock.sub_show_status() (client/user view); +-- * the apply worker restarts and reaches 'replicating' again. +-- We also check all expected error paths. + +SELECT * FROM spock_regress_variables() +\gset + +\c :subscriber_dsn + +-- Block until statement_timeout fires if subscription never reaches +-- 'replicating'; applied for the whole session from here on. +SET statement_timeout = '30s'; + +-- Helper: block until the named subscription is replicating (or +-- statement_timeout fires). +CREATE OR REPLACE FUNCTION wait_replicating(sub_name name) RETURNS void AS $$ +BEGIN + WHILE EXISTS ( + SELECT 1 FROM spock.sub_show_status() + WHERE subscription_name = sub_name + AND status != 'replicating' + ) LOOP END LOOP; +END; +$$ LANGUAGE plpgsql; + +-- ========================================================================= +-- Baseline: record the initial state before any changes. +-- ========================================================================= + +-- Server view (catalog): +SELECT sub_name, sub_forward_origins, sub_apply_delay, sub_skip_schema +FROM spock.subscription +WHERE sub_name = 'test_subscription'; + +-- Client view (public API): +SELECT subscription_name, status, forward_origins +FROM spock.sub_show_status() +WHERE subscription_name = 'test_subscription'; + +-- ========================================================================= +-- Empty object — no-op: nothing changes, apply worker is NOT signalled. +-- ========================================================================= + +SELECT spock.sub_alter_options('test_subscription', '{}'::jsonb); + +-- Returns false: nothing changed, alter_subscription() is skipped, apply +-- worker is never killed and the subscription stays in 'replicating'. +SELECT sub_name, sub_forward_origins, sub_apply_delay, sub_skip_schema +FROM spock.subscription +WHERE sub_name = 'test_subscription'; + +-- ========================================================================= +-- forward_origins +-- ========================================================================= + +-- Enable forwarding of all origins. +SELECT spock.sub_alter_options('test_subscription', '{"forward_origins": ["all"]}'); +SELECT pg_sleep(1); +SELECT wait_replicating('test_subscription'); + +-- Server view — forward_origins should now be '{all}'. +SELECT sub_name, sub_forward_origins +FROM spock.subscription +WHERE sub_name = 'test_subscription'; + +-- Client view — forward_origins column in sub_show_status. +SELECT subscription_name, status, forward_origins +FROM spock.sub_show_status() +WHERE subscription_name = 'test_subscription'; + +-- Clear forward_origins back to empty. +SELECT spock.sub_alter_options('test_subscription', + '{"forward_origins": []}'); + +SELECT pg_sleep(1); +SELECT wait_replicating('test_subscription'); + +-- Server view — forward_origins should be NULL (empty list stored as NULL). +SELECT sub_name, sub_forward_origins +FROM spock.subscription +WHERE sub_name = 'test_subscription'; + +-- Client view — forward_origins should be empty / NULL. +SELECT subscription_name, status, forward_origins +FROM spock.sub_show_status() +WHERE subscription_name = 'test_subscription'; + +-- ========================================================================= +-- apply_delay +-- ========================================================================= + +-- Set a non-zero delay. +SELECT spock.sub_alter_options('test_subscription', + '{"apply_delay": "2 seconds"}'); + +SELECT pg_sleep(1); +SELECT wait_replicating('test_subscription'); + +-- Server view — apply_delay should now be '00:00:02'. +SELECT sub_name, sub_apply_delay +FROM spock.subscription +WHERE sub_name = 'test_subscription'; + +-- sub_show_status does not expose apply_delay; verify status is still healthy. +SELECT subscription_name, status +FROM spock.sub_show_status() +WHERE subscription_name = 'test_subscription'; + +-- Reset delay to zero. +SELECT spock.sub_alter_options('test_subscription', + '{"apply_delay": "0"}'); + +SELECT pg_sleep(1); +SELECT wait_replicating('test_subscription'); + +SELECT sub_name, sub_apply_delay +FROM spock.subscription +WHERE sub_name = 'test_subscription'; + +-- ========================================================================= +-- skip_schema +-- ========================================================================= + +-- Exclude a schema from apply. +SELECT spock.sub_alter_options('test_subscription', + '{"skip_schema": ["myschema"]}'); + +SELECT pg_sleep(1); +SELECT wait_replicating('test_subscription'); + +-- Server view — skip_schema should be '{myschema}'. +SELECT sub_name, sub_skip_schema +FROM spock.subscription +WHERE sub_name = 'test_subscription'; + +-- sub_show_status does not expose skip_schema; verify status remains healthy. +SELECT subscription_name, status +FROM spock.sub_show_status() +WHERE subscription_name = 'test_subscription'; + +-- Clear the excluded schema list. +SELECT spock.sub_alter_options('test_subscription', + '{"skip_schema": []}'); + +SELECT pg_sleep(1); +SELECT wait_replicating('test_subscription'); + +SELECT sub_name, sub_skip_schema +FROM spock.subscription +WHERE sub_name = 'test_subscription'; + +-- ========================================================================= +-- Multiple options in a single call +-- ========================================================================= + +SELECT spock.sub_alter_options('test_subscription', + '{"forward_origins": ["all"], "apply_delay": "500ms", "skip_schema": ["pg_catalog"]}'); + +SELECT pg_sleep(1); +SELECT wait_replicating('test_subscription'); + +-- Server view — all three columns should reflect the new values. +SELECT sub_name, sub_forward_origins, sub_apply_delay, sub_skip_schema +FROM spock.subscription +WHERE sub_name = 'test_subscription'; + +-- Client view. +SELECT subscription_name, status, forward_origins +FROM spock.sub_show_status() +WHERE subscription_name = 'test_subscription'; + +-- Restore original state (empty forward_origins, zero delay, no skip_schema). +SELECT spock.sub_alter_options('test_subscription', + '{"forward_origins": [], "apply_delay": "0", "skip_schema": []}'); + +SELECT pg_sleep(1); +SELECT wait_replicating('test_subscription'); + +-- Confirm baseline is restored. +SELECT sub_name, sub_forward_origins, sub_apply_delay, sub_skip_schema +FROM spock.subscription +WHERE sub_name = 'test_subscription'; + +-- ========================================================================= +-- Error cases +-- ========================================================================= + +\set VERBOSITY terse + +-- options argument is not a JSON object. +SELECT spock.sub_alter_options('test_subscription', '"not_an_object"'); + +-- options argument is a JSON array, not an object. +SELECT spock.sub_alter_options('test_subscription', '["all"]'); + +-- Unrecognised key. +SELECT spock.sub_alter_options('test_subscription', '{"no_such_option": "x"}'); + +-- forward_origins: value is a string, not an array. +SELECT spock.sub_alter_options('test_subscription', '{"forward_origins": "all"}'); + +-- forward_origins: value other than "all" is rejected. +SELECT spock.sub_alter_options('test_subscription', '{"forward_origins": ["meaningless"]}'); + +-- forward_origins: array contains a non-string element. +SELECT spock.sub_alter_options('test_subscription', '{"forward_origins": [1, 2]}'); + +-- apply_delay: value is a number, not a string. +SELECT spock.sub_alter_options('test_subscription', '{"apply_delay": 5}'); + +-- apply_delay: value is not a valid interval string. +SELECT spock.sub_alter_options('test_subscription', '{"apply_delay": "not_an_interval"}'); + +-- skip_schema: value is a string, not an array. +SELECT spock.sub_alter_options('test_subscription', '{"skip_schema": "public"}'); + +-- Non-existent subscription. +SELECT spock.sub_alter_options('no_such_subscription', '{"forward_origins": []}'); + +\set VERBOSITY default + +-- ========================================================================= +-- Cleanup +-- ========================================================================= + +DROP FUNCTION wait_replicating(name); From 89fb3344fc9bf069a8f9a3fc45f79e178e5d3246 Mon Sep 17 00:00:00 2001 From: "Andrei V. Lepikhov" Date: Fri, 10 Apr 2026 12:11:25 +0200 Subject: [PATCH 3/3] Code polishing after review --- src/spock_functions.c | 3 +- tests/regress/expected/alter_options.out | 82 +++--------------------- tests/regress/sql/alter_options.sql | 47 ++------------ 3 files changed, 14 insertions(+), 118 deletions(-) diff --git a/src/spock_functions.c b/src/spock_functions.c index e1ad86cd..cbef22b9 100644 --- a/src/spock_functions.c +++ b/src/spock_functions.c @@ -935,8 +935,7 @@ spock_alter_subscription_skip_lsn(PG_FUNCTION_ARGS) * * Note: options that affect the replication stream (forward_origins, * apply_delay) take effect only after the apply worker reconnects to the - * publisher. Call sub_disable() followed by sub_enable() to force an - * immediate restart. + * publisher. */ Datum spock_alter_subscription_options(PG_FUNCTION_ARGS) diff --git a/tests/regress/expected/alter_options.out b/tests/regress/expected/alter_options.out index 98b82969..2e41fffc 100644 --- a/tests/regress/expected/alter_options.out +++ b/tests/regress/expected/alter_options.out @@ -26,7 +26,6 @@ $$ LANGUAGE plpgsql; -- ========================================================================= -- Baseline: record the initial state before any changes. -- ========================================================================= --- Server view (catalog): SELECT sub_name, sub_forward_origins, sub_apply_delay, sub_skip_schema FROM spock.subscription WHERE sub_name = 'test_subscription'; @@ -35,7 +34,6 @@ WHERE sub_name = 'test_subscription'; test_subscription | | @ 0 | (1 row) --- Client view (public API): SELECT subscription_name, status, forward_origins FROM spock.sub_show_status() WHERE subscription_name = 'test_subscription'; @@ -85,27 +83,17 @@ SELECT wait_replicating('test_subscription'); (1 row) --- Server view — forward_origins should now be '{all}'. -SELECT sub_name, sub_forward_origins -FROM spock.subscription -WHERE sub_name = 'test_subscription'; - sub_name | sub_forward_origins --------------------+--------------------- - test_subscription | {all} -(1 row) - --- Client view — forward_origins column in sub_show_status. -SELECT subscription_name, status, forward_origins -FROM spock.sub_show_status() -WHERE subscription_name = 'test_subscription'; - subscription_name | status | forward_origins --------------------+-------------+----------------- - test_subscription | replicating | {all} +\c :provider_dsn +SELECT query ~ 'forward_origins.*all' AS new_param_active +FROM pg_stat_activity WHERE backend_type = 'walsender'; + new_param_active +------------------ + t (1 row) +\c :subscriber_dsn -- Clear forward_origins back to empty. -SELECT spock.sub_alter_options('test_subscription', - '{"forward_origins": []}'); +SELECT spock.sub_alter_options('test_subscription', '{"forward_origins": []}'); sub_alter_options ------------------- t @@ -123,24 +111,6 @@ SELECT wait_replicating('test_subscription'); (1 row) --- Server view — forward_origins should be NULL (empty list stored as NULL). -SELECT sub_name, sub_forward_origins -FROM spock.subscription -WHERE sub_name = 'test_subscription'; - sub_name | sub_forward_origins --------------------+--------------------- - test_subscription | -(1 row) - --- Client view — forward_origins should be empty / NULL. -SELECT subscription_name, status, forward_origins -FROM spock.sub_show_status() -WHERE subscription_name = 'test_subscription'; - subscription_name | status | forward_origins --------------------+-------------+----------------- - test_subscription | replicating | -(1 row) - -- ========================================================================= -- apply_delay -- ========================================================================= @@ -164,15 +134,6 @@ SELECT wait_replicating('test_subscription'); (1 row) --- Server view — apply_delay should now be '00:00:02'. -SELECT sub_name, sub_apply_delay -FROM spock.subscription -WHERE sub_name = 'test_subscription'; - sub_name | sub_apply_delay --------------------+----------------- - test_subscription | @ 2 secs -(1 row) - -- sub_show_status does not expose apply_delay; verify status is still healthy. SELECT subscription_name, status FROM spock.sub_show_status() @@ -202,14 +163,6 @@ SELECT wait_replicating('test_subscription'); (1 row) -SELECT sub_name, sub_apply_delay -FROM spock.subscription -WHERE sub_name = 'test_subscription'; - sub_name | sub_apply_delay --------------------+----------------- - test_subscription | @ 0 -(1 row) - -- ========================================================================= -- skip_schema -- ========================================================================= @@ -233,15 +186,6 @@ SELECT wait_replicating('test_subscription'); (1 row) --- Server view — skip_schema should be '{myschema}'. -SELECT sub_name, sub_skip_schema -FROM spock.subscription -WHERE sub_name = 'test_subscription'; - sub_name | sub_skip_schema --------------------+----------------- - test_subscription | {myschema} -(1 row) - -- sub_show_status does not expose skip_schema; verify status remains healthy. SELECT subscription_name, status FROM spock.sub_show_status() @@ -271,14 +215,6 @@ SELECT wait_replicating('test_subscription'); (1 row) -SELECT sub_name, sub_skip_schema -FROM spock.subscription -WHERE sub_name = 'test_subscription'; - sub_name | sub_skip_schema --------------------+----------------- - test_subscription | -(1 row) - -- ========================================================================= -- Multiple options in a single call -- ========================================================================= @@ -301,7 +237,6 @@ SELECT wait_replicating('test_subscription'); (1 row) --- Server view — all three columns should reflect the new values. SELECT sub_name, sub_forward_origins, sub_apply_delay, sub_skip_schema FROM spock.subscription WHERE sub_name = 'test_subscription'; @@ -310,7 +245,6 @@ WHERE sub_name = 'test_subscription'; test_subscription | {all} | @ 0.5 secs | {pg_catalog} (1 row) --- Client view. SELECT subscription_name, status, forward_origins FROM spock.sub_show_status() WHERE subscription_name = 'test_subscription'; diff --git a/tests/regress/sql/alter_options.sql b/tests/regress/sql/alter_options.sql index 78180564..38d8ed82 100644 --- a/tests/regress/sql/alter_options.sql +++ b/tests/regress/sql/alter_options.sql @@ -32,12 +32,10 @@ $$ LANGUAGE plpgsql; -- Baseline: record the initial state before any changes. -- ========================================================================= --- Server view (catalog): SELECT sub_name, sub_forward_origins, sub_apply_delay, sub_skip_schema FROM spock.subscription WHERE sub_name = 'test_subscription'; --- Client view (public API): SELECT subscription_name, status, forward_origins FROM spock.sub_show_status() WHERE subscription_name = 'test_subscription'; @@ -63,33 +61,18 @@ SELECT spock.sub_alter_options('test_subscription', '{"forward_origins": ["all"] SELECT pg_sleep(1); SELECT wait_replicating('test_subscription'); --- Server view — forward_origins should now be '{all}'. -SELECT sub_name, sub_forward_origins -FROM spock.subscription -WHERE sub_name = 'test_subscription'; +\c :provider_dsn --- Client view — forward_origins column in sub_show_status. -SELECT subscription_name, status, forward_origins -FROM spock.sub_show_status() -WHERE subscription_name = 'test_subscription'; +SELECT query ~ 'forward_origins.*all' AS new_param_active +FROM pg_stat_activity WHERE backend_type = 'walsender'; +\c :subscriber_dsn -- Clear forward_origins back to empty. -SELECT spock.sub_alter_options('test_subscription', - '{"forward_origins": []}'); +SELECT spock.sub_alter_options('test_subscription', '{"forward_origins": []}'); SELECT pg_sleep(1); SELECT wait_replicating('test_subscription'); --- Server view — forward_origins should be NULL (empty list stored as NULL). -SELECT sub_name, sub_forward_origins -FROM spock.subscription -WHERE sub_name = 'test_subscription'; - --- Client view — forward_origins should be empty / NULL. -SELECT subscription_name, status, forward_origins -FROM spock.sub_show_status() -WHERE subscription_name = 'test_subscription'; - -- ========================================================================= -- apply_delay -- ========================================================================= @@ -101,11 +84,6 @@ SELECT spock.sub_alter_options('test_subscription', SELECT pg_sleep(1); SELECT wait_replicating('test_subscription'); --- Server view — apply_delay should now be '00:00:02'. -SELECT sub_name, sub_apply_delay -FROM spock.subscription -WHERE sub_name = 'test_subscription'; - -- sub_show_status does not expose apply_delay; verify status is still healthy. SELECT subscription_name, status FROM spock.sub_show_status() @@ -118,10 +96,6 @@ SELECT spock.sub_alter_options('test_subscription', SELECT pg_sleep(1); SELECT wait_replicating('test_subscription'); -SELECT sub_name, sub_apply_delay -FROM spock.subscription -WHERE sub_name = 'test_subscription'; - -- ========================================================================= -- skip_schema -- ========================================================================= @@ -133,11 +107,6 @@ SELECT spock.sub_alter_options('test_subscription', SELECT pg_sleep(1); SELECT wait_replicating('test_subscription'); --- Server view — skip_schema should be '{myschema}'. -SELECT sub_name, sub_skip_schema -FROM spock.subscription -WHERE sub_name = 'test_subscription'; - -- sub_show_status does not expose skip_schema; verify status remains healthy. SELECT subscription_name, status FROM spock.sub_show_status() @@ -150,10 +119,6 @@ SELECT spock.sub_alter_options('test_subscription', SELECT pg_sleep(1); SELECT wait_replicating('test_subscription'); -SELECT sub_name, sub_skip_schema -FROM spock.subscription -WHERE sub_name = 'test_subscription'; - -- ========================================================================= -- Multiple options in a single call -- ========================================================================= @@ -164,12 +129,10 @@ SELECT spock.sub_alter_options('test_subscription', SELECT pg_sleep(1); SELECT wait_replicating('test_subscription'); --- Server view — all three columns should reflect the new values. SELECT sub_name, sub_forward_origins, sub_apply_delay, sub_skip_schema FROM spock.subscription WHERE sub_name = 'test_subscription'; --- Client view. SELECT subscription_name, status, forward_origins FROM spock.sub_show_status() WHERE subscription_name = 'test_subscription';