Skip to content

Commit ced0174

Browse files
author
Ibrar Ahmed
committed
Make the set of reserved schemas and extensions configurable
Spock keeps certain schemas and extensions out of the structure synchronization dump, and forbids their tables from being added to a replication set. This set was fixed and could only be changed by rebuilding the extension, and the two behaviours were maintained independently, so they could drift apart. Keep the set in a catalog instead, seeded with the same defaults as before: spock and snowflake are both excluded from the dump and blocked from replication sets, while lolor is excluded from the dump but still allowed in replication sets so that large objects survive dropping the extension on every node. The seeded entries are protected and cannot be removed, and an operator can now reserve additional schemas or extensions without rebuilding. Such additions are preserved across dump and restore, while the built-in entries are always re-created.
1 parent 5ed173c commit ced0174

7 files changed

Lines changed: 455 additions & 53 deletions

File tree

docs/spock_functions/repset_mgmt.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,47 @@ SELECT spock.repset_add_table('accts', 'payables');
8585
Adds a table named `payables` to a replication set named `accts`. Since no
8686
columns are specified, all columns will be replicated.
8787

88+
## Reserved Schemas and Extensions
89+
90+
Some schemas and extensions are reserved and are treated specially by Spock.
91+
They are listed in the `spock.reserved_object` catalog, which is the single
92+
source of truth for two behaviours:
93+
94+
- `exclude_from_dump`: the object is left out of the structure-sync dump used
95+
when a subscription synchronizes structure. Restoring these on a subscriber
96+
that already has them would fail.
97+
- `block_in_repset`: a table in this schema, or belonging to this extension,
98+
may not be added to a replication set.
99+
100+
Spock seeds the following built-in rows, and they cannot be removed or
101+
modified:
102+
103+
| Object | exclude_from_dump | block_in_repset |
104+
|-------------------|-------------------|-----------------|
105+
| `spock` | yes | yes |
106+
| `snowflake` | yes | yes |
107+
| `lolor` | yes | no |
108+
109+
`lolor` is excluded from the dump but is intentionally allowed in replication
110+
sets: its tables must replicate so that large objects survive a
111+
`DROP EXTENSION` on every node, not only where the drop was issued.
112+
113+
To reserve an additional schema or extension of your own, use
114+
`spock.reserved_object_add`:
115+
116+
```sql
117+
-- keep a custom schema out of structure sync and out of replication sets
118+
SELECT spock.reserved_object_add('ace', 'schema');
119+
120+
-- exclude an extension from the dump but still allow its tables in repsets
121+
SELECT spock.reserved_object_add('myext', 'extension', p_block_in_repset := false);
122+
```
123+
124+
Use `spock.reserved_object_remove('ace', 'schema')` to drop your own entry.
125+
Reserved objects are node-local configuration: apply the same additions on
126+
each node of the cluster. Operator-added rows are preserved across
127+
`pg_dump`/`pg_restore`; the built-in rows are always re-seeded.
128+
88129
## Removing a Table from a Replication Set
89130

90131
To remove a table from a replication set with Spock, connect to the server
@@ -293,3 +334,34 @@ Parameters:
293334

294335
- `subscription_name` is the name of an existing subscription.
295336
- `replication_set` is the name of replication set to remove.
337+
338+
### spock.reserved_object_add
339+
340+
Use `spock.reserved_object_add` to reserve a schema or extension (see
341+
[Reserved Schemas and Extensions](#reserved-schemas-and-extensions)). If the
342+
object is already reserved, its flags are updated. Built-in objects cannot be
343+
changed.
344+
345+
`spock.reserved_object_add(p_name name, p_kind text, p_exclude_from_dump bool
346+
DEFAULT true, p_block_in_repset bool DEFAULT true)`
347+
348+
Parameters:
349+
350+
- `p_name` is the schema or extension name.
351+
- `p_kind` is `'schema'` or `'extension'`.
352+
- `p_exclude_from_dump` keeps the object out of the structure-sync dump; the
353+
default is `true`.
354+
- `p_block_in_repset` blocks the object from replication sets; the default is
355+
`true`.
356+
357+
### spock.reserved_object_remove
358+
359+
Use `spock.reserved_object_remove` to remove one of your own reserved objects.
360+
Built-in objects are protected and cannot be removed.
361+
362+
`spock.reserved_object_remove(p_name name, p_kind text)`
363+
364+
Parameters:
365+
366+
- `p_name` is the schema or extension name.
367+
- `p_kind` is `'schema'` or `'extension'`.

include/spock_node.h

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,26 @@ typedef struct SpockSubscription
6060
TimestampTz created_at; /* When this subscription was created */
6161
} SpockSubscription;
6262

63-
/* NULL-terminated arrays */
64-
extern const char *const skip_schema[];
65-
extern const char *const skip_extension[];
63+
/*
64+
* Reserved schemas/extensions are stored in the spock.reserved_object catalog
65+
* (the single source of truth, replacing the old hard-coded C arrays).
66+
* spock_reserved_object_names() returns the names of one kind of object
67+
* reserved for one purpose.
68+
*/
69+
typedef enum ReservedObjectKind
70+
{
71+
RESERVED_KIND_SCHEMA,
72+
RESERVED_KIND_EXTENSION
73+
} ReservedObjectKind;
74+
75+
typedef enum ReservedObjectPurpose
76+
{
77+
RESERVED_PURPOSE_DUMP, /* exclude_from_dump: kept out of structure sync */
78+
RESERVED_PURPOSE_REPSET /* block_in_repset: cannot join a replication set */
79+
} ReservedObjectPurpose;
80+
81+
extern List *spock_reserved_object_names(ReservedObjectKind kind,
82+
ReservedObjectPurpose purpose);
6683

6784
extern void create_node(SpockNode *node);
6885
extern void drop_node(Oid nodeid);

sql/spock--5.0.10--6.0.0.sql

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,3 +346,76 @@ REVOKE ALL ON FUNCTION spock.slot_enable_failover() FROM PUBLIC;
346346

347347
SELECT spock.slot_enable_failover();
348348

349+
-- ----------------------------------------------------------------------------
350+
-- Reserved objects: schemas and extensions Spock treats specially.
351+
-- Single source of truth (replacing hard-coded lists in the C code) for
352+
-- exclude_from_dump (structure-sync dump exclusion) and block_in_repset
353+
-- (may not be added to a replication set). Built-in rows are protected.
354+
-- ----------------------------------------------------------------------------
355+
CREATE TABLE spock.reserved_object (
356+
name name NOT NULL,
357+
kind text NOT NULL CHECK (kind IN ('schema', 'extension')),
358+
exclude_from_dump boolean NOT NULL DEFAULT true,
359+
block_in_repset boolean NOT NULL DEFAULT true,
360+
builtin boolean NOT NULL DEFAULT false,
361+
PRIMARY KEY (name, kind)
362+
) WITH (user_catalog_table=true);
363+
364+
-- Preserve operator-added rows across pg_dump/restore; built-ins are re-seeded
365+
-- by this script, so only non-built-in rows are dumped.
366+
SELECT pg_catalog.pg_extension_config_dump('spock.reserved_object', 'WHERE NOT builtin');
367+
368+
-- lolor is excluded from the dump but NOT blocked from replication sets: its
369+
-- tables must replicate so large objects survive a DROP EXTENSION on all nodes.
370+
INSERT INTO spock.reserved_object (name, kind, exclude_from_dump, block_in_repset, builtin) VALUES
371+
('spock', 'schema', true, true, true),
372+
('spock', 'extension', true, true, true),
373+
('snowflake', 'schema', true, true, true),
374+
('snowflake', 'extension', true, true, true),
375+
('lolor', 'schema', true, false, true),
376+
('lolor', 'extension', true, false, true);
377+
378+
CREATE FUNCTION spock.reserved_object_guard()
379+
RETURNS trigger AS $$
380+
BEGIN
381+
IF OLD.builtin THEN
382+
RAISE EXCEPTION 'cannot % built-in reserved object "%" (%)',
383+
lower(TG_OP), OLD.name, OLD.kind;
384+
END IF;
385+
IF TG_OP = 'DELETE' THEN
386+
RETURN OLD;
387+
END IF;
388+
RETURN NEW;
389+
END;
390+
$$ LANGUAGE plpgsql;
391+
392+
CREATE TRIGGER reserved_object_guard
393+
BEFORE UPDATE OR DELETE ON spock.reserved_object
394+
FOR EACH ROW EXECUTE FUNCTION spock.reserved_object_guard();
395+
396+
CREATE FUNCTION spock.reserved_object_add(
397+
p_name name,
398+
p_kind text,
399+
p_exclude_from_dump boolean DEFAULT true,
400+
p_block_in_repset boolean DEFAULT true)
401+
RETURNS void
402+
LANGUAGE plpgsql AS $$
403+
BEGIN
404+
UPDATE spock.reserved_object
405+
SET exclude_from_dump = p_exclude_from_dump,
406+
block_in_repset = p_block_in_repset
407+
WHERE name = p_name AND kind = p_kind;
408+
IF NOT FOUND THEN
409+
INSERT INTO spock.reserved_object
410+
(name, kind, exclude_from_dump, block_in_repset, builtin)
411+
VALUES (p_name, p_kind, p_exclude_from_dump, p_block_in_repset, false);
412+
END IF;
413+
END;
414+
$$;
415+
416+
CREATE FUNCTION spock.reserved_object_remove(object_name name, object_kind text)
417+
RETURNS void
418+
LANGUAGE sql AS $$
419+
DELETE FROM spock.reserved_object WHERE name = object_name AND kind = object_kind;
420+
$$;
421+

sql/spock--6.0.0.sql

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,91 @@ CREATE TABLE spock.replication_set_seq (
319319
PRIMARY KEY(set_id, set_seqoid)
320320
) WITH (user_catalog_table=true);
321321

322+
-- ----------------------------------------------------------------------------
323+
-- Reserved objects: schemas and extensions Spock treats specially.
324+
--
325+
-- Single source of truth (replacing hard-coded lists in the C code) for:
326+
-- * exclude_from_dump - kept out of the structure-sync dump (pg_dump
327+
-- --exclude-schema / --exclude-extension); restoring these on a subscriber
328+
-- that already has them would fail.
329+
-- * block_in_repset - may not be added to a replication set.
330+
--
331+
-- Built-in rows (builtin = true) are seeded by the extension and are protected
332+
-- from removal/modification. Add your own with spock.reserved_object_add().
333+
-- ----------------------------------------------------------------------------
334+
CREATE TABLE spock.reserved_object (
335+
name name NOT NULL,
336+
kind text NOT NULL CHECK (kind IN ('schema', 'extension')),
337+
exclude_from_dump boolean NOT NULL DEFAULT true,
338+
block_in_repset boolean NOT NULL DEFAULT true,
339+
builtin boolean NOT NULL DEFAULT false,
340+
PRIMARY KEY (name, kind)
341+
) WITH (user_catalog_table=true);
342+
343+
-- Preserve operator-added rows across pg_dump/restore; built-ins are re-seeded
344+
-- by this script, so only non-built-in rows are dumped.
345+
SELECT pg_catalog.pg_extension_config_dump('spock.reserved_object', 'WHERE NOT builtin');
346+
347+
-- lolor is excluded from the dump but NOT blocked from replication sets: its
348+
-- tables must replicate so large objects survive a DROP EXTENSION on all nodes.
349+
INSERT INTO spock.reserved_object (name, kind, exclude_from_dump, block_in_repset, builtin) VALUES
350+
('spock', 'schema', true, true, true),
351+
('spock', 'extension', true, true, true),
352+
('snowflake', 'schema', true, true, true),
353+
('snowflake', 'extension', true, true, true),
354+
('lolor', 'schema', true, false, true),
355+
('lolor', 'extension', true, false, true);
356+
357+
-- Protect built-in rows so replication can't be broken by dropping
358+
-- spock/snowflake/lolor from the reserved set.
359+
CREATE FUNCTION spock.reserved_object_guard()
360+
RETURNS trigger AS $$
361+
BEGIN
362+
IF OLD.builtin THEN
363+
RAISE EXCEPTION 'cannot % built-in reserved object "%" (%)',
364+
lower(TG_OP), OLD.name, OLD.kind;
365+
END IF;
366+
IF TG_OP = 'DELETE' THEN
367+
RETURN OLD;
368+
END IF;
369+
RETURN NEW;
370+
END;
371+
$$ LANGUAGE plpgsql;
372+
373+
CREATE TRIGGER reserved_object_guard
374+
BEFORE UPDATE OR DELETE ON spock.reserved_object
375+
FOR EACH ROW EXECUTE FUNCTION spock.reserved_object_guard();
376+
377+
-- Add (or update) a user-defined reserved object. (ON CONFLICT is not
378+
-- allowed on a user_catalog_table, so upsert by hand; updating a built-in row
379+
-- is rejected by the guard trigger.)
380+
CREATE FUNCTION spock.reserved_object_add(
381+
p_name name,
382+
p_kind text,
383+
p_exclude_from_dump boolean DEFAULT true,
384+
p_block_in_repset boolean DEFAULT true)
385+
RETURNS void
386+
LANGUAGE plpgsql AS $$
387+
BEGIN
388+
UPDATE spock.reserved_object
389+
SET exclude_from_dump = p_exclude_from_dump,
390+
block_in_repset = p_block_in_repset
391+
WHERE name = p_name AND kind = p_kind;
392+
IF NOT FOUND THEN
393+
INSERT INTO spock.reserved_object
394+
(name, kind, exclude_from_dump, block_in_repset, builtin)
395+
VALUES (p_name, p_kind, p_exclude_from_dump, p_block_in_repset, false);
396+
END IF;
397+
END;
398+
$$;
399+
400+
-- Remove a user-defined reserved object (built-ins are protected by the guard).
401+
CREATE FUNCTION spock.reserved_object_remove(object_name name, object_kind text)
402+
RETURNS void
403+
LANGUAGE sql AS $$
404+
DELETE FROM spock.reserved_object WHERE name = object_name AND kind = object_kind;
405+
$$;
406+
322407
CREATE TABLE spock.sequence_state (
323408
seqoid oid NOT NULL PRIMARY KEY,
324409
cache_size integer NOT NULL,

0 commit comments

Comments
 (0)