From f62bc633a806a7bd64ab75cf9a7a16fb37e44cb3 Mon Sep 17 00:00:00 2001 From: "Andrei V. Lepikhov" Date: Mon, 24 Nov 2025 15:41:05 +0100 Subject: [PATCH 1/8] Introduce a delta_apply security label management. With this commit, we add the 'spock.delta_apply' routine, which adds or removes a security label on a specific table column. It is mostly useless for now. It demonstrates how to consistently manage security labels and survive ALTER/DROP DDL commands, including column type change. Trivial regression tests are added to fix the appropriate behaviour. --- include/spock.h | 1 + sql/spock--6.0.0-devel.sql | 54 ++++++++++++++++++ src/spock.c | 30 ++++++++++ src/spock_executor.c | 65 +++++++++++++++++++++- tests/regress/expected/basic.out | 82 ++++++++++++++++++++++++++++ tests/regress/expected/infofuncs.out | 20 +++++++ tests/regress/sql/basic.sql | 32 +++++++++++ tests/regress/sql/infofuncs.sql | 8 +++ 8 files changed, 291 insertions(+), 1 deletion(-) diff --git a/include/spock.h b/include/spock.h index a4bc2563..508ead26 100644 --- a/include/spock.h +++ b/include/spock.h @@ -28,6 +28,7 @@ #define SPOCK_VERSION_NUM 60000 #define EXTENSION_NAME "spock" +#define spock_SECLABEL_PROVIDER "spock" #define REPLICATION_ORIGIN_ALL "all" diff --git a/sql/spock--6.0.0-devel.sql b/sql/spock--6.0.0-devel.sql index 1ad9a9a8..52bc546e 100644 --- a/sql/spock--6.0.0-devel.sql +++ b/sql/spock--6.0.0-devel.sql @@ -633,3 +633,57 @@ BEGIN END LOOP; END; $$ LANGUAGE plpgsql; + +-- Set delta_apply security label on specific column +CREATE FUNCTION spock.delta_apply( + rel regclass, + att_name name, + to_drop boolean DEFAULT false +) RETURNS boolean AS $$ +DECLARE + label text; + atttype name; + attdata record; + ctypname name; +BEGIN + IF (to_drop = true) THEN + DELETE FROM pg_seclabel WHERE objoid = rel AND + classoid = 'pg_class'::regclass AND + objsubid > 0 AND provider = 'spock'; + IF NOT FOUND THEN + RETURN false; + END IF; + + RETURN true; + END IF; + + -- + -- Find proper delta_apply function for the column type or ERROR + -- + + SELECT t.typname,t.typinput,t.typoutput + FROM pg_catalog.pg_attribute a, pg_type t + WHERE a.attrelid = rel AND a.attname = att_name AND (a.atttypid = t.oid) + INTO attdata; + IF NOT FOUND THEN + RAISE EXCEPTION 'column % does not exist in the table %', att_name, rel; + END IF; + + SELECT typname FROM pg_type WHERE + typname IN ('int2','int4','int8','float4','float8','numeric','money') AND + typinput = attdata.typinput AND typoutput = attdata.typoutput + INTO ctypname; + IF NOT FOUND THEN + RAISE EXCEPTION 'type "%" can not be used in delta_apply conflict resolution', + attdata.typname; + END IF; + + -- + -- Create security label on the column + -- + EXECUTE format('SECURITY LABEL FOR spock ON COLUMN %I.%I IS %L' , + rel, att_name, 'delta_apply'); + + RETURN true; +END; +$$ LANGUAGE plpgsql STRICT VOLATILE; diff --git a/src/spock.c b/src/spock.c index 27c6ee38..be47fe30 100644 --- a/src/spock.c +++ b/src/spock.c @@ -25,6 +25,7 @@ #include "catalog/pg_type.h" #include "commands/extension.h" +#include "commands/seclabel.h" #include "executor/executor.h" @@ -900,6 +901,32 @@ log_message_filter(ErrorData *edata) } } +/* + * Spock security label hook. + * + * Just to be sure user adds a proper label: only table columns may be applied. + */ +static void +spock_object_relabel(const ObjectAddress *object, const char *seclabel) +{ + Oid extoid; + + extoid = get_extension_oid(EXTENSION_NAME, true); + if (!OidIsValid(extoid)) + elog(ERROR, "spock extension is not created yet"); + + /* + * Check: classId must be pg_class, objectId should an existing table and + * attnum must be more than 0. + */ + if (object->classId != RelationRelationId || object->objectSubId <= 0 || + !SearchSysCacheExists1(RELOID, ObjectIdGetDatum(object->objectId))) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("spock provider does not support labels on %s", + getObjectTypeDescription(object, false)))); +} + /* * Entry point for this module. */ @@ -1171,4 +1198,7 @@ _PG_init(void) /* General-purpose message filter */ prev_emit_log_hook = emit_log_hook; emit_log_hook = log_message_filter; + + /* Security label provider hook */ + register_label_provider(spock_SECLABEL_PROVIDER, spock_object_relabel); } diff --git a/src/spock_executor.c b/src/spock_executor.c index 78c13c45..6512da1a 100644 --- a/src/spock_executor.c +++ b/src/spock_executor.c @@ -20,16 +20,18 @@ #include "catalog/dependency.h" #include "catalog/index.h" +#include "catalog/indexing.h" #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_authid_d.h" #include "catalog/pg_extension.h" #include "catalog/pg_inherits.h" +#include "catalog/pg_seclabel.h" #include "catalog/pg_type.h" #include "commands/defrem.h" #include "commands/extension.h" - +#include "commands/seclabel.h" #include "executor/executor.h" #include "nodes/nodeFuncs.h" @@ -189,6 +191,40 @@ spock_ProcessUtility(PlannedStmt *pstmt, const char *queryString, spock_autoddl_process(pstmt, queryString, context, toplevel_stmt); } +/* + * Derived from the core DeleteSecurityLabel routine + */ +static void +DeleteSecurityLabels(const char *provider) +{ + Relation pg_seclabel; + SysScanDesc scan; + HeapTuple htup; + + pg_seclabel = table_open(SecLabelRelationId, RowExclusiveLock); + + scan = systable_beginscan(pg_seclabel, InvalidOid, false, NULL, 0, NULL); + while (HeapTupleIsValid(htup = systable_getnext(scan))) + { + Datum datum; + bool isnull; + char *provider; + + datum = heap_getattr(htup, Anum_pg_seclabel_provider, + RelationGetDescr(pg_seclabel), &isnull); + Assert(!isnull); + provider = TextDatumGetCString(datum); + + if (strcmp(provider, spock_SECLABEL_PROVIDER) != 0) + continue; + + CatalogTupleDelete(pg_seclabel, &htup->t_self); + } + + systable_endscan(scan); + table_close(pg_seclabel, RowExclusiveLock); +} + /* * Handle object drop. * @@ -203,6 +239,7 @@ spock_object_access(ObjectAccessType access, { Oid save_userid = 0; int save_sec_context = 0; + ObjectAddress object; if (next_object_access_hook) (*next_object_access_hook) (access, classId, objectId, subId, arg); @@ -241,7 +278,11 @@ spock_object_access(ObjectAccessType access, * be handled by Postgres. */ if (dropping_spock_obj) + { + /* Need to drop any security labels created by the extension */ + DeleteSecurityLabels(spock_SECLABEL_PROVIDER); return; + } /* * Check that we have a local node. We need to elevate access because @@ -269,6 +310,28 @@ spock_object_access(ObjectAccessType access, /* Restore previous session privileges */ SetUserIdAndSecContext(save_userid, save_sec_context); } + /* SECURITY LABEL related section (see delta_apply for more details) */ + else if (access == OAT_POST_ALTER && subId > 0) + { + char *label; + + /* + * Something changes in the definition of the column. We have not enough + * data at the moment to check if the column will satisfy delta_apply + * type requirements. So, just warn and drop security label, if exists. + * TODO: the direction of further improvement is discovery of syscache + * or querying the table definition in attempt to identify the new type. + */ + ObjectAddressSubSet(object, classId, objectId, subId); + label = GetSecurityLabel(&object, "spock"); + if (label != NULL) + { + DeleteSecurityLabel(&object); + elog(WARNING, "the alter column statement removes spock security label '%s' on this column", + label); + pfree(label); + } + } } void diff --git a/tests/regress/expected/basic.out b/tests/regress/expected/basic.out index 642df00c..7cbab6d8 100644 --- a/tests/regress/expected/basic.out +++ b/tests/regress/expected/basic.out @@ -211,3 +211,85 @@ CREATE FUNCTION call_fn(creds text) RETURNS void AS $$ $$ LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE; SELECT call_fn(:fakecreds); ERROR: dsn "dbname=regression passwordXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +DROP FUNCTION call_fn; +-- +-- Basic SECURITY LABEL tests. Fix limits of acceptable behavior. +-- Remember, these tests still check intra-node behaviour. +-- +-- A label creation checks +CREATE TABLE slabel (x integer, y text PRIMARY KEY); +SELECT spock.delta_apply('slabel', 'x'); + delta_apply +------------- + t +(1 row) + +SELECT spock.delta_apply('slabel', 'y'); -- ERROR +ERROR: type "text" can not be used in delta_apply conflict resolution +SELECT spock.delta_apply('slabel', 'z'); -- ERROR +ERROR: column z does not exist in the table slabel +SELECT spock.delta_apply('slabel', 'x'); -- repeating call do nothing + delta_apply +------------- + t +(1 row) + +SELECT objname, label FROM pg_seclabels; + objname | label +----------+------------- + slabel.x | delta_apply +(1 row) + +-- Label drop checks +SELECT spock.delta_apply('slabel', 'x', true); + delta_apply +------------- + t +(1 row) + +SELECT spock.delta_apply('slabel', 'y', true); + delta_apply +------------- + f +(1 row) + +SELECT spock.delta_apply('slabel', 'z', true); + delta_apply +------------- + f +(1 row) + +SELECT objname, label FROM pg_seclabels; + objname | label +---------+------- +(0 rows) + +-- Dependencies +SELECT spock.delta_apply('slabel', 'x', false); + delta_apply +------------- + t +(1 row) + +ALTER TABLE slabel ALTER COLUMN x TYPE text; -- just warn +WARNING: the alter column statement removes spock security label 'delta_apply' on this column +SELECT objname, label FROM pg_seclabels; + objname | label +---------+------- +(0 rows) + +ALTER TABLE slabel DROP COLUMN x; +ALTER TABLE slabel ADD COLUMN x numeric; +SELECT spock.delta_apply('slabel', 'x', false); + delta_apply +------------- + t +(1 row) + +ALTER TABLE slabel DROP COLUMN x; +SELECT objname, label FROM pg_seclabels; + objname | label +---------+------- +(0 rows) + +DROP TABLE slabel; diff --git a/tests/regress/expected/infofuncs.out b/tests/regress/expected/infofuncs.out index 02b33bfc..1a7bf836 100644 --- a/tests/regress/expected/infofuncs.out +++ b/tests/regress/expected/infofuncs.out @@ -20,4 +20,24 @@ WHERE extname = 'spock'; t (1 row) +-- Check that security label is cleaned up on the extension drop +CREATE TABLE slabel (x money, y text PRIMARY KEY); +SELECT spock.delta_apply('slabel', 'x', false); + delta_apply +------------- + t +(1 row) + +SELECT objname, label FROM pg_seclabels; + objname | label +----------+------------- + slabel.x | delta_apply +(1 row) + DROP EXTENSION spock; +SELECT objname, label FROM pg_seclabels; + objname | label +---------+------- +(0 rows) + +DROP TABLE slabel; diff --git a/tests/regress/sql/basic.sql b/tests/regress/sql/basic.sql index 9bf4900d..6e1db841 100644 --- a/tests/regress/sql/basic.sql +++ b/tests/regress/sql/basic.sql @@ -110,3 +110,35 @@ CREATE FUNCTION call_fn(creds text) RETURNS void AS $$ $$ LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE; SELECT call_fn(:fakecreds); +DROP FUNCTION call_fn; + +-- +-- Basic SECURITY LABEL tests. Fix limits of acceptable behavior. +-- Remember, these tests still check intra-node behaviour. +-- + +-- A label creation checks +CREATE TABLE slabel (x integer, y text PRIMARY KEY); +SELECT spock.delta_apply('slabel', 'x'); +SELECT spock.delta_apply('slabel', 'y'); -- ERROR +SELECT spock.delta_apply('slabel', 'z'); -- ERROR +SELECT spock.delta_apply('slabel', 'x'); -- repeating call do nothing +SELECT objname, label FROM pg_seclabels; + +-- Label drop checks +SELECT spock.delta_apply('slabel', 'x', true); +SELECT spock.delta_apply('slabel', 'y', true); +SELECT spock.delta_apply('slabel', 'z', true); +SELECT objname, label FROM pg_seclabels; + +-- Dependencies +SELECT spock.delta_apply('slabel', 'x', false); +ALTER TABLE slabel ALTER COLUMN x TYPE text; -- just warn +SELECT objname, label FROM pg_seclabels; +ALTER TABLE slabel DROP COLUMN x; +ALTER TABLE slabel ADD COLUMN x numeric; +SELECT spock.delta_apply('slabel', 'x', false); +ALTER TABLE slabel DROP COLUMN x; +SELECT objname, label FROM pg_seclabels; + +DROP TABLE slabel; diff --git a/tests/regress/sql/infofuncs.sql b/tests/regress/sql/infofuncs.sql index ee7536ac..c0b19cdb 100644 --- a/tests/regress/sql/infofuncs.sql +++ b/tests/regress/sql/infofuncs.sql @@ -9,4 +9,12 @@ SELECT spock.spock_version() = extversion FROM pg_extension WHERE extname = 'spock'; +-- Check that security label is cleaned up on the extension drop +CREATE TABLE slabel (x money, y text PRIMARY KEY); +SELECT spock.delta_apply('slabel', 'x', false); +SELECT objname, label FROM pg_seclabels; + DROP EXTENSION spock; + +SELECT objname, label FROM pg_seclabels; +DROP TABLE slabel; From 18b21f53640511f87cc39833e1a50ffe34df8dbb Mon Sep 17 00:00:00 2001 From: "Andrei V. Lepikhov" Date: Tue, 25 Nov 2025 13:00:39 +0100 Subject: [PATCH 2/8] Replicate SECURITY LABEL commands. pg_seclabel is a pg_catalog table, and the core forbids LR on it. Hence, we need to allow this non-DDL utility command to be replicated. During creation/removal of such a label, warn if auto-ddl replication is disabled. --- sql/spock--6.0.0-devel.sql | 41 +++++++++++++---------- src/spock_apply.c | 5 ++- src/spock_autoddl.c | 5 +-- tests/regress/expected/autoddl.out | 53 +++++++++++++++++++++++++++++- tests/regress/expected/basic.out | 25 ++++++++------ tests/regress/sql/autoddl.sql | 21 +++++++++++- tests/regress/sql/basic.sql | 6 ++++ 7 files changed, 124 insertions(+), 32 deletions(-) diff --git a/sql/spock--6.0.0-devel.sql b/sql/spock--6.0.0-devel.sql index 52bc546e..ae42639c 100644 --- a/sql/spock--6.0.0-devel.sql +++ b/sql/spock--6.0.0-devel.sql @@ -641,22 +641,13 @@ CREATE FUNCTION spock.delta_apply( to_drop boolean DEFAULT false ) RETURNS boolean AS $$ DECLARE - label text; - atttype name; - attdata record; - ctypname name; + label text; + atttype name; + attdata record; + ctypname name; + sqlstring text; + status boolean; BEGIN - IF (to_drop = true) THEN - DELETE FROM pg_seclabel WHERE objoid = rel AND - classoid = 'pg_class'::regclass AND - objsubid > 0 AND provider = 'spock'; - IF NOT FOUND THEN - RETURN false; - END IF; - - RETURN true; - END IF; - -- -- Find proper delta_apply function for the column type or ERROR -- @@ -681,8 +672,24 @@ BEGIN -- -- Create security label on the column -- - EXECUTE format('SECURITY LABEL FOR spock ON COLUMN %I.%I IS %L' , - rel, att_name, 'delta_apply'); + IF (to_drop = true) THEN + sqlstring := format('SECURITY LABEL FOR spock ON COLUMN %I.%I IS NULL;' , + rel, att_name); + ELSE + sqlstring := format('SECURITY LABEL FOR spock ON COLUMN %I.%I IS %L;' , + rel, att_name, 'delta_apply'); + END IF; + + EXECUTE sqlstring; + + /* + * Auto replication will propagate security label if needed. Just warn if it's + * not - the structure sync pg_dump call would copy security labels, isn't it? + */ + SELECT pg_catalog.current_setting('spock.enable_ddl_replication') INTO status; + IF EXISTS (SELECT 1 FROM spock.local_node) AND status = false THEN + raise WARNING 'delta_apply setting has not been propagated to other spock nodes'; + END IF; RETURN true; END; diff --git a/src/spock_apply.c b/src/spock_apply.c index c40a8303..fb8b5d08 100644 --- a/src/spock_apply.c +++ b/src/spock_apply.c @@ -3425,9 +3425,12 @@ spock_execute_sql_command(char *cmdstr, char *role, bool isTopLevel) /* * check if it's a DDL statement. we only do this for * in_spock_replicate_ddl_command + * SECURITY LABEL command is not a DDL, just an utility one. Hence, let + * spock execute this command. */ if (in_spock_replicate_ddl_command && - GetCommandLogLevel(command->stmt) != LOGSTMT_DDL) + GetCommandLogLevel(command->stmt) != LOGSTMT_DDL && + !IsA(command->stmt, SecLabelStmt)) { ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), diff --git a/src/spock_autoddl.c b/src/spock_autoddl.c index 5aff9ea7..0428aa5d 100644 --- a/src/spock_autoddl.c +++ b/src/spock_autoddl.c @@ -331,8 +331,9 @@ autoddl_can_proceed(Node *parsetree, ProcessUtilityContext context, */ return false; - /* Only process DDL statements */ - if (GetCommandLogLevel(parsetree) != LOGSTMT_DDL) + /* Only process DDL statements and SECURITY LABEL's */ + if (GetCommandLogLevel(parsetree) != LOGSTMT_DDL && + !IsA(parsetree, SecLabelStmt)) return false; /* If DDL replication is disabled, do nothing */ diff --git a/tests/regress/expected/autoddl.out b/tests/regress/expected/autoddl.out index 09cc8039..afd630ed 100644 --- a/tests/regress/expected/autoddl.out +++ b/tests/regress/expected/autoddl.out @@ -100,9 +100,60 @@ WARNING: This DDL statement will not be replicated. DROP TABLE test_383 CASCADE; NOTICE: drop cascades to table test_383 membership in replication set default_insert_only INFO: DDL statement replicated. +\c :provider_dsn +\set VERBOSITY terse +-- Check propagation of security labels +CREATE TABLE slabel1 (x integer, y text PRIMARY KEY); +INFO: DDL statement replicated. +SELECT spock.delta_apply('slabel1', 'x'); +INFO: DDL statement replicated. + delta_apply +------------- + t +(1 row) + +SELECT spock.delta_apply('slabel1', 'y'); -- ERROR +ERROR: type "text" can not be used in delta_apply conflict resolution +SELECT spock.delta_apply('slabel1', 'z'); -- ERROR +ERROR: column z does not exist in the table slabel1 +SELECT spock.delta_apply('slabel1', 'x'); -- repeating call do nothing +INFO: DDL statement replicated. + delta_apply +------------- + t +(1 row) + +SELECT objname, label FROM pg_seclabels; + objname | label +-----------+------------- + slabel1.x | delta_apply +(1 row) + +-- Short round trip to check that subscriber has the security label \c :subscriber_dsn --- Reset the configuration to the default value +SELECT objname, label FROM pg_seclabels; + objname | label +-----------+------------- + slabel1.x | delta_apply +(1 row) + \c :provider_dsn +SELECT spock.delta_apply('slabel1', 'x', true); +INFO: DDL statement replicated. + delta_apply +------------- + t +(1 row) + +-- Short round trip to check that subscriber has removed the security label too +\c :subscriber_dsn +SELECT objname, label FROM pg_seclabels; + objname | label +---------+------- +(0 rows) + +\c :provider_dsn +-- Reset the configuration to the default value ALTER SYSTEM SET spock.enable_ddl_replication = 'off'; WARNING: This DDL statement will not be replicated. ALTER SYSTEM SET spock.include_ddl_repset = 'off'; diff --git a/tests/regress/expected/basic.out b/tests/regress/expected/basic.out index 7cbab6d8..b65c726d 100644 --- a/tests/regress/expected/basic.out +++ b/tests/regress/expected/basic.out @@ -219,6 +219,7 @@ DROP FUNCTION call_fn; -- A label creation checks CREATE TABLE slabel (x integer, y text PRIMARY KEY); SELECT spock.delta_apply('slabel', 'x'); +WARNING: delta_apply setting has not been propagated to other spock nodes delta_apply ------------- t @@ -229,6 +230,7 @@ ERROR: type "text" can not be used in delta_apply conflict resolution SELECT spock.delta_apply('slabel', 'z'); -- ERROR ERROR: column z does not exist in the table slabel SELECT spock.delta_apply('slabel', 'x'); -- repeating call do nothing +WARNING: delta_apply setting has not been propagated to other spock nodes delta_apply ------------- t @@ -240,25 +242,26 @@ SELECT objname, label FROM pg_seclabels; slabel.x | delta_apply (1 row) +-- Short round trip to check that subscriber has no security labels +\c :subscriber_dsn +SELECT objname, label FROM pg_seclabels; + objname | label +---------+------- +(0 rows) + +\c :provider_dsn -- Label drop checks SELECT spock.delta_apply('slabel', 'x', true); +WARNING: delta_apply setting has not been propagated to other spock nodes delta_apply ------------- t (1 row) SELECT spock.delta_apply('slabel', 'y', true); - delta_apply -------------- - f -(1 row) - +ERROR: type "text" can not be used in delta_apply conflict resolution SELECT spock.delta_apply('slabel', 'z', true); - delta_apply -------------- - f -(1 row) - +ERROR: column z does not exist in the table slabel SELECT objname, label FROM pg_seclabels; objname | label ---------+------- @@ -266,6 +269,7 @@ SELECT objname, label FROM pg_seclabels; -- Dependencies SELECT spock.delta_apply('slabel', 'x', false); +WARNING: delta_apply setting has not been propagated to other spock nodes delta_apply ------------- t @@ -281,6 +285,7 @@ SELECT objname, label FROM pg_seclabels; ALTER TABLE slabel DROP COLUMN x; ALTER TABLE slabel ADD COLUMN x numeric; SELECT spock.delta_apply('slabel', 'x', false); +WARNING: delta_apply setting has not been propagated to other spock nodes delta_apply ------------- t diff --git a/tests/regress/sql/autoddl.sql b/tests/regress/sql/autoddl.sql index e6e18fcb..71698376 100644 --- a/tests/regress/sql/autoddl.sql +++ b/tests/regress/sql/autoddl.sql @@ -65,10 +65,29 @@ CREATE INDEX test_383_y_idx ON test_383 (a); CLUSTER test_383 USING test_383_y_idx; -- Should not be replicated DROP TABLE test_383 CASCADE; +\c :provider_dsn +\set VERBOSITY terse +-- Check propagation of security labels +CREATE TABLE slabel1 (x integer, y text PRIMARY KEY); + +SELECT spock.delta_apply('slabel1', 'x'); +SELECT spock.delta_apply('slabel1', 'y'); -- ERROR +SELECT spock.delta_apply('slabel1', 'z'); -- ERROR +SELECT spock.delta_apply('slabel1', 'x'); -- repeating call do nothing +SELECT objname, label FROM pg_seclabels; + +-- Short round trip to check that subscriber has the security label +\c :subscriber_dsn +SELECT objname, label FROM pg_seclabels; +\c :provider_dsn + +SELECT spock.delta_apply('slabel1', 'x', true); +-- Short round trip to check that subscriber has removed the security label too \c :subscriber_dsn +SELECT objname, label FROM pg_seclabels; +\c :provider_dsn -- Reset the configuration to the default value -\c :provider_dsn ALTER SYSTEM SET spock.enable_ddl_replication = 'off'; ALTER SYSTEM SET spock.include_ddl_repset = 'off'; ALTER SYSTEM SET spock.allow_ddl_from_functions = 'off'; diff --git a/tests/regress/sql/basic.sql b/tests/regress/sql/basic.sql index 6e1db841..cdf16fe4 100644 --- a/tests/regress/sql/basic.sql +++ b/tests/regress/sql/basic.sql @@ -119,12 +119,18 @@ DROP FUNCTION call_fn; -- A label creation checks CREATE TABLE slabel (x integer, y text PRIMARY KEY); + SELECT spock.delta_apply('slabel', 'x'); SELECT spock.delta_apply('slabel', 'y'); -- ERROR SELECT spock.delta_apply('slabel', 'z'); -- ERROR SELECT spock.delta_apply('slabel', 'x'); -- repeating call do nothing SELECT objname, label FROM pg_seclabels; +-- Short round trip to check that subscriber has no security labels +\c :subscriber_dsn +SELECT objname, label FROM pg_seclabels; +\c :provider_dsn + -- Label drop checks SELECT spock.delta_apply('slabel', 'x', true); SELECT spock.delta_apply('slabel', 'y', true); From 149c5857a72baab3b0f774c4334228a242c78d54 Mon Sep 17 00:00:00 2001 From: "Andrei V. Lepikhov" Date: Wed, 26 Nov 2025 16:46:24 +0100 Subject: [PATCH 3/8] Use security labels to implement the delta_apply feature. The general idea is to utilise the REPLICA_DENTITY_FULL status to force the backend to write the WAL record columns we need to calculate a delta tuple. Auto DDL replication transfers security labels (and replica identity changes) across all nodes involved. On the subscriber side, the SpockRelation cache contains a specific Oid list for each column, or an InvalidOid if the column doesn't need the delta_apply feature. This commit doesn't contain any tests, nor does it remove the previous 'core' approach; that should be done in the following commits. Words on upgrade: there is no easy way to upgrade from a patched version to the extensible one. So, at a glance, it seems we could just recommend that people remove the delta apply settings before the upgrade and add them after the upgrade using the new interface. NOTE: There is no good reason to deliver the whole tuple by the LR protocol to the subscriber. We can send only IDENTITY columns and columns needed for the delta_apply. It may be done by employing SpockRelayion cache inside the walsender. TODO: To stay safe, we should limit 'delta apply' types with increment, decrement and initialisation operators only. Also, we should decline nullable columns. --- include/spock_relcache.h | 3 ++ sql/spock--6.0.0-devel.sql | 76 +++++++++++++++++++++++----- src/spock_apply_heap.c | 17 ++++++- src/spock_relcache.c | 65 +++++++++++++++++++++--- tests/regress/expected/autoddl.out | 15 +++--- tests/regress/expected/basic.out | 14 +++-- tests/regress/expected/infofuncs.out | 6 +-- tests/regress/sql/basic.sql | 5 ++ 8 files changed, 166 insertions(+), 35 deletions(-) diff --git a/include/spock_relcache.h b/include/spock_relcache.h index f99c0d82..8d3adb99 100644 --- a/include/spock_relcache.h +++ b/include/spock_relcache.h @@ -49,6 +49,9 @@ typedef struct SpockRelation /* Additional cache, only valid as long as relation mapping is. */ bool hasTriggers; + + Oid *delta_functions; + bool has_delta_apply; } SpockRelation; extern void spock_relation_cache_update(uint32 remoteid, diff --git a/sql/spock--6.0.0-devel.sql b/sql/spock--6.0.0-devel.sql index ae42639c..2fecdb29 100644 --- a/sql/spock--6.0.0-devel.sql +++ b/sql/spock--6.0.0-devel.sql @@ -561,19 +561,33 @@ CREATE FUNCTION spock.terminate_active_transactions() RETURNS bool -- Generic delta apply functions for all numeric data types -- ---- CREATE FUNCTION spock.delta_apply(int2, int2, int2) -RETURNS int2 LANGUAGE c AS 'MODULE_PATHNAME', 'delta_apply_int2'; +RETURNS int2 +AS 'MODULE_PATHNAME', 'delta_apply_int2' +LANGUAGE C; CREATE FUNCTION spock.delta_apply(int4, int4, int4) -RETURNS int4 LANGUAGE c AS 'MODULE_PATHNAME', 'delta_apply_int4'; +RETURNS int4 +AS 'MODULE_PATHNAME', 'delta_apply_int4' +LANGUAGE C; CREATE FUNCTION spock.delta_apply(int8, int8, int8) -RETURNS int8 LANGUAGE c AS 'MODULE_PATHNAME', 'delta_apply_int8'; +RETURNS int8 +AS 'MODULE_PATHNAME', 'delta_apply_int8' +LANGUAGE C; CREATE FUNCTION spock.delta_apply(float4, float4, float4) -RETURNS float4 LANGUAGE c AS 'MODULE_PATHNAME', 'delta_apply_float4'; +RETURNS float4 +AS 'MODULE_PATHNAME', 'delta_apply_float4' +LANGUAGE C; CREATE FUNCTION spock.delta_apply(float8, float8, float8) -RETURNS float8 LANGUAGE c AS 'MODULE_PATHNAME', 'delta_apply_float8'; +RETURNS float8 +AS 'MODULE_PATHNAME', 'delta_apply_float8' +LANGUAGE C; CREATE FUNCTION spock.delta_apply(numeric, numeric, numeric) -RETURNS numeric LANGUAGE c AS 'MODULE_PATHNAME', 'delta_apply_numeric'; +RETURNS numeric +AS 'MODULE_PATHNAME', 'delta_apply_numeric' +LANGUAGE C; CREATE FUNCTION spock.delta_apply(money, money, money) -RETURNS money LANGUAGE c AS 'MODULE_PATHNAME', 'delta_apply_money'; +RETURNS money +AS 'MODULE_PATHNAME', 'delta_apply_money' +LANGUAGE C; -- ---- -- Function to control REPAIR mode @@ -644,13 +658,31 @@ DECLARE label text; atttype name; attdata record; - ctypname name; sqlstring text; status boolean; + relreplident char (1); + ctypname name; BEGIN - -- - -- Find proper delta_apply function for the column type or ERROR - -- + + /* + * regclass input type guarantees we see this table, no 'not found' check + * is needed. + */ + SELECT c.relreplident FROM pg_class c WHERE oid = rel INTO relreplident; + /* + * Allow only DEFAULT type of replica identity. FULL type means we have + * already requested delta_apply feature on this table. + * Avoid INDEX type because indexes may have different names on the nodes and + * it would be better to stay paranoid than afraid of consequences. + */ + IF (relreplident <> 'd' AND relreplident <> 'f') + THEN + RAISE EXCEPTION 'spock can apply delta_apply feature to the DEFAULT replica identity type only. This table holds "%" idenity', relreplident; + END IF; + + /* + * Find proper delta_apply function for the column type or ERROR + */ SELECT t.typname,t.typinput,t.typoutput FROM pg_catalog.pg_attribute a, pg_type t @@ -676,8 +708,8 @@ BEGIN sqlstring := format('SECURITY LABEL FOR spock ON COLUMN %I.%I IS NULL;' , rel, att_name); ELSE - sqlstring := format('SECURITY LABEL FOR spock ON COLUMN %I.%I IS %L;' , - rel, att_name, 'delta_apply'); + sqlstring := format('SECURITY LABEL FOR spock ON COLUMN %I.%I IS %L;' , + rel, att_name, 'spock.delta_apply'); END IF; EXECUTE sqlstring; @@ -691,6 +723,24 @@ BEGIN raise WARNING 'delta_apply setting has not been propagated to other spock nodes'; END IF; + IF EXISTS (SELECT 1 FROM pg_catalog.pg_seclabel + WHERE objoid = rel AND classoid = 'pg_class'::regclass AND + provider = 'spock') THEN + /* + * Call it each time to trigger relcache invalidation callback that causes + * refresh of the SpockRelation entry and guarantees actual state of the + * delta_apply columns. + */ + EXECUTE format('ALTER TABLE %I REPLICA IDENTITY FULL', rel); + ELSIF EXISTS (SELECT 1 FROM pg_catalog.pg_class c + WHERE c.oid = rel AND c.relreplident = 'f') THEN + /* + * Have removed he last security label. Revert this spock hack change, + * if needed. + */ + EXECUTE format('ALTER TABLE %I REPLICA IDENTITY DEFAULT', rel); + END IF; + RETURN true; END; $$ LANGUAGE plpgsql STRICT VOLATILE; diff --git a/src/spock_apply_heap.c b/src/spock_apply_heap.c index 14a4e049..49d9d6a8 100644 --- a/src/spock_apply_heap.c +++ b/src/spock_apply_heap.c @@ -606,7 +606,8 @@ build_delta_tuple(SpockRelation *rel, SpockTupleData *oldtup, int remoteattnum = rel->attmap[attidx]; Assert(remoteattnum < tupdesc->natts); - if (rel->delta_apply_functions[remoteattnum] == InvalidOid) + if (rel->delta_apply_functions[remoteattnum] == InvalidOid && + rel->delta_functions[remoteattnum] == InvalidOid) { deltatup->values[remoteattnum] = 0xdeadbeef; deltatup->nulls[remoteattnum] = true; @@ -642,6 +643,18 @@ build_delta_tuple(SpockRelation *rel, SpockTupleData *oldtup, deltatup->nulls[remoteattnum] = false; deltatup->changed[remoteattnum] = true; } + else if (rel->delta_functions[remoteattnum] != InvalidOid) + { + loc_value = heap_getattr(TTS_TUP(localslot), remoteattnum + 1, tupdesc, + &loc_isnull); + + result = OidFunctionCall3Coll(rel->delta_functions[remoteattnum], + InvalidOid, oldtup->values[remoteattnum], + newtup->values[remoteattnum], loc_value); + deltatup->values[remoteattnum] = result; + deltatup->nulls[remoteattnum] = false; + deltatup->changed[remoteattnum] = true; + } else { loc_value = heap_getattr(TTS_TUP(localslot), remoteattnum + 1, tupdesc, @@ -774,7 +787,7 @@ spock_handle_conflict_and_apply(SpockRelation *rel, EState *estate, xmin, local_origin_found, local_origin, local_ts, idxused); - if (rel->has_delta_columns) + if (rel->has_delta_columns || rel->has_delta_apply) { SpockTupleData deltatup; HeapTuple currenttuple; diff --git a/src/spock_relcache.c b/src/spock_relcache.c index 7b322f58..1dfb3265 100644 --- a/src/spock_relcache.c +++ b/src/spock_relcache.c @@ -16,7 +16,7 @@ #include "catalog/namespace.h" #include "catalog/pg_trigger.h" - +#include "commands/seclabel.h" #include "utils/attoptcache.h" #include "utils/builtins.h" #include "utils/catcache.h" @@ -57,11 +57,14 @@ relcache_free_entry(SpockRelation *entry) pfree(entry->attmap); if (entry->delta_apply_functions) pfree(entry->delta_apply_functions); + if (entry->delta_functions) + pfree(entry->delta_functions); entry->natts = 0; entry->reloid = InvalidOid; entry->rel = NULL; entry->has_delta_columns = false; + entry->has_delta_apply = false; } @@ -99,7 +102,10 @@ spock_relation_open(uint32 remoteid, LOCKMODE lockmode) desc = RelationGetDescr(entry->rel); for (i = 0; i < entry->natts; i++) { - AttributeOpts *aopt; + AttributeOpts *aopt; + Form_pg_attribute att; + ObjectAddress object; + char *seclabel; entry->attmap[i] = tupdesc_get_att_by_name(desc, entry->attnames[i]); @@ -107,15 +113,15 @@ spock_relation_open(uint32 remoteid, LOCKMODE lockmode) * If we find attribute options for this column and the * delta_apply_function is set, lookup the oid for it. */ + att = TupleDescAttr(desc, entry->attmap[i]); aopt = get_attribute_options(entry->rel->rd_id, entry->attmap[i] + 1); if (aopt != NULL && aopt->delta_apply_function != 0) { - char *fname; - Form_pg_attribute att; - Oid dfunc; + char *fname; + Form_pg_attribute att; + Oid dfunc; - att = TupleDescAttr(desc, entry->attmap[i]); fname = pstrdup(GET_STRING_RELOPTION(aopt, delta_apply_function)); dfunc = spock_lookup_delta_function(fname, att->atttypid); @@ -132,12 +138,53 @@ spock_relation_open(uint32 remoteid, LOCKMODE lockmode) entry->has_delta_columns = true; entry->delta_apply_functions[entry->attmap[i]] = dfunc; } + + if (entry->rel->rd_rel->relreplident != REPLICA_IDENTITY_FULL) + continue; + + /* + * Read security labels for each attname. For each such an attribute + * choose corresponding delta function. + * + * XXX: What about non-existing columns on remote side? + */ + object.classId = RelationRelationId; + object.objectId = RelationGetRelid(entry->rel); + object.objectSubId = entry->attmap[i] + 1; + seclabel = GetSecurityLabel(&object, spock_SECLABEL_PROVIDER); + if (seclabel != NULL) + { + entry->has_delta_apply = true; + entry->delta_functions[entry->attmap[i]] = + spock_lookup_delta_function(seclabel, att->atttypid); + Assert(entry->delta_functions[entry->attmap[i]] != InvalidOid); + } + else + { + /* Main case */ + entry->delta_functions[entry->attmap[i]] = InvalidOid; + } } relinfo = makeNode(ResultRelInfo); InitResultRelInfo(relinfo, entry->rel, 1, NULL, 0); entry->reloid = RelationGetRelid(entry->rel); - entry->idxoid = RelationGetReplicaIndex(relinfo->ri_RelationDesc); + if (entry->has_delta_apply) + { + /* + * It looks like a hack — which, in fact, it is. + * We assume that delta_apply may be used for the DEFAULT identity + * only and will be immediately removed after altering the table. + * Also, if an ERROR happens here we will stay with an inconsistent + * value of the relreplident field. But it is just a cache ... + */ + relinfo->ri_RelationDesc->rd_rel->relreplident = REPLICA_IDENTITY_DEFAULT; + entry->idxoid = RelationGetReplicaIndex(relinfo->ri_RelationDesc); + Assert(entry->idxoid != InvalidOid); + relinfo->ri_RelationDesc->rd_rel->relreplident = REPLICA_IDENTITY_FULL; + } + else + entry->idxoid = RelationGetReplicaIndex(relinfo->ri_RelationDesc); /* Cache trigger info. */ entry->hasTriggers = false; @@ -209,6 +256,8 @@ spock_relation_cache_update(uint32 remoteid, char *schemaname, entry->attmap = palloc(natts * sizeof(int)); entry->has_delta_columns = false; entry->delta_apply_functions = palloc0(natts * sizeof(Oid)); + entry->has_delta_apply = false; + entry->delta_functions = (Oid *) palloc0(entry->natts * sizeof(Oid)); MemoryContextSwitchTo(oldcontext); /* XXX Should we validate the relation against local schema here? */ @@ -247,6 +296,8 @@ spock_relation_cache_updater(SpockRemoteRel *remoterel) entry->attmap = palloc(remoterel->natts * sizeof(int)); entry->has_delta_columns = false; entry->delta_apply_functions = palloc0(remoterel->natts * sizeof(Oid)); + entry->has_delta_apply = false; + entry->delta_functions = (Oid *) palloc0(entry->natts * sizeof(Oid)); MemoryContextSwitchTo(oldcontext); /* XXX Should we validate the relation against local schema here? */ diff --git a/tests/regress/expected/autoddl.out b/tests/regress/expected/autoddl.out index afd630ed..ccd97590 100644 --- a/tests/regress/expected/autoddl.out +++ b/tests/regress/expected/autoddl.out @@ -106,6 +106,7 @@ INFO: DDL statement replicated. CREATE TABLE slabel1 (x integer, y text PRIMARY KEY); INFO: DDL statement replicated. SELECT spock.delta_apply('slabel1', 'x'); +INFO: DDL statement replicated. INFO: DDL statement replicated. delta_apply ------------- @@ -117,6 +118,7 @@ ERROR: type "text" can not be used in delta_apply conflict resolution SELECT spock.delta_apply('slabel1', 'z'); -- ERROR ERROR: column z does not exist in the table slabel1 SELECT spock.delta_apply('slabel1', 'x'); -- repeating call do nothing +INFO: DDL statement replicated. INFO: DDL statement replicated. delta_apply ------------- @@ -124,21 +126,22 @@ INFO: DDL statement replicated. (1 row) SELECT objname, label FROM pg_seclabels; - objname | label ------------+------------- - slabel1.x | delta_apply + objname | label +-----------+------------------- + slabel1.x | spock.delta_apply (1 row) -- Short round trip to check that subscriber has the security label \c :subscriber_dsn SELECT objname, label FROM pg_seclabels; - objname | label ------------+------------- - slabel1.x | delta_apply + objname | label +-----------+------------------- + slabel1.x | spock.delta_apply (1 row) \c :provider_dsn SELECT spock.delta_apply('slabel1', 'x', true); +INFO: DDL statement replicated. INFO: DDL statement replicated. delta_apply ------------- diff --git a/tests/regress/expected/basic.out b/tests/regress/expected/basic.out index b65c726d..32b095d3 100644 --- a/tests/regress/expected/basic.out +++ b/tests/regress/expected/basic.out @@ -218,6 +218,9 @@ DROP FUNCTION call_fn; -- -- A label creation checks CREATE TABLE slabel (x integer, y text PRIMARY KEY); +CREATE TABLE slabel_ri (x integer NOT NULL, y text); +CREATE UNIQUE INDEX slabel_ri_idx ON slabel_ri(x); +ALTER TABLE slabel_ri REPLICA IDENTITY USING INDEX slabel_ri_idx; SELECT spock.delta_apply('slabel', 'x'); WARNING: delta_apply setting has not been propagated to other spock nodes delta_apply @@ -236,12 +239,15 @@ WARNING: delta_apply setting has not been propagated to other spock nodes t (1 row) +SELECT spock.delta_apply('slabel_ri', 'x'); -- ERROR +ERROR: spock can apply delta_apply feature to the DEFAULT replica identity type only. This table holds "i" idenity SELECT objname, label FROM pg_seclabels; - objname | label -----------+------------- - slabel.x | delta_apply + objname | label +----------+------------------- + slabel.x | spock.delta_apply (1 row) +DROP TABLE slabel_ri CASCADE; -- Short round trip to check that subscriber has no security labels \c :subscriber_dsn SELECT objname, label FROM pg_seclabels; @@ -276,7 +282,7 @@ WARNING: delta_apply setting has not been propagated to other spock nodes (1 row) ALTER TABLE slabel ALTER COLUMN x TYPE text; -- just warn -WARNING: the alter column statement removes spock security label 'delta_apply' on this column +WARNING: the alter column statement removes spock security label 'spock.delta_apply' on this column SELECT objname, label FROM pg_seclabels; objname | label ---------+------- diff --git a/tests/regress/expected/infofuncs.out b/tests/regress/expected/infofuncs.out index 1a7bf836..5e33bef4 100644 --- a/tests/regress/expected/infofuncs.out +++ b/tests/regress/expected/infofuncs.out @@ -29,9 +29,9 @@ SELECT spock.delta_apply('slabel', 'x', false); (1 row) SELECT objname, label FROM pg_seclabels; - objname | label -----------+------------- - slabel.x | delta_apply + objname | label +----------+------------------- + slabel.x | spock.delta_apply (1 row) DROP EXTENSION spock; diff --git a/tests/regress/sql/basic.sql b/tests/regress/sql/basic.sql index cdf16fe4..ef6ea088 100644 --- a/tests/regress/sql/basic.sql +++ b/tests/regress/sql/basic.sql @@ -119,12 +119,17 @@ DROP FUNCTION call_fn; -- A label creation checks CREATE TABLE slabel (x integer, y text PRIMARY KEY); +CREATE TABLE slabel_ri (x integer NOT NULL, y text); +CREATE UNIQUE INDEX slabel_ri_idx ON slabel_ri(x); +ALTER TABLE slabel_ri REPLICA IDENTITY USING INDEX slabel_ri_idx; SELECT spock.delta_apply('slabel', 'x'); SELECT spock.delta_apply('slabel', 'y'); -- ERROR SELECT spock.delta_apply('slabel', 'z'); -- ERROR SELECT spock.delta_apply('slabel', 'x'); -- repeating call do nothing +SELECT spock.delta_apply('slabel_ri', 'x'); -- ERROR SELECT objname, label FROM pg_seclabels; +DROP TABLE slabel_ri CASCADE; -- Short round trip to check that subscriber has no security labels \c :subscriber_dsn From 32ad9e902c843e754a75b2bd29e22f8a5da2a2c3 Mon Sep 17 00:00:00 2001 From: "Andrei V. Lepikhov" Date: Wed, 26 Nov 2025 23:08:10 +0100 Subject: [PATCH 4/8] Remove the core attribute options. With this patch, we merely remove the core patch and the delta apply feature based on it. Little code readability has been made, just being in place. --- Makefile | 3 - include/spock_relcache.h | 2 - patches/15/pg15-015-attoptions.diff | 194 --------------------------- patches/16/pg16-015-attoptions.diff | 194 --------------------------- patches/17/pg17-015-attoptions.diff | 194 --------------------------- patches/18/pg18-015-attoptions.diff | 199 ---------------------------- src/spock_apply_heap.c | 27 +--- src/spock_relcache.c | 64 +++------ 8 files changed, 24 insertions(+), 853 deletions(-) delete mode 100644 patches/15/pg15-015-attoptions.diff delete mode 100644 patches/16/pg16-015-attoptions.diff delete mode 100644 patches/17/pg17-015-attoptions.diff delete mode 100644 patches/18/pg18-015-attoptions.diff diff --git a/Makefile b/Makefile index 818ecf35..a55761ce 100644 --- a/Makefile +++ b/Makefile @@ -23,9 +23,6 @@ PG_CPPFLAGS += -I$(libpq_srcdir) \ -I$(realpath src/compat/$(PGVER)) \ -Werror=implicit-function-declaration SHLIB_LINK += $(libpq) $(filter -lintl, $(LIBS)) -ifdef NO_LOG_OLD_VALUE -PG_CPPFLAGS += -DNO_LOG_OLD_VALUE -endif REGRESS := __placeholder__ EXTRA_CLEAN += $(control_path) spock_compat.bc diff --git a/include/spock_relcache.h b/include/spock_relcache.h index 8d3adb99..a71dcdc8 100644 --- a/include/spock_relcache.h +++ b/include/spock_relcache.h @@ -44,8 +44,6 @@ typedef struct SpockRelation Oid idxoid; Relation rel; int *attmap; - bool has_delta_columns; - Oid *delta_apply_functions; /* Additional cache, only valid as long as relation mapping is. */ bool hasTriggers; diff --git a/patches/15/pg15-015-attoptions.diff b/patches/15/pg15-015-attoptions.diff deleted file mode 100644 index 7b63311a..00000000 --- a/patches/15/pg15-015-attoptions.diff +++ /dev/null @@ -1,194 +0,0 @@ -diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c -index 5b696043c5..16ec985928 100644 ---- a/src/backend/access/common/reloptions.c -+++ b/src/backend/access/common/reloptions.c -@@ -168,6 +168,15 @@ static relopt_bool boolRelOpts[] = - }, - true - }, -+ { -+ { -+ "log_old_value", -+ "Add old value of attribute to WAL for logical decoding", -+ RELOPT_KIND_ATTRIBUTE, -+ ShareUpdateExclusiveLock -+ }, -+ false -+ }, - /* list terminator */ - {{NULL}} - }; -@@ -548,6 +557,19 @@ static relopt_enum enumRelOpts[] = - - static relopt_string stringRelOpts[] = - { -+ { -+ { -+ "delta_apply_function", -+ "Function called to perform delta conflict avoidance", -+ RELOPT_KIND_ATTRIBUTE, -+ ShareUpdateExclusiveLock -+ }, -+ -1, -+ true, -+ NULL, -+ NULL, -+ NULL -+ }, - /* list terminator */ - {{NULL}} - }; -@@ -2076,7 +2098,9 @@ attribute_reloptions(Datum reloptions, bool validate) - { - static const relopt_parse_elt tab[] = { - {"n_distinct", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct)}, -- {"n_distinct_inherited", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct_inherited)} -+ {"n_distinct_inherited", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct_inherited)}, -+ {"log_old_value", RELOPT_TYPE_BOOL, offsetof(AttributeOpts, log_old_value)}, -+ {"delta_apply_function", RELOPT_TYPE_STRING, offsetof(AttributeOpts, delta_apply_function)} - }; - - return (bytea *) build_reloptions(reloptions, validate, -diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c -index 88ab5f99c8..161333c5e0 100644 ---- a/src/backend/access/heap/heapam.c -+++ b/src/backend/access/heap/heapam.c -@@ -66,6 +66,7 @@ - #include "storage/smgr.h" - #include "storage/spin.h" - #include "storage/standby.h" -+#include "utils/attoptcache.h" - #include "utils/datum.h" - #include "utils/inval.h" - #include "utils/lsyscache.h" -@@ -86,6 +87,7 @@ static void check_lock_if_inplace_updateable_rel(Relation relation, - HeapTuple newtup); - static void check_inplace_rel_lock(HeapTuple oldtup); - #endif -+static Bitmapset *HeapDetermineLogOldColumns(Relation relation); - static Bitmapset *HeapDetermineColumnsInfo(Relation relation, - Bitmapset *interesting_cols, - Bitmapset *external_cols, -@@ -117,6 +119,7 @@ static void index_delete_sort(TM_IndexDeleteOp *delstate); - static int bottomup_sort_and_shrink(TM_IndexDeleteOp *delstate); - static XLogRecPtr log_heap_new_cid(Relation relation, HeapTuple tup); - static HeapTuple ExtractReplicaIdentity(Relation rel, HeapTuple tup, bool key_required, -+ Bitmapset *logged_old_attrs, - bool *copy); - - -@@ -2919,7 +2922,7 @@ l1: - * Compute replica identity tuple before entering the critical section so - * we don't PANIC upon a memory allocation failure. - */ -- old_key_tuple = ExtractReplicaIdentity(relation, &tp, true, &old_key_copied); -+ old_key_tuple = ExtractReplicaIdentity(relation, &tp, true, NULL, &old_key_copied); - - /* - * If this is the first possibly-multixact-able operation in the current -@@ -3150,6 +3153,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, - Bitmapset *id_attrs; - Bitmapset *interesting_attrs; - Bitmapset *modified_attrs; -+ Bitmapset *logged_old_attrs; - ItemId lp; - HeapTupleData oldtup; - HeapTuple heaptup; -@@ -3267,6 +3271,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, - modified_attrs = HeapDetermineColumnsInfo(relation, interesting_attrs, - id_attrs, &oldtup, - newtup, &id_has_external); -+ logged_old_attrs = HeapDetermineLogOldColumns(relation); - - /* - * If we're not updating any "key" column, we can grab a weaker lock type. -@@ -3538,6 +3543,7 @@ l2: - bms_free(key_attrs); - bms_free(id_attrs); - bms_free(modified_attrs); -+ bms_free(logged_old_attrs); - bms_free(interesting_attrs); - return result; - } -@@ -3874,6 +3880,7 @@ l2: - old_key_tuple = ExtractReplicaIdentity(relation, &oldtup, - bms_overlap(modified_attrs, id_attrs) || - id_has_external, -+ logged_old_attrs, - &old_key_copied); - - /* NO EREPORT(ERROR) from here till changes are logged */ -@@ -4023,6 +4030,7 @@ l2: - bms_free(key_attrs); - bms_free(id_attrs); - bms_free(modified_attrs); -+ bms_free(logged_old_attrs); - bms_free(interesting_attrs); - - return TM_Ok; -@@ -4195,6 +4203,26 @@ heap_attr_equals(TupleDesc tupdesc, int attrnum, Datum value1, Datum value2, - } - } - -+static Bitmapset * -+HeapDetermineLogOldColumns(Relation relation) -+{ -+ int attnum; -+ Bitmapset *logged_cols = NULL; -+ TupleDesc tupdesc = RelationGetDescr(relation); -+ AttributeOpts *aopt; -+ -+ for (attnum = 1; attnum <= tupdesc->natts; attnum++) -+ { -+ aopt = get_attribute_options(relation->rd_id, attnum); -+ if (aopt != NULL && aopt->log_old_value) -+ logged_cols = bms_add_member(logged_cols, -+ attnum - -+ FirstLowInvalidHeapAttributeNumber); -+ } -+ -+ return logged_cols; -+} -+ - /* - * Check which columns are being updated. - * -@@ -8913,6 +8941,7 @@ log_heap_new_cid(Relation relation, HeapTuple tup) - */ - static HeapTuple - ExtractReplicaIdentity(Relation relation, HeapTuple tp, bool key_required, -+ Bitmapset *logged_old_attrs, - bool *copy) - { - TupleDesc desc = RelationGetDescr(relation); -@@ -8945,13 +8974,16 @@ ExtractReplicaIdentity(Relation relation, HeapTuple tp, bool key_required, - } - - /* if the key isn't required and we're only logging the key, we're done */ -- if (!key_required) -+ if (!key_required && logged_old_attrs == NULL) - return NULL; - - /* find out the replica identity columns */ - idattrs = RelationGetIndexAttrBitmap(relation, - INDEX_ATTR_BITMAP_IDENTITY_KEY); - -+ /* merge the columns that are marked LOG_OLD_VALUE */ -+ idattrs = bms_union(idattrs, logged_old_attrs); -+ - /* - * If there's no defined replica identity columns, treat as !key_required. - * (This case should not be reachable from heap_update, since that should -diff --git a/src/include/utils/attoptcache.h b/src/include/utils/attoptcache.h -index ee37af9500..98b48a8fd8 100644 ---- a/src/include/utils/attoptcache.h -+++ b/src/include/utils/attoptcache.h -@@ -21,6 +21,8 @@ typedef struct AttributeOpts - int32 vl_len_; /* varlena header (do not touch directly!) */ - float8 n_distinct; - float8 n_distinct_inherited; -+ bool log_old_value; -+ Oid delta_apply_function; - } AttributeOpts; - - extern AttributeOpts *get_attribute_options(Oid spcid, int attnum); diff --git a/patches/16/pg16-015-attoptions.diff b/patches/16/pg16-015-attoptions.diff deleted file mode 100644 index ffea2382..00000000 --- a/patches/16/pg16-015-attoptions.diff +++ /dev/null @@ -1,194 +0,0 @@ -diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c -index 469de9bb49..0119e5aa7c 100644 ---- a/src/backend/access/common/reloptions.c -+++ b/src/backend/access/common/reloptions.c -@@ -168,6 +168,15 @@ static relopt_bool boolRelOpts[] = - }, - true - }, -+ { -+ { -+ "log_old_value", -+ "Add old value of attribute to WAL for logical decoding", -+ RELOPT_KIND_ATTRIBUTE, -+ ShareUpdateExclusiveLock -+ }, -+ false -+ }, - /* list terminator */ - {{NULL}} - }; -@@ -548,6 +557,19 @@ static relopt_enum enumRelOpts[] = - - static relopt_string stringRelOpts[] = - { -+ { -+ { -+ "delta_apply_function", -+ "Function called to perform delta conflict avoidance", -+ RELOPT_KIND_ATTRIBUTE, -+ ShareUpdateExclusiveLock -+ }, -+ -1, -+ true, -+ NULL, -+ NULL, -+ NULL -+ }, - /* list terminator */ - {{NULL}} - }; -@@ -2072,7 +2094,9 @@ attribute_reloptions(Datum reloptions, bool validate) - { - static const relopt_parse_elt tab[] = { - {"n_distinct", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct)}, -- {"n_distinct_inherited", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct_inherited)} -+ {"n_distinct_inherited", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct_inherited)}, -+ {"log_old_value", RELOPT_TYPE_BOOL, offsetof(AttributeOpts, log_old_value)}, -+ {"delta_apply_function", RELOPT_TYPE_STRING, offsetof(AttributeOpts, delta_apply_function)} - }; - - return (bytea *) build_reloptions(reloptions, validate, -diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c -index 723e34e464..b2f845211d 100644 ---- a/src/backend/access/heap/heapam.c -+++ b/src/backend/access/heap/heapam.c -@@ -67,6 +67,7 @@ - #include "storage/smgr.h" - #include "storage/spin.h" - #include "storage/standby.h" -+#include "utils/attoptcache.h" - #include "utils/datum.h" - #include "utils/inval.h" - #include "utils/lsyscache.h" -@@ -87,6 +88,7 @@ static void check_lock_if_inplace_updateable_rel(Relation relation, - HeapTuple newtup); - static void check_inplace_rel_lock(HeapTuple oldtup); - #endif -+static Bitmapset *HeapDetermineLogOldColumns(Relation relation); - static Bitmapset *HeapDetermineColumnsInfo(Relation relation, - Bitmapset *interesting_cols, - Bitmapset *external_cols, -@@ -121,6 +123,7 @@ static void index_delete_sort(TM_IndexDeleteOp *delstate); - static int bottomup_sort_and_shrink(TM_IndexDeleteOp *delstate); - static XLogRecPtr log_heap_new_cid(Relation relation, HeapTuple tup); - static HeapTuple ExtractReplicaIdentity(Relation relation, HeapTuple tp, bool key_required, -+ Bitmapset *logged_old_attrs, - bool *copy); - - -@@ -2780,7 +2783,7 @@ l1: - * Compute replica identity tuple before entering the critical section so - * we don't PANIC upon a memory allocation failure. - */ -- old_key_tuple = ExtractReplicaIdentity(relation, &tp, true, &old_key_copied); -+ old_key_tuple = ExtractReplicaIdentity(relation, &tp, true, NULL, &old_key_copied); - - /* - * If this is the first possibly-multixact-able operation in the current -@@ -3013,6 +3016,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, - Bitmapset *id_attrs; - Bitmapset *interesting_attrs; - Bitmapset *modified_attrs; -+ Bitmapset *logged_old_attrs; - ItemId lp; - HeapTupleData oldtup; - HeapTuple heaptup; -@@ -3135,6 +3139,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, - modified_attrs = HeapDetermineColumnsInfo(relation, interesting_attrs, - id_attrs, &oldtup, - newtup, &id_has_external); -+ logged_old_attrs = HeapDetermineLogOldColumns(relation); - - /* - * If we're not updating any "key" column, we can grab a weaker lock type. -@@ -3409,6 +3414,7 @@ l2: - bms_free(key_attrs); - bms_free(id_attrs); - bms_free(modified_attrs); -+ bms_free(logged_old_attrs); - bms_free(interesting_attrs); - return result; - } -@@ -3758,6 +3764,7 @@ l2: - old_key_tuple = ExtractReplicaIdentity(relation, &oldtup, - bms_overlap(modified_attrs, id_attrs) || - id_has_external, -+ logged_old_attrs, - &old_key_copied); - - /* NO EREPORT(ERROR) from here till changes are logged */ -@@ -3924,6 +3931,7 @@ l2: - bms_free(key_attrs); - bms_free(id_attrs); - bms_free(modified_attrs); -+ bms_free(logged_old_attrs); - bms_free(interesting_attrs); - - return TM_Ok; -@@ -4096,6 +4104,26 @@ heap_attr_equals(TupleDesc tupdesc, int attrnum, Datum value1, Datum value2, - } - } - -+static Bitmapset * -+HeapDetermineLogOldColumns(Relation relation) -+{ -+ int attnum; -+ Bitmapset *logged_cols = NULL; -+ TupleDesc tupdesc = RelationGetDescr(relation); -+ AttributeOpts *aopt; -+ -+ for (attnum = 1; attnum <= tupdesc->natts; attnum++) -+ { -+ aopt = get_attribute_options(relation->rd_id, attnum); -+ if (aopt != NULL && aopt->log_old_value) -+ logged_cols = bms_add_member(logged_cols, -+ attnum - -+ FirstLowInvalidHeapAttributeNumber); -+ } -+ -+ return logged_cols; -+} -+ - /* - * Check which columns are being updated. - * -@@ -9051,6 +9079,7 @@ log_heap_new_cid(Relation relation, HeapTuple tup) - */ - static HeapTuple - ExtractReplicaIdentity(Relation relation, HeapTuple tp, bool key_required, -+ Bitmapset *logged_old_attrs, - bool *copy) - { - TupleDesc desc = RelationGetDescr(relation); -@@ -9083,13 +9112,16 @@ ExtractReplicaIdentity(Relation relation, HeapTuple tp, bool key_required, - } - - /* if the key isn't required and we're only logging the key, we're done */ -- if (!key_required) -+ if (!key_required && logged_old_attrs == NULL) - return NULL; - - /* find out the replica identity columns */ - idattrs = RelationGetIndexAttrBitmap(relation, - INDEX_ATTR_BITMAP_IDENTITY_KEY); - -+ /* merge the columns that are marked LOG_OLD_VALUE */ -+ idattrs = bms_union(idattrs, logged_old_attrs); -+ - /* - * If there's no defined replica identity columns, treat as !key_required. - * (This case should not be reachable from heap_update, since that should -diff --git a/src/include/utils/attoptcache.h b/src/include/utils/attoptcache.h -index e4119b6aa2..6354a98157 100644 ---- a/src/include/utils/attoptcache.h -+++ b/src/include/utils/attoptcache.h -@@ -21,6 +21,8 @@ typedef struct AttributeOpts - int32 vl_len_; /* varlena header (do not touch directly!) */ - float8 n_distinct; - float8 n_distinct_inherited; -+ bool log_old_value; -+ Oid delta_apply_function; - } AttributeOpts; - - extern AttributeOpts *get_attribute_options(Oid attrelid, int attnum); diff --git a/patches/17/pg17-015-attoptions.diff b/patches/17/pg17-015-attoptions.diff deleted file mode 100644 index df156695..00000000 --- a/patches/17/pg17-015-attoptions.diff +++ /dev/null @@ -1,194 +0,0 @@ -diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c -index d6eb5d85599..4ef2e55cc44 100644 ---- a/src/backend/access/common/reloptions.c -+++ b/src/backend/access/common/reloptions.c -@@ -166,6 +166,15 @@ static relopt_bool boolRelOpts[] = - }, - true - }, -+ { -+ { -+ "log_old_value", -+ "Add old value of attribute to WAL for logical decoding", -+ RELOPT_KIND_ATTRIBUTE, -+ ShareUpdateExclusiveLock -+ }, -+ false -+ }, - /* list terminator */ - {{NULL}} - }; -@@ -546,6 +555,19 @@ static relopt_enum enumRelOpts[] = - - static relopt_string stringRelOpts[] = - { -+ { -+ { -+ "delta_apply_function", -+ "Function called to perform delta conflict avoidance", -+ RELOPT_KIND_ATTRIBUTE, -+ ShareUpdateExclusiveLock -+ }, -+ -1, -+ true, -+ NULL, -+ NULL, -+ NULL -+ }, - /* list terminator */ - {{NULL}} - }; -@@ -2070,7 +2092,9 @@ attribute_reloptions(Datum reloptions, bool validate) - { - static const relopt_parse_elt tab[] = { - {"n_distinct", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct)}, -- {"n_distinct_inherited", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct_inherited)} -+ {"n_distinct_inherited", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct_inherited)}, -+ {"log_old_value", RELOPT_TYPE_BOOL, offsetof(AttributeOpts, log_old_value)}, -+ {"delta_apply_function", RELOPT_TYPE_STRING, offsetof(AttributeOpts, delta_apply_function)} - }; - - return (bytea *) build_reloptions(reloptions, validate, -diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c -index 95e3be524a7..708d55b2618 100644 ---- a/src/backend/access/heap/heapam.c -+++ b/src/backend/access/heap/heapam.c -@@ -64,6 +64,7 @@ - #include "storage/predicate.h" - #include "storage/procarray.h" - #include "storage/standby.h" -+#include "utils/attoptcache.h" - #include "utils/datum.h" - #include "utils/injection_point.h" - #include "utils/inval.h" -@@ -85,6 +86,7 @@ static void check_lock_if_inplace_updateable_rel(Relation relation, - HeapTuple newtup); - static void check_inplace_rel_lock(HeapTuple oldtup); - #endif -+static Bitmapset *HeapDetermineLogOldColumns(Relation relation); - static Bitmapset *HeapDetermineColumnsInfo(Relation relation, - Bitmapset *interesting_cols, - Bitmapset *external_cols, -@@ -121,6 +123,7 @@ static void index_delete_sort(TM_IndexDeleteOp *delstate); - static int bottomup_sort_and_shrink(TM_IndexDeleteOp *delstate); - static XLogRecPtr log_heap_new_cid(Relation relation, HeapTuple tup); - static HeapTuple ExtractReplicaIdentity(Relation relation, HeapTuple tp, bool key_required, -+ Bitmapset *logged_old_attrs, - bool *copy); - - -@@ -2938,7 +2941,7 @@ l1: - * Compute replica identity tuple before entering the critical section so - * we don't PANIC upon a memory allocation failure. - */ -- old_key_tuple = ExtractReplicaIdentity(relation, &tp, true, &old_key_copied); -+ old_key_tuple = ExtractReplicaIdentity(relation, &tp, true, NULL, &old_key_copied); - - /* - * If this is the first possibly-multixact-able operation in the current -@@ -3171,6 +3174,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, - Bitmapset *id_attrs; - Bitmapset *interesting_attrs; - Bitmapset *modified_attrs; -+ Bitmapset *logged_old_attrs; - ItemId lp; - HeapTupleData oldtup; - HeapTuple heaptup; -@@ -3338,6 +3342,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, - modified_attrs = HeapDetermineColumnsInfo(relation, interesting_attrs, - id_attrs, &oldtup, - newtup, &id_has_external); -+ logged_old_attrs = HeapDetermineLogOldColumns(relation); - - /* - * If we're not updating any "key" column, we can grab a weaker lock type. -@@ -3612,6 +3617,7 @@ l2: - bms_free(key_attrs); - bms_free(id_attrs); - bms_free(modified_attrs); -+ bms_free(logged_old_attrs); - bms_free(interesting_attrs); - return result; - } -@@ -3961,6 +3967,7 @@ l2: - old_key_tuple = ExtractReplicaIdentity(relation, &oldtup, - bms_overlap(modified_attrs, id_attrs) || - id_has_external, -+ logged_old_attrs, - &old_key_copied); - - /* NO EREPORT(ERROR) from here till changes are logged */ -@@ -4127,6 +4134,7 @@ l2: - bms_free(key_attrs); - bms_free(id_attrs); - bms_free(modified_attrs); -+ bms_free(logged_old_attrs); - bms_free(interesting_attrs); - - return TM_Ok; -@@ -4299,6 +4307,26 @@ heap_attr_equals(TupleDesc tupdesc, int attrnum, Datum value1, Datum value2, - } - } - -+static Bitmapset * -+HeapDetermineLogOldColumns(Relation relation) -+{ -+ int attnum; -+ Bitmapset *logged_cols = NULL; -+ TupleDesc tupdesc = RelationGetDescr(relation); -+ AttributeOpts *aopt; -+ -+ for (attnum = 1; attnum <= tupdesc->natts; attnum++) -+ { -+ aopt = get_attribute_options(relation->rd_id, attnum); -+ if (aopt != NULL && aopt->log_old_value) -+ logged_cols = bms_add_member(logged_cols, -+ attnum - -+ FirstLowInvalidHeapAttributeNumber); -+ } -+ -+ return logged_cols; -+} -+ - /* - * Check which columns are being updated. - * -@@ -9076,6 +9104,7 @@ log_heap_new_cid(Relation relation, HeapTuple tup) - */ - static HeapTuple - ExtractReplicaIdentity(Relation relation, HeapTuple tp, bool key_required, -+ Bitmapset *logged_old_attrs, - bool *copy) - { - TupleDesc desc = RelationGetDescr(relation); -@@ -9108,13 +9137,16 @@ ExtractReplicaIdentity(Relation relation, HeapTuple tp, bool key_required, - } - - /* if the key isn't required and we're only logging the key, we're done */ -- if (!key_required) -+ if (!key_required && logged_old_attrs == NULL) - return NULL; - - /* find out the replica identity columns */ - idattrs = RelationGetIndexAttrBitmap(relation, - INDEX_ATTR_BITMAP_IDENTITY_KEY); - -+ /* merge the columns that are marked LOG_OLD_VALUE */ -+ idattrs = bms_union(idattrs, logged_old_attrs); -+ - /* - * If there's no defined replica identity columns, treat as !key_required. - * (This case should not be reachable from heap_update, since that should -diff --git a/src/include/utils/attoptcache.h b/src/include/utils/attoptcache.h -index a1a9bfc0fb9..e9b6dfab474 100644 ---- a/src/include/utils/attoptcache.h -+++ b/src/include/utils/attoptcache.h -@@ -21,6 +21,8 @@ typedef struct AttributeOpts - int32 vl_len_; /* varlena header (do not touch directly!) */ - float8 n_distinct; - float8 n_distinct_inherited; -+ bool log_old_value; -+ Oid delta_apply_function; - } AttributeOpts; - - extern AttributeOpts *get_attribute_options(Oid attrelid, int attnum); diff --git a/patches/18/pg18-015-attoptions.diff b/patches/18/pg18-015-attoptions.diff deleted file mode 100644 index dd42114d..00000000 --- a/patches/18/pg18-015-attoptions.diff +++ /dev/null @@ -1,199 +0,0 @@ -diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c -index 50747c16396..6cdbbe1d0dd 100644 ---- a/src/backend/access/common/reloptions.c -+++ b/src/backend/access/common/reloptions.c -@@ -166,6 +166,15 @@ static relopt_bool boolRelOpts[] = - }, - true - }, -+ { -+ { -+ "log_old_value", -+ "Add old value of attribute to WAL for logical decoding", -+ RELOPT_KIND_ATTRIBUTE, -+ ShareUpdateExclusiveLock -+ }, -+ false -+ }, - /* list terminator */ - {{NULL}} - }; -@@ -557,6 +566,19 @@ static relopt_enum enumRelOpts[] = - - static relopt_string stringRelOpts[] = - { -+ { -+ { -+ "delta_apply_function", -+ "Function called to perform delta conflict avoidance", -+ RELOPT_KIND_ATTRIBUTE, -+ ShareUpdateExclusiveLock -+ }, -+ -1, -+ true, -+ NULL, -+ NULL, -+ NULL -+ }, - /* list terminator */ - {{NULL}} - }; -@@ -2106,7 +2128,9 @@ attribute_reloptions(Datum reloptions, bool validate) - { - static const relopt_parse_elt tab[] = { - {"n_distinct", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct)}, -- {"n_distinct_inherited", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct_inherited)} -+ {"n_distinct_inherited", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct_inherited)}, -+ {"log_old_value", RELOPT_TYPE_BOOL, offsetof(AttributeOpts, log_old_value)}, -+ {"delta_apply_function", RELOPT_TYPE_STRING, offsetof(AttributeOpts, delta_apply_function)} - }; - - return (bytea *) build_reloptions(reloptions, validate, -diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c -index 0dcd6ee817e..dcf26167cae 100644 ---- a/src/backend/access/heap/heapam.c -+++ b/src/backend/access/heap/heapam.c -@@ -48,6 +48,7 @@ - #include "storage/lmgr.h" - #include "storage/predicate.h" - #include "storage/procarray.h" -+#include "utils/attoptcache.h" - #include "utils/datum.h" - #include "utils/injection_point.h" - #include "utils/inval.h" -@@ -67,6 +68,7 @@ static void check_lock_if_inplace_updateable_rel(Relation relation, - HeapTuple newtup); - static void check_inplace_rel_lock(HeapTuple oldtup); - #endif -+static Bitmapset *HeapDetermineLogOldColumns(Relation relation); - static Bitmapset *HeapDetermineColumnsInfo(Relation relation, - Bitmapset *interesting_cols, - Bitmapset *external_cols, -@@ -104,6 +106,7 @@ static void index_delete_sort(TM_IndexDeleteOp *delstate); - static int bottomup_sort_and_shrink(TM_IndexDeleteOp *delstate); - static XLogRecPtr log_heap_new_cid(Relation relation, HeapTuple tup); - static HeapTuple ExtractReplicaIdentity(Relation relation, HeapTuple tp, bool key_required, -+ Bitmapset *logged_old_attrs, - bool *copy); - - -@@ -3016,7 +3019,7 @@ l1: - * Compute replica identity tuple before entering the critical section so - * we don't PANIC upon a memory allocation failure. - */ -- old_key_tuple = ExtractReplicaIdentity(relation, &tp, true, &old_key_copied); -+ old_key_tuple = ExtractReplicaIdentity(relation, &tp, true, NULL, &old_key_copied); - - /* - * If this is the first possibly-multixact-able operation in the current -@@ -3249,6 +3252,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, - Bitmapset *id_attrs; - Bitmapset *interesting_attrs; - Bitmapset *modified_attrs; -+ Bitmapset *logged_old_attrs; - ItemId lp; - HeapTupleData oldtup; - HeapTuple heaptup; -@@ -3419,6 +3423,12 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, - id_attrs, &oldtup, - newtup, &id_has_external); - -+ if (!IsCatalogRelationOid(relation->rd_id)) -+ logged_old_attrs = HeapDetermineLogOldColumns(relation); -+ else -+ /* No need to log old values for catalog tables */ -+ logged_old_attrs = NULL; -+ - /* - * If we're not updating any "key" column, we can grab a weaker lock type. - * This allows for more concurrency when we are running simultaneously -@@ -3692,6 +3702,7 @@ l2: - bms_free(key_attrs); - bms_free(id_attrs); - bms_free(modified_attrs); -+ bms_free(logged_old_attrs); - bms_free(interesting_attrs); - return result; - } -@@ -4041,6 +4052,7 @@ l2: - old_key_tuple = ExtractReplicaIdentity(relation, &oldtup, - bms_overlap(modified_attrs, id_attrs) || - id_has_external, -+ logged_old_attrs, - &old_key_copied); - - /* NO EREPORT(ERROR) from here till changes are logged */ -@@ -4207,6 +4219,7 @@ l2: - bms_free(key_attrs); - bms_free(id_attrs); - bms_free(modified_attrs); -+ bms_free(logged_old_attrs); - bms_free(interesting_attrs); - - return TM_Ok; -@@ -4379,6 +4392,26 @@ heap_attr_equals(TupleDesc tupdesc, int attrnum, Datum value1, Datum value2, - } - } - -+static Bitmapset * -+HeapDetermineLogOldColumns(Relation relation) -+{ -+ int attnum; -+ Bitmapset *logged_cols = NULL; -+ TupleDesc tupdesc = RelationGetDescr(relation); -+ AttributeOpts *aopt; -+ -+ for (attnum = 1; attnum <= tupdesc->natts; attnum++) -+ { -+ aopt = get_attribute_options(relation->rd_id, attnum); -+ if (aopt != NULL && aopt->log_old_value) -+ logged_cols = bms_add_member(logged_cols, -+ attnum - -+ FirstLowInvalidHeapAttributeNumber); -+ } -+ -+ return logged_cols; -+} -+ - /* - * Check which columns are being updated. - * -@@ -9132,6 +9165,7 @@ log_heap_new_cid(Relation relation, HeapTuple tup) - */ - static HeapTuple - ExtractReplicaIdentity(Relation relation, HeapTuple tp, bool key_required, -+ Bitmapset *logged_old_attrs, - bool *copy) - { - TupleDesc desc = RelationGetDescr(relation); -@@ -9164,13 +9198,16 @@ ExtractReplicaIdentity(Relation relation, HeapTuple tp, bool key_required, - } - - /* if the key isn't required and we're only logging the key, we're done */ -- if (!key_required) -+ if (!key_required && logged_old_attrs == NULL) - return NULL; - - /* find out the replica identity columns */ - idattrs = RelationGetIndexAttrBitmap(relation, - INDEX_ATTR_BITMAP_IDENTITY_KEY); - -+ /* merge the columns that are marked LOG_OLD_VALUE */ -+ idattrs = bms_union(idattrs, logged_old_attrs); -+ - /* - * If there's no defined replica identity columns, treat as !key_required. - * (This case should not be reachable from heap_update, since that should -diff --git a/src/include/utils/attoptcache.h b/src/include/utils/attoptcache.h -index f684a772af5..6c965fede13 100644 ---- a/src/include/utils/attoptcache.h -+++ b/src/include/utils/attoptcache.h -@@ -21,6 +21,8 @@ typedef struct AttributeOpts - int32 vl_len_; /* varlena header (do not touch directly!) */ - float8 n_distinct; - float8 n_distinct_inherited; -+ bool log_old_value; -+ Oid delta_apply_function; - } AttributeOpts; - - extern AttributeOpts *get_attribute_options(Oid attrelid, int attnum); diff --git a/src/spock_apply_heap.c b/src/spock_apply_heap.c index 49d9d6a8..39eb4044 100644 --- a/src/spock_apply_heap.c +++ b/src/spock_apply_heap.c @@ -111,11 +111,9 @@ typedef struct ApplyMIState static ApplyMIState *spkmistate = NULL; -#ifndef NO_LOG_OLD_VALUE static void build_delta_tuple(SpockRelation *rel, SpockTupleData *oldtup, SpockTupleData *newtup, SpockTupleData *deltatup, TupleTableSlot *localslot); -#endif static bool physatt_in_attmap(SpockRelation *rel, int attid); /* @@ -583,8 +581,6 @@ fill_missing_defaults(SpockRelation *rel, EState *estate, &tuple->nulls[defmap[i]]); } -#ifndef NO_LOG_OLD_VALUE - static void build_delta_tuple(SpockRelation *rel, SpockTupleData *oldtup, SpockTupleData *newtup, @@ -606,8 +602,7 @@ build_delta_tuple(SpockRelation *rel, SpockTupleData *oldtup, int remoteattnum = rel->attmap[attidx]; Assert(remoteattnum < tupdesc->natts); - if (rel->delta_apply_functions[remoteattnum] == InvalidOid && - rel->delta_functions[remoteattnum] == InvalidOid) + if (rel->delta_functions[remoteattnum] == InvalidOid) { deltatup->values[remoteattnum] = 0xdeadbeef; deltatup->nulls[remoteattnum] = true; @@ -634,8 +629,7 @@ build_delta_tuple(SpockRelation *rel, SpockTupleData *oldtup, { /* * This is a special case. Columns for delta apply need to be - * marked NOT NULL and LOG_OLD_VALUE=true. During this remote - * UPDATE LOG_OLD_VALUE setting was false. We use this as a flag + * marked NOT NULL. We use this as a flag * to force plain NEW value application. This is useful in case a * server ever gets out of sync. */ @@ -643,24 +637,12 @@ build_delta_tuple(SpockRelation *rel, SpockTupleData *oldtup, deltatup->nulls[remoteattnum] = false; deltatup->changed[remoteattnum] = true; } - else if (rel->delta_functions[remoteattnum] != InvalidOid) - { - loc_value = heap_getattr(TTS_TUP(localslot), remoteattnum + 1, tupdesc, - &loc_isnull); - - result = OidFunctionCall3Coll(rel->delta_functions[remoteattnum], - InvalidOid, oldtup->values[remoteattnum], - newtup->values[remoteattnum], loc_value); - deltatup->values[remoteattnum] = result; - deltatup->nulls[remoteattnum] = false; - deltatup->changed[remoteattnum] = true; - } else { loc_value = heap_getattr(TTS_TUP(localslot), remoteattnum + 1, tupdesc, &loc_isnull); - result = OidFunctionCall3Coll(rel->delta_apply_functions[remoteattnum], + result = OidFunctionCall3Coll(rel->delta_functions[remoteattnum], InvalidOid, oldtup->values[remoteattnum], newtup->values[remoteattnum], loc_value); deltatup->values[remoteattnum] = result; @@ -669,7 +651,6 @@ build_delta_tuple(SpockRelation *rel, SpockTupleData *oldtup, } } } -#endif /* NO_LOG_OLD_VALUE */ static ApplyExecState * init_apply_exec_state(SpockRelation *rel) @@ -787,7 +768,7 @@ spock_handle_conflict_and_apply(SpockRelation *rel, EState *estate, xmin, local_origin_found, local_origin, local_ts, idxused); - if (rel->has_delta_columns || rel->has_delta_apply) + if (rel->has_delta_apply) { SpockTupleData deltatup; HeapTuple currenttuple; diff --git a/src/spock_relcache.c b/src/spock_relcache.c index 1dfb3265..ce170b4d 100644 --- a/src/spock_relcache.c +++ b/src/spock_relcache.c @@ -55,15 +55,12 @@ relcache_free_entry(SpockRelation *entry) if (entry->attmap) pfree(entry->attmap); - if (entry->delta_apply_functions) - pfree(entry->delta_apply_functions); if (entry->delta_functions) pfree(entry->delta_functions); entry->natts = 0; entry->reloid = InvalidOid; entry->rel = NULL; - entry->has_delta_columns = false; entry->has_delta_apply = false; } @@ -99,46 +96,18 @@ spock_relation_open(uint32 remoteid, LOCKMODE lockmode) if (unlikely(entry->rel == NULL)) return NULL; - desc = RelationGetDescr(entry->rel); + for (i = 0; i < entry->natts; i++) { AttributeOpts *aopt; Form_pg_attribute att; ObjectAddress object; char *seclabel; + TupleDesc desc; + desc = RelationGetDescr(entry->rel); entry->attmap[i] = tupdesc_get_att_by_name(desc, entry->attnames[i]); - /* - * If we find attribute options for this column and the - * delta_apply_function is set, lookup the oid for it. - */ - att = TupleDescAttr(desc, entry->attmap[i]); - aopt = get_attribute_options(entry->rel->rd_id, - entry->attmap[i] + 1); - if (aopt != NULL && aopt->delta_apply_function != 0) - { - char *fname; - Form_pg_attribute att; - Oid dfunc; - - fname = pstrdup(GET_STRING_RELOPTION(aopt, - delta_apply_function)); - dfunc = spock_lookup_delta_function(fname, att->atttypid); - pfree(fname); - - if (dfunc == InvalidOid) - elog(ERROR, "SPOCK: column %s.%s.%s is configured for " - "delta_apply_function %s - function not found", - entry->nspname, entry->relname, - entry->attnames[i], - GET_STRING_RELOPTION(aopt, delta_apply_function)); - - - entry->has_delta_columns = true; - entry->delta_apply_functions[entry->attmap[i]] = dfunc; - } - if (entry->rel->rd_rel->relreplident != REPLICA_IDENTITY_FULL) continue; @@ -148,16 +117,27 @@ spock_relation_open(uint32 remoteid, LOCKMODE lockmode) * * XXX: What about non-existing columns on remote side? */ - object.classId = RelationRelationId; - object.objectId = RelationGetRelid(entry->rel); - object.objectSubId = entry->attmap[i] + 1; + ObjectAddressSubSet(object, RelationRelationId, + RelationGetRelid(entry->rel), + entry->attmap[i] + 1); seclabel = GetSecurityLabel(&object, spock_SECLABEL_PROVIDER); if (seclabel != NULL) { - entry->has_delta_apply = true; - entry->delta_functions[entry->attmap[i]] = - spock_lookup_delta_function(seclabel, att->atttypid); + Form_pg_attribute att; + Oid dfunc; + + att = TupleDescAttr(desc, entry->attmap[i]); + dfunc = spock_lookup_delta_function(seclabel, att->atttypid); + + if (dfunc == InvalidOid) + elog(ERROR, "SPOCK: column %s.%s.%s is configured for " + "delta_apply function %s - function not found", + entry->nspname, entry->relname, + entry->attnames[i], seclabel); + + entry->delta_functions[entry->attmap[i]] = dfunc; Assert(entry->delta_functions[entry->attmap[i]] != InvalidOid); + entry->has_delta_apply = true; } else { @@ -254,8 +234,6 @@ spock_relation_cache_update(uint32 remoteid, char *schemaname, entry->attrtypmods[i] = attrtypmods[i]; } entry->attmap = palloc(natts * sizeof(int)); - entry->has_delta_columns = false; - entry->delta_apply_functions = palloc0(natts * sizeof(Oid)); entry->has_delta_apply = false; entry->delta_functions = (Oid *) palloc0(entry->natts * sizeof(Oid)); MemoryContextSwitchTo(oldcontext); @@ -294,8 +272,6 @@ spock_relation_cache_updater(SpockRemoteRel *remoterel) for (i = 0; i < remoterel->natts; i++) entry->attnames[i] = pstrdup(remoterel->attnames[i]); entry->attmap = palloc(remoterel->natts * sizeof(int)); - entry->has_delta_columns = false; - entry->delta_apply_functions = palloc0(remoterel->natts * sizeof(Oid)); entry->has_delta_apply = false; entry->delta_functions = (Oid *) palloc0(entry->natts * sizeof(Oid)); MemoryContextSwitchTo(oldcontext); From 6c154a693629a91da70336a4d3a0a1115f89a9a4 Mon Sep 17 00:00:00 2001 From: "Andrei V. Lepikhov" Date: Fri, 28 Nov 2025 12:29:52 +0100 Subject: [PATCH 5/8] Let the security label-based delta apply feature be a first-class citizen. There is a limitation in Spock: FULL replica identity relations can't be replicated. So, we need to add machinery to identify specific delta_apply cases; there, we will use the PK on the replica and shouldn't run into any issues. Add a trivial tap test demonstrating how delta_apply works. TODO: tests for corner cases are needed: NULL value, restrictions, or incorrect usage of delta_apply. --- include/spock_relcache.h | 2 + sql/spock--5.0.4--6.0.0-devel.sql | 97 ++++++++++++++++++++++++++++ src/spock_autoddl.c | 6 +- src/spock_relcache.c | 41 ++++++++++++ src/spock_repset.c | 3 +- tests/tap/schedule | 1 + tests/tap/t/013_delta_apply.pl | 102 ++++++++++++++++++++++++++++++ 7 files changed, 249 insertions(+), 3 deletions(-) create mode 100644 tests/tap/t/013_delta_apply.pl diff --git a/include/spock_relcache.h b/include/spock_relcache.h index a71dcdc8..09ed0437 100644 --- a/include/spock_relcache.h +++ b/include/spock_relcache.h @@ -69,6 +69,8 @@ extern void spock_relation_cache_reset(void); extern Oid spock_lookup_delta_function(char *fname, Oid typeoid); +extern Oid get_replication_identity(Relation rel); + struct SpockTupleData; #endif /* SPOCK_RELCACHE_H */ diff --git a/sql/spock--5.0.4--6.0.0-devel.sql b/sql/spock--5.0.4--6.0.0-devel.sql index 5a54e744..306ee36d 100644 --- a/sql/spock--5.0.4--6.0.0-devel.sql +++ b/sql/spock--5.0.4--6.0.0-devel.sql @@ -67,3 +67,100 @@ SET conflict_type = CASE conflict_type WHEN 'delete_delete' THEN 'delete_missing' ELSE conflict_type END; + +-- Set delta_apply security label on specific column +CREATE FUNCTION spock.delta_apply( + rel regclass, + att_name name, + to_drop boolean DEFAULT false +) RETURNS boolean AS $$ +DECLARE + label text; + atttype name; + attdata record; + sqlstring text; + status boolean; + relreplident char (1); + ctypname name; +BEGIN + + /* + * regclass input type guarantees we see this table, no 'not found' check + * is needed. + */ + SELECT c.relreplident FROM pg_class c WHERE oid = rel INTO relreplident; + /* + * Allow only DEFAULT type of replica identity. FULL type means we have + * already requested delta_apply feature on this table. + * Avoid INDEX type because indexes may have different names on the nodes and + * it would be better to stay paranoid than afraid of consequences. + */ + IF (relreplident <> 'd' AND relreplident <> 'f') + THEN + RAISE EXCEPTION 'spock can apply delta_apply feature to the DEFAULT replica identity type only. This table holds "%" idenity', relreplident; + END IF; + + /* + * Find proper delta_apply function for the column type or ERROR + */ + + SELECT t.typname,t.typinput,t.typoutput + FROM pg_catalog.pg_attribute a, pg_type t + WHERE a.attrelid = rel AND a.attname = att_name AND (a.atttypid = t.oid) + INTO attdata; + IF NOT FOUND THEN + RAISE EXCEPTION 'column % does not exist in the table %', att_name, rel; + END IF; + + SELECT typname FROM pg_type WHERE + typname IN ('int2','int4','int8','float4','float8','numeric','money') AND + typinput = attdata.typinput AND typoutput = attdata.typoutput + INTO ctypname; + IF NOT FOUND THEN + RAISE EXCEPTION 'type "%" can not be used in delta_apply conflict resolution', + attdata.typname; + END IF; + + -- + -- Create security label on the column + -- + IF (to_drop = true) THEN + sqlstring := format('SECURITY LABEL FOR spock ON COLUMN %I.%I IS NULL;' , + rel, att_name); + ELSE + sqlstring := format('SECURITY LABEL FOR spock ON COLUMN %I.%I IS %L;' , + rel, att_name, 'spock.delta_apply'); + END IF; + + EXECUTE sqlstring; + + /* + * Auto replication will propagate security label if needed. Just warn if it's + * not - the structure sync pg_dump call would copy security labels, isn't it? + */ + SELECT pg_catalog.current_setting('spock.enable_ddl_replication') INTO status; + IF EXISTS (SELECT 1 FROM spock.local_node) AND status = false THEN + raise WARNING 'delta_apply setting has not been propagated to other spock nodes'; + END IF; + + IF EXISTS (SELECT 1 FROM pg_catalog.pg_seclabel + WHERE objoid = rel AND classoid = 'pg_class'::regclass AND + provider = 'spock') THEN + /* + * Call it each time to trigger relcache invalidation callback that causes + * refresh of the SpockRelation entry and guarantees actual state of the + * delta_apply columns. + */ + EXECUTE format('ALTER TABLE %I REPLICA IDENTITY FULL', rel); + ELSIF EXISTS (SELECT 1 FROM pg_catalog.pg_class c + WHERE c.oid = rel AND c.relreplident = 'f') THEN + /* + * Have removed he last security label. Revert this spock hack change, + * if needed. + */ + EXECUTE format('ALTER TABLE %I REPLICA IDENTITY DEFAULT', rel); + END IF; + + RETURN true; +END; +$$ LANGUAGE plpgsql STRICT VOLATILE; diff --git a/src/spock_autoddl.c b/src/spock_autoddl.c index 0428aa5d..a2e13549 100644 --- a/src/spock_autoddl.c +++ b/src/spock_autoddl.c @@ -21,7 +21,7 @@ #include "commands/defrem.h" #include "commands/extension.h" - +#include "commands/seclabel.h" #include "tcop/utility.h" #include "utils/builtins.h" #include "utils/lsyscache.h" @@ -30,6 +30,7 @@ #include "spock_autoddl.h" #include "spock_executor.h" #include "spock_queue.h" +#include "spock_relcache.h" #include "spock_repset.h" #include "spock_node.h" #include "spock_output_plugin.h" @@ -270,7 +271,8 @@ add_ddl_to_repset(Node *parsetree) } if (!OidIsValid(targetrel->rd_replidindex) && - (repset->replicate_update || repset->replicate_delete)) + (repset->replicate_update || repset->replicate_delete) && + !OidIsValid(get_replication_identity(targetrel))) { table_close(targetrel, NoLock); return; diff --git a/src/spock_relcache.c b/src/spock_relcache.c index ce170b4d..03761b9b 100644 --- a/src/spock_relcache.c +++ b/src/spock_relcache.c @@ -430,3 +430,44 @@ spock_lookup_delta_function(char *fname, Oid typeoid) return funcoid; } + +/* + * Detect if the FULL identity is just covering delta_apply + * feature underpinned by PK. + * This is quite a rare case. Don't afraid overhead. + */ +Oid +get_replication_identity(Relation rel) +{ + TupleDesc tupDesc = RelationGetDescr(rel); + int i = 0; + + if (OidIsValid(rel->rd_replidindex)) + return rel->rd_replidindex; + + if (rel->rd_rel->relreplident != REPLICA_IDENTITY_FULL || + !OidIsValid(rel->rd_pkindex)) + return InvalidOid; + + for (i = 0; i < tupDesc->natts; i++) + { + char *seclabel; + ObjectAddress object; + + ObjectAddressSubSet(object, RelationRelationId, + RelationGetRelid(rel), i + 1); + seclabel = GetSecurityLabel(&object, spock_SECLABEL_PROVIDER); + + if (seclabel != NULL) + { + /* + * Relation has at least on security label on it. Treat it as a + * delta_apply feature. + */ + pfree(seclabel); + return rel->rd_pkindex; + } + } + + return InvalidOid; +} diff --git a/src/spock_repset.c b/src/spock_repset.c index f0e2bcd1..6fac2a4c 100644 --- a/src/spock_repset.c +++ b/src/spock_repset.c @@ -45,6 +45,7 @@ #include "spock_dependency.h" #include "spock_node.h" #include "spock_queue.h" +#include "spock_relcache.h" #include "spock_repset.h" #include "spock.h" #include "spock_compat.h" @@ -1099,7 +1100,7 @@ replication_set_add_table(Oid setid, Oid reloid, List *att_list, if (targetrel->rd_indexvalid == 0) RelationGetIndexList(targetrel); - if (!OidIsValid(targetrel->rd_replidindex) && + if (!OidIsValid(get_replication_identity(targetrel)) && (repset->replicate_update || repset->replicate_delete)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), diff --git a/tests/tap/schedule b/tests/tap/schedule index 6bfe6451..00229b3c 100644 --- a/tests/tap/schedule +++ b/tests/tap/schedule @@ -15,6 +15,7 @@ test: 004_non_default_repset test: 008_rmgr test: 009_zodan_add_remove_nodes test: 010_zodan_add_remove_python +test: 013_delta_apply # Tests, consuming too much time to be launched on each check: #test: 011_zodan_sync_third diff --git a/tests/tap/t/013_delta_apply.pl b/tests/tap/t/013_delta_apply.pl new file mode 100644 index 00000000..1f7b0aef --- /dev/null +++ b/tests/tap/t/013_delta_apply.pl @@ -0,0 +1,102 @@ +use strict; +use warnings; +use Test::More tests => 13; +use IPC::Run; +use lib '.'; +use lib 't'; +use SpockTest qw(create_cluster destroy_cluster system_or_bail get_test_config cross_wire psql_or_bail scalar_query); + +my ($result1, $result2, $result3, $lsn1, $lsn2); + +create_cluster(3, 'Create initial 3-node Spock test cluster'); +cross_wire(3, ['n1', 'n2', 'n3'], 'Cross-wire nodes'); +# Get cluster configuration +my $config = get_test_config(); +my $node_count = $config->{node_count}; +my $node_ports = $config->{node_ports}; +my $host = $config->{host}; +my $dbname = $config->{db_name}; +my $db_user = $config->{db_user}; +my $db_password = $config->{db_password}; +my $pg_bin = $config->{pg_bin}; + +psql_or_bail(1, qq( + CREATE TABLE t1 (id integer PRIMARY KEY, x integer NOT NULL, y integer); + INSERT INTO t1 (id, x) VALUES (1,42); +)); + +psql_or_bail(1, 'SELECT spock.wait_slot_confirm_lsn(NULL, NULL)'); +psql_or_bail(2, 'SELECT spock.wait_slot_confirm_lsn(NULL, NULL)'); +psql_or_bail(3, 'SELECT spock.wait_slot_confirm_lsn(NULL, NULL)'); + +psql_or_bail(1, "SELECT spock.delta_apply('t1', 'x');"); +psql_or_bail(2, "SELECT spock.delta_apply('t1', 'x');"); +psql_or_bail(3, "SELECT spock.delta_apply('t1', 'x');"); + +psql_or_bail(3, q( +CREATE PROCEDURE counter_change(relname name, attname name, + value integer, cycles integer) +AS $$ +DECLARE + i integer := 0; +BEGIN + WHILE i < cycles LOOP + EXECUTE format('UPDATE %I SET %s = %s + %L;', relname, attname, attname, value); + -- raise WARNING '[%] iteration % from %', value, i, cycles; + COMMIT; + i := i + 1; + + END LOOP; + raise NOTICE '[%] FINISH: iteration % from %', value, i, cycles; +END; +$$ LANGUAGE plpgsql;)); + +psql_or_bail(1, 'SELECT spock.wait_slot_confirm_lsn(NULL, NULL)'); +psql_or_bail(2, 'SELECT spock.wait_slot_confirm_lsn(NULL, NULL)'); +psql_or_bail(3, 'SELECT spock.wait_slot_confirm_lsn(NULL, NULL)'); + +$result1 = scalar_query(1,"SELECT * FROM pg_catalog.pg_seclabels"); +$result2 = scalar_query(2,"SELECT * FROM pg_catalog.pg_seclabels"); +$result3 = scalar_query(3,"SELECT * FROM pg_catalog.pg_seclabels"); +print STDERR "DEBUGGING. Spock nodes IDs: \n$result1 $result2 $result3\n"; + +my ($pgbench_stdout1, $pgbench_stderr1) = ('', ''); +my $phandle1 = IPC::Run::start( + [ + 'psql', + '-c', "CALL counter_change('t1', 'x', -1, 10000)", + '-h', $host, '-p', $node_ports->[1], '-U', $db_user, $dbname + ], + '>' => \$pgbench_stdout1, + '2>' => \$pgbench_stderr1); +#$phandle1->pump(); + +psql_or_bail(1, "CALL counter_change('t1', 'x', +1, 10001)"); + +$phandle1->finish; +is($phandle1->full_result(0), 0, "alternative run successfull"); + +print STDERR "##### output of psql #####\n"; +print STDERR "$pgbench_stdout1"; +print STDERR "$pgbench_stderr1"; +print STDERR "##### end of output #####\n"; + +# Wait for sync ... +$lsn1 = scalar_query(1, "SELECT spock.sync_event()"); +$lsn2 = scalar_query(2, "SELECT spock.sync_event()"); +psql_or_bail(1, "CALL spock.wait_for_sync_event(true, 'n2', '$lsn2'::pg_lsn, 600)"); +psql_or_bail(2, "CALL spock.wait_for_sync_event(true, 'n1', '$lsn1'::pg_lsn, 600)"); +psql_or_bail(3, "CALL spock.wait_for_sync_event(true, 'n1', '$lsn1'::pg_lsn, 600)"); +psql_or_bail(3, "CALL spock.wait_for_sync_event(true, 'n2', '$lsn2'::pg_lsn, 600)"); + +$result1 = scalar_query(1, "SELECT x FROM t1"); +$result2 = scalar_query(2, "SELECT x FROM t1"); +$result3 = scalar_query(3, "SELECT x FROM t1"); + +ok($result1 eq '43', "Data on the node N1 has correct value"); +ok($result1 eq $result3, "Equality of the data on N1 and N3 is confirmed"); +ok($result2 eq $result3, "Equality of the data on N2 and N3 is confirmed"); +print STDERR "DEBUGGING. Results: $result1 | $result2 | $result3\n"; + +# Cleanup will be handled by SpockTest.pm END block +# No need for done_testing() when using a test plan From 57c21f67925158918011046ff0c03c40d81c41e2 Mon Sep 17 00:00:00 2001 From: "Andrei V. Lepikhov" Date: Tue, 2 Dec 2025 12:46:27 +0100 Subject: [PATCH 6/8] Disallow the delta apply feature on nullable columns. Having this allowed, we should resolve multiple cases with NULL op NULL and NULL op value that seem too expensive, keeping in mind that the database schema shouldn't let that in principle. This example also highlights that we don't yet support 2PC commit in our protocol. Here, it could do the stuff more consistently. TAP and regression tests were changed to fit the new behaviour. --- sql/spock--5.0.4--6.0.0-devel.sql | 17 +++++++- sql/spock--6.0.0-devel.sql | 17 +++++++- src/spock_apply_heap.c | 61 +++++++++++----------------- tests/regress/expected/autoddl.out | 2 +- tests/regress/expected/basic.out | 6 +-- tests/regress/expected/infofuncs.out | 2 +- tests/regress/sql/autoddl.sql | 2 +- tests/regress/sql/basic.sql | 6 +-- tests/regress/sql/infofuncs.sql | 2 +- tests/tap/t/013_delta_apply.pl | 11 +++-- 10 files changed, 74 insertions(+), 52 deletions(-) diff --git a/sql/spock--5.0.4--6.0.0-devel.sql b/sql/spock--5.0.4--6.0.0-devel.sql index 306ee36d..7695cd1d 100644 --- a/sql/spock--5.0.4--6.0.0-devel.sql +++ b/sql/spock--5.0.4--6.0.0-devel.sql @@ -104,7 +104,7 @@ BEGIN * Find proper delta_apply function for the column type or ERROR */ - SELECT t.typname,t.typinput,t.typoutput + SELECT t.typname,t.typinput,t.typoutput, a.attnotnull FROM pg_catalog.pg_attribute a, pg_type t WHERE a.attrelid = rel AND a.attname = att_name AND (a.atttypid = t.oid) INTO attdata; @@ -112,6 +112,21 @@ BEGIN RAISE EXCEPTION 'column % does not exist in the table %', att_name, rel; END IF; + IF (attdata.attnotnull = false) THEN + /* + * TODO: Here is a case where the table has different constraints on nodes. + * Using prepared transactions, we might be sure this operation will finish + * if only each node satisfies the rule. But we need to add support for 2PC + * commit beforehand. + */ + RAISE NOTICE USING + MESSAGE = format('delta_apply feature can not be applied to nullable column %L of the table %I', + att_name, rel), + HINT = 'Set NOT NULL constraint on the column', + ERRCODE = 'object_not_in_prerequisite_state'; + RETURN false; + END IF; + SELECT typname FROM pg_type WHERE typname IN ('int2','int4','int8','float4','float8','numeric','money') AND typinput = attdata.typinput AND typoutput = attdata.typoutput diff --git a/sql/spock--6.0.0-devel.sql b/sql/spock--6.0.0-devel.sql index 2fecdb29..580265f6 100644 --- a/sql/spock--6.0.0-devel.sql +++ b/sql/spock--6.0.0-devel.sql @@ -684,7 +684,7 @@ BEGIN * Find proper delta_apply function for the column type or ERROR */ - SELECT t.typname,t.typinput,t.typoutput + SELECT t.typname,t.typinput,t.typoutput, a.attnotnull FROM pg_catalog.pg_attribute a, pg_type t WHERE a.attrelid = rel AND a.attname = att_name AND (a.atttypid = t.oid) INTO attdata; @@ -692,6 +692,21 @@ BEGIN RAISE EXCEPTION 'column % does not exist in the table %', att_name, rel; END IF; + IF (attdata.attnotnull = false) THEN + /* + * TODO: Here is a case where the table has different constraints on nodes. + * Using prepared transactions, we might be sure this operation will finish + * if only each node satisfies the rule. But we need to add support for 2PC + * commit beforehand. + */ + RAISE NOTICE USING + MESSAGE = format('delta_apply feature can not be applied to nullable column %L of the table %I', + att_name, rel), + HINT = 'Set NOT NULL constraint on the column', + ERRCODE = 'object_not_in_prerequisite_state'; + RETURN false; + END IF; + SELECT typname FROM pg_type WHERE typname IN ('int2','int4','int8','float4','float8','numeric','money') AND typinput = attdata.typinput AND typoutput = attdata.typoutput diff --git a/src/spock_apply_heap.c b/src/spock_apply_heap.c index 39eb4044..65f52fde 100644 --- a/src/spock_apply_heap.c +++ b/src/spock_apply_heap.c @@ -611,44 +611,31 @@ build_delta_tuple(SpockRelation *rel, SpockTupleData *oldtup, } /* - * Column is marked LOG_OLD_VALUE=true. We use that as flag to apply - * the delta between the remote old and new instead of the plain new - * value. - * - * To perform the actual delta math we need the functions behind the - * '+' and '-' operators for the data type. - * - * XXX: This is currently hardcoded for the builtin data types we - * support. Ideally we would lookup those operators in the system - * cache, but that isn't straight forward and we get into all sorts of - * trouble when it comes to user defined data types and the search - * path. + * This shouldn't happen: creating delta_apply column we must check that + * NOT NULL constraint is set on this column or reject. But just to + * survive in case of a bug we complain and send the apply worker to + * exception behavior path way. */ - - if (oldtup->nulls[remoteattnum]) - { - /* - * This is a special case. Columns for delta apply need to be - * marked NOT NULL. We use this as a flag - * to force plain NEW value application. This is useful in case a - * server ever gets out of sync. - */ - deltatup->values[remoteattnum] = newtup->values[remoteattnum]; - deltatup->nulls[remoteattnum] = false; - deltatup->changed[remoteattnum] = true; - } - else - { - loc_value = heap_getattr(TTS_TUP(localslot), remoteattnum + 1, tupdesc, - &loc_isnull); - - result = OidFunctionCall3Coll(rel->delta_functions[remoteattnum], - InvalidOid, oldtup->values[remoteattnum], - newtup->values[remoteattnum], loc_value); - deltatup->values[remoteattnum] = result; - deltatup->nulls[remoteattnum] = false; - deltatup->changed[remoteattnum] = true; - } + if (oldtup->nulls[remoteattnum] || newtup->nulls[remoteattnum]) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("delta apply column can't operate NULL values"), + errdetail("attribute %d for remote tuple is %s, and for the local tuple is %s", + remoteattnum + 1, + newtup->nulls[remoteattnum] ? "NULL" : "NOT NULL", + oldtup->nulls[remoteattnum] ? "NULL" : "NOT NULL" + ))); + + loc_value = heap_getattr(TTS_TUP(localslot), remoteattnum + 1, tupdesc, + &loc_isnull); + Assert(!loc_isnull); + + result = OidFunctionCall3Coll(rel->delta_functions[remoteattnum], + InvalidOid, oldtup->values[remoteattnum], + newtup->values[remoteattnum], loc_value); + deltatup->values[remoteattnum] = result; + deltatup->nulls[remoteattnum] = false; + deltatup->changed[remoteattnum] = true; } } diff --git a/tests/regress/expected/autoddl.out b/tests/regress/expected/autoddl.out index ccd97590..3478f981 100644 --- a/tests/regress/expected/autoddl.out +++ b/tests/regress/expected/autoddl.out @@ -103,7 +103,7 @@ INFO: DDL statement replicated. \c :provider_dsn \set VERBOSITY terse -- Check propagation of security labels -CREATE TABLE slabel1 (x integer, y text PRIMARY KEY); +CREATE TABLE slabel1 (x integer NOT NULL, y text PRIMARY KEY); INFO: DDL statement replicated. SELECT spock.delta_apply('slabel1', 'x'); INFO: DDL statement replicated. diff --git a/tests/regress/expected/basic.out b/tests/regress/expected/basic.out index 32b095d3..78ef5101 100644 --- a/tests/regress/expected/basic.out +++ b/tests/regress/expected/basic.out @@ -217,8 +217,8 @@ DROP FUNCTION call_fn; -- Remember, these tests still check intra-node behaviour. -- -- A label creation checks -CREATE TABLE slabel (x integer, y text PRIMARY KEY); -CREATE TABLE slabel_ri (x integer NOT NULL, y text); +CREATE TABLE slabel (x integer NOT NULL, y text PRIMARY KEY); +CREATE TABLE slabel_ri (x integer NOT NULL, y text, z text); CREATE UNIQUE INDEX slabel_ri_idx ON slabel_ri(x); ALTER TABLE slabel_ri REPLICA IDENTITY USING INDEX slabel_ri_idx; SELECT spock.delta_apply('slabel', 'x'); @@ -289,7 +289,7 @@ SELECT objname, label FROM pg_seclabels; (0 rows) ALTER TABLE slabel DROP COLUMN x; -ALTER TABLE slabel ADD COLUMN x numeric; +ALTER TABLE slabel ADD COLUMN x numeric NOT NULL; SELECT spock.delta_apply('slabel', 'x', false); WARNING: delta_apply setting has not been propagated to other spock nodes delta_apply diff --git a/tests/regress/expected/infofuncs.out b/tests/regress/expected/infofuncs.out index 5e33bef4..b2813db0 100644 --- a/tests/regress/expected/infofuncs.out +++ b/tests/regress/expected/infofuncs.out @@ -21,7 +21,7 @@ WHERE extname = 'spock'; (1 row) -- Check that security label is cleaned up on the extension drop -CREATE TABLE slabel (x money, y text PRIMARY KEY); +CREATE TABLE slabel (x money NOT NULL, y text PRIMARY KEY); SELECT spock.delta_apply('slabel', 'x', false); delta_apply ------------- diff --git a/tests/regress/sql/autoddl.sql b/tests/regress/sql/autoddl.sql index 71698376..f71a12cb 100644 --- a/tests/regress/sql/autoddl.sql +++ b/tests/regress/sql/autoddl.sql @@ -68,7 +68,7 @@ DROP TABLE test_383 CASCADE; \c :provider_dsn \set VERBOSITY terse -- Check propagation of security labels -CREATE TABLE slabel1 (x integer, y text PRIMARY KEY); +CREATE TABLE slabel1 (x integer NOT NULL, y text PRIMARY KEY); SELECT spock.delta_apply('slabel1', 'x'); SELECT spock.delta_apply('slabel1', 'y'); -- ERROR diff --git a/tests/regress/sql/basic.sql b/tests/regress/sql/basic.sql index ef6ea088..3bdc63a1 100644 --- a/tests/regress/sql/basic.sql +++ b/tests/regress/sql/basic.sql @@ -118,8 +118,8 @@ DROP FUNCTION call_fn; -- -- A label creation checks -CREATE TABLE slabel (x integer, y text PRIMARY KEY); -CREATE TABLE slabel_ri (x integer NOT NULL, y text); +CREATE TABLE slabel (x integer NOT NULL, y text PRIMARY KEY); +CREATE TABLE slabel_ri (x integer NOT NULL, y text, z text); CREATE UNIQUE INDEX slabel_ri_idx ON slabel_ri(x); ALTER TABLE slabel_ri REPLICA IDENTITY USING INDEX slabel_ri_idx; @@ -147,7 +147,7 @@ SELECT spock.delta_apply('slabel', 'x', false); ALTER TABLE slabel ALTER COLUMN x TYPE text; -- just warn SELECT objname, label FROM pg_seclabels; ALTER TABLE slabel DROP COLUMN x; -ALTER TABLE slabel ADD COLUMN x numeric; +ALTER TABLE slabel ADD COLUMN x numeric NOT NULL; SELECT spock.delta_apply('slabel', 'x', false); ALTER TABLE slabel DROP COLUMN x; SELECT objname, label FROM pg_seclabels; diff --git a/tests/regress/sql/infofuncs.sql b/tests/regress/sql/infofuncs.sql index c0b19cdb..f21daf66 100644 --- a/tests/regress/sql/infofuncs.sql +++ b/tests/regress/sql/infofuncs.sql @@ -10,7 +10,7 @@ FROM pg_extension WHERE extname = 'spock'; -- Check that security label is cleaned up on the extension drop -CREATE TABLE slabel (x money, y text PRIMARY KEY); +CREATE TABLE slabel (x money NOT NULL, y text PRIMARY KEY); SELECT spock.delta_apply('slabel', 'x', false); SELECT objname, label FROM pg_seclabels; diff --git a/tests/tap/t/013_delta_apply.pl b/tests/tap/t/013_delta_apply.pl index 1f7b0aef..49213e2e 100644 --- a/tests/tap/t/013_delta_apply.pl +++ b/tests/tap/t/013_delta_apply.pl @@ -1,6 +1,6 @@ use strict; use warnings; -use Test::More tests => 13; +use Test::More tests => 14; use IPC::Run; use lib '.'; use lib 't'; @@ -30,8 +30,8 @@ psql_or_bail(3, 'SELECT spock.wait_slot_confirm_lsn(NULL, NULL)'); psql_or_bail(1, "SELECT spock.delta_apply('t1', 'x');"); -psql_or_bail(2, "SELECT spock.delta_apply('t1', 'x');"); -psql_or_bail(3, "SELECT spock.delta_apply('t1', 'x');"); +#psql_or_bail(2, "SELECT spock.delta_apply('t1', 'x');"); +#psql_or_bail(3, "SELECT spock.delta_apply('t1', 'x');"); psql_or_bail(3, q( CREATE PROCEDURE counter_change(relname name, attname name, @@ -98,5 +98,10 @@ BEGIN ok($result2 eq $result3, "Equality of the data on N2 and N3 is confirmed"); print STDERR "DEBUGGING. Results: $result1 | $result2 | $result3\n"; +# Remove delta_apply from the column x and set it on the column y +psql_or_bail(1, "SELECT spock.delta_apply('t1', 'x', true)"); +$result1 = scalar_query(1, "SELECT spock.delta_apply('t1', 'y')"); # ERROR +print STDERR "DEBUGGING. Result: $result1\n"; +ok($result1 eq 'f', "delta_apply can't be used with nullable columns"); # Cleanup will be handled by SpockTest.pm END block # No need for done_testing() when using a test plan From 6f25d27476c4d82f38b2054ca39badd4f0ef2914 Mon Sep 17 00:00:00 2001 From: "Andrei V. Lepikhov" Date: Wed, 17 Dec 2025 14:01:20 +0100 Subject: [PATCH 7/8] Rebase onto current master --- src/spock_relcache.c | 9 +++------ tests/tap/schedule | 3 +-- tests/tap/t/{013_delta_apply.pl => 012_delta_apply.pl} | 0 3 files changed, 4 insertions(+), 8 deletions(-) rename tests/tap/t/{013_delta_apply.pl => 012_delta_apply.pl} (100%) diff --git a/src/spock_relcache.c b/src/spock_relcache.c index 03761b9b..1b1a96ab 100644 --- a/src/spock_relcache.c +++ b/src/spock_relcache.c @@ -85,10 +85,9 @@ spock_relation_open(uint32 remoteid, LOCKMODE lockmode) /* Need to update the local cache? */ if (!OidIsValid(entry->reloid)) { - RangeVar *rv = makeNode(RangeVar); - int i; - TupleDesc desc; - ResultRelInfo *relinfo; + RangeVar *rv = makeNode(RangeVar); + int i; + ResultRelInfo *relinfo; rv->schemaname = (char *) entry->nspname; rv->relname = (char *) entry->relname; @@ -99,8 +98,6 @@ spock_relation_open(uint32 remoteid, LOCKMODE lockmode) for (i = 0; i < entry->natts; i++) { - AttributeOpts *aopt; - Form_pg_attribute att; ObjectAddress object; char *seclabel; TupleDesc desc; diff --git a/tests/tap/schedule b/tests/tap/schedule index 00229b3c..266451f9 100644 --- a/tests/tap/schedule +++ b/tests/tap/schedule @@ -15,7 +15,6 @@ test: 004_non_default_repset test: 008_rmgr test: 009_zodan_add_remove_nodes test: 010_zodan_add_remove_python -test: 013_delta_apply # Tests, consuming too much time to be launched on each check: #test: 011_zodan_sync_third @@ -31,7 +30,7 @@ test: 013_delta_apply # break # fi # done - +test: 012_delta_apply # Exception handling tests test: 013_exception_handling # test: 014_rolling_upgrade diff --git a/tests/tap/t/013_delta_apply.pl b/tests/tap/t/012_delta_apply.pl similarity index 100% rename from tests/tap/t/013_delta_apply.pl rename to tests/tap/t/012_delta_apply.pl From d7d2ba8732dc21aafc20f228871ff296c39095d5 Mon Sep 17 00:00:00 2001 From: "Andrei V. Lepikhov" Date: Tue, 30 Dec 2025 12:50:50 +0100 Subject: [PATCH 8/8] Rename delta_apply related variables. While developing a non-core version of the delta_apply feature, we have retained both versions for testing. Now we finalise the switch from core-based to an extensible approach and rename variables to match those used in the previous implementation. --- include/spock.h | 2 +- include/spock_relcache.h | 4 ++-- src/spock.c | 2 +- src/spock_apply_heap.c | 6 +++--- src/spock_executor.c | 4 ++-- src/spock_relcache.c | 28 ++++++++++++++-------------- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/include/spock.h b/include/spock.h index 508ead26..b4701df4 100644 --- a/include/spock.h +++ b/include/spock.h @@ -28,7 +28,7 @@ #define SPOCK_VERSION_NUM 60000 #define EXTENSION_NAME "spock" -#define spock_SECLABEL_PROVIDER "spock" +#define SPOCK_SECLABEL_PROVIDER "spock" #define REPLICATION_ORIGIN_ALL "all" diff --git a/include/spock_relcache.h b/include/spock_relcache.h index 09ed0437..c2048c54 100644 --- a/include/spock_relcache.h +++ b/include/spock_relcache.h @@ -48,8 +48,8 @@ typedef struct SpockRelation /* Additional cache, only valid as long as relation mapping is. */ bool hasTriggers; - Oid *delta_functions; - bool has_delta_apply; + Oid *delta_apply_functions; + bool has_delta_columns; } SpockRelation; extern void spock_relation_cache_update(uint32 remoteid, diff --git a/src/spock.c b/src/spock.c index be47fe30..28343f7a 100644 --- a/src/spock.c +++ b/src/spock.c @@ -1200,5 +1200,5 @@ _PG_init(void) emit_log_hook = log_message_filter; /* Security label provider hook */ - register_label_provider(spock_SECLABEL_PROVIDER, spock_object_relabel); + register_label_provider(SPOCK_SECLABEL_PROVIDER, spock_object_relabel); } diff --git a/src/spock_apply_heap.c b/src/spock_apply_heap.c index 65f52fde..54cd694e 100644 --- a/src/spock_apply_heap.c +++ b/src/spock_apply_heap.c @@ -602,7 +602,7 @@ build_delta_tuple(SpockRelation *rel, SpockTupleData *oldtup, int remoteattnum = rel->attmap[attidx]; Assert(remoteattnum < tupdesc->natts); - if (rel->delta_functions[remoteattnum] == InvalidOid) + if (rel->delta_apply_functions[remoteattnum] == InvalidOid) { deltatup->values[remoteattnum] = 0xdeadbeef; deltatup->nulls[remoteattnum] = true; @@ -630,7 +630,7 @@ build_delta_tuple(SpockRelation *rel, SpockTupleData *oldtup, &loc_isnull); Assert(!loc_isnull); - result = OidFunctionCall3Coll(rel->delta_functions[remoteattnum], + result = OidFunctionCall3Coll(rel->delta_apply_functions[remoteattnum], InvalidOid, oldtup->values[remoteattnum], newtup->values[remoteattnum], loc_value); deltatup->values[remoteattnum] = result; @@ -755,7 +755,7 @@ spock_handle_conflict_and_apply(SpockRelation *rel, EState *estate, xmin, local_origin_found, local_origin, local_ts, idxused); - if (rel->has_delta_apply) + if (rel->has_delta_columns) { SpockTupleData deltatup; HeapTuple currenttuple; diff --git a/src/spock_executor.c b/src/spock_executor.c index 6512da1a..f7eaa743 100644 --- a/src/spock_executor.c +++ b/src/spock_executor.c @@ -215,7 +215,7 @@ DeleteSecurityLabels(const char *provider) Assert(!isnull); provider = TextDatumGetCString(datum); - if (strcmp(provider, spock_SECLABEL_PROVIDER) != 0) + if (strcmp(provider, SPOCK_SECLABEL_PROVIDER) != 0) continue; CatalogTupleDelete(pg_seclabel, &htup->t_self); @@ -280,7 +280,7 @@ spock_object_access(ObjectAccessType access, if (dropping_spock_obj) { /* Need to drop any security labels created by the extension */ - DeleteSecurityLabels(spock_SECLABEL_PROVIDER); + DeleteSecurityLabels(SPOCK_SECLABEL_PROVIDER); return; } diff --git a/src/spock_relcache.c b/src/spock_relcache.c index 1b1a96ab..b1dd0ff1 100644 --- a/src/spock_relcache.c +++ b/src/spock_relcache.c @@ -55,13 +55,13 @@ relcache_free_entry(SpockRelation *entry) if (entry->attmap) pfree(entry->attmap); - if (entry->delta_functions) - pfree(entry->delta_functions); + if (entry->delta_apply_functions) + pfree(entry->delta_apply_functions); entry->natts = 0; entry->reloid = InvalidOid; entry->rel = NULL; - entry->has_delta_apply = false; + entry->has_delta_columns = false; } @@ -117,7 +117,7 @@ spock_relation_open(uint32 remoteid, LOCKMODE lockmode) ObjectAddressSubSet(object, RelationRelationId, RelationGetRelid(entry->rel), entry->attmap[i] + 1); - seclabel = GetSecurityLabel(&object, spock_SECLABEL_PROVIDER); + seclabel = GetSecurityLabel(&object, SPOCK_SECLABEL_PROVIDER); if (seclabel != NULL) { Form_pg_attribute att; @@ -132,21 +132,21 @@ spock_relation_open(uint32 remoteid, LOCKMODE lockmode) entry->nspname, entry->relname, entry->attnames[i], seclabel); - entry->delta_functions[entry->attmap[i]] = dfunc; - Assert(entry->delta_functions[entry->attmap[i]] != InvalidOid); - entry->has_delta_apply = true; + entry->delta_apply_functions[entry->attmap[i]] = dfunc; + Assert(entry->delta_apply_functions[entry->attmap[i]] != InvalidOid); + entry->has_delta_columns = true; } else { /* Main case */ - entry->delta_functions[entry->attmap[i]] = InvalidOid; + entry->delta_apply_functions[entry->attmap[i]] = InvalidOid; } } relinfo = makeNode(ResultRelInfo); InitResultRelInfo(relinfo, entry->rel, 1, NULL, 0); entry->reloid = RelationGetRelid(entry->rel); - if (entry->has_delta_apply) + if (entry->has_delta_columns) { /* * It looks like a hack — which, in fact, it is. @@ -231,8 +231,8 @@ spock_relation_cache_update(uint32 remoteid, char *schemaname, entry->attrtypmods[i] = attrtypmods[i]; } entry->attmap = palloc(natts * sizeof(int)); - entry->has_delta_apply = false; - entry->delta_functions = (Oid *) palloc0(entry->natts * sizeof(Oid)); + entry->has_delta_columns = false; + entry->delta_apply_functions = (Oid *) palloc0(entry->natts * sizeof(Oid)); MemoryContextSwitchTo(oldcontext); /* XXX Should we validate the relation against local schema here? */ @@ -269,8 +269,8 @@ spock_relation_cache_updater(SpockRemoteRel *remoterel) for (i = 0; i < remoterel->natts; i++) entry->attnames[i] = pstrdup(remoterel->attnames[i]); entry->attmap = palloc(remoterel->natts * sizeof(int)); - entry->has_delta_apply = false; - entry->delta_functions = (Oid *) palloc0(entry->natts * sizeof(Oid)); + entry->has_delta_columns = false; + entry->delta_apply_functions = (Oid *) palloc0(entry->natts * sizeof(Oid)); MemoryContextSwitchTo(oldcontext); /* XXX Should we validate the relation against local schema here? */ @@ -453,7 +453,7 @@ get_replication_identity(Relation rel) ObjectAddressSubSet(object, RelationRelationId, RelationGetRelid(rel), i + 1); - seclabel = GetSecurityLabel(&object, spock_SECLABEL_PROVIDER); + seclabel = GetSecurityLabel(&object, SPOCK_SECLABEL_PROVIDER); if (seclabel != NULL) {