From 6c58dfe4c5f4b972059454b98c1bb709b45e7930 Mon Sep 17 00:00:00 2001 From: Craig Ringer Date: Mon, 22 Jun 2015 15:30:07 +0800 Subject: [PATCH] Improvements to BDR/UDR join sanity checking This is a set of related changes to the init/join processes, focused on sanity checks and robustness when joining nodes. Introduces extension rev bdr 0.10.0.5. * Expose bdr_internal_create_truncate_trigger function to add the bdr truncate_trigger to a table. Use it from bdr_truncate_trigger_add. * Use bdr_truncate_trigger_add in bdr_group_join to create truncate triggers on all existing tables when BDR gets enabled. Previously we'd fail to replicate TRUNCATE for tables created before CREATE EXTENSION bdr; was run. * Fix broken prototype for bdr_truncate_trigger_add * In bdr_group_join prohibit enabling BDR where exclusion constraints exist in the database's current state. Previously you could get a BDR db with exclusion constraints and/or a broken clone by creating a table with exclusion constraints before enabling BDR. * In bdr_group_join, warn users if there are secondary unique indexes on tables, as currently we can't do last-update-wins conflict handling on tuples that violate multiple unique constraints. See issue #95. Still permit join to continue. * Warn users about missing PRIMARY KEYs, but permit join to continue, as it's legal to have no PK on an INSERT-only table. * In bdr.internal_begin_join, use bdr_test_replication_connection to test the remote DSN in replication mode and make sure that pg_hba.conf permits connection in replication mode. Fixes #94. --- Makefile.in | 10 +- bdr.control | 2 +- bdr_executor.c | 91 +++++++--- expected/init_bdr.out | 3 + expected/upgrade.out | 7 +- extsql/bdr--0.10.0.4--0.10.0.5.sql | 273 +++++++++++++++++++++++++++++ sql/upgrade.sql | 4 + 7 files changed, 357 insertions(+), 33 deletions(-) create mode 100644 extsql/bdr--0.10.0.4--0.10.0.5.sql diff --git a/Makefile.in b/Makefile.in index 68c3b4a55d..779ac14681 100644 --- a/Makefile.in +++ b/Makefile.in @@ -33,7 +33,8 @@ DATA = \ extsql/bdr--0.10.0.0--0.10.0.1.sql \ extsql/bdr--0.10.0.1--0.10.0.2.sql \ extsql/bdr--0.10.0.2--0.10.0.3.sql \ - extsql/bdr--0.10.0.3--0.10.0.4.sql + extsql/bdr--0.10.0.3--0.10.0.4.sql \ + extsql/bdr--0.10.0.4--0.10.0.5.sql DATA_built = \ extsql/bdr--0.8.0.1.sql \ @@ -54,7 +55,8 @@ DATA_built = \ extsql/bdr--0.10.0.1.sql \ extsql/bdr--0.10.0.2.sql \ extsql/bdr--0.10.0.3.sql \ - extsql/bdr--0.10.0.4.sql + extsql/bdr--0.10.0.4.sql \ + extsql/bdr--0.10.0.5.sql DOCS = bdr.conf.sample README.bdr SCRIPTS = scripts/bdr_initial_load bdr_init_copy bdr_resetxlog bdr_dump @@ -211,6 +213,10 @@ extsql/bdr--0.10.0.4.sql: extsql/bdr--0.10.0.3.sql extsql/bdr--0.10.0.3--0.10.0. mkdir -p extsql cat $^ > $@ +extsql/bdr--0.10.0.5.sql: extsql/bdr--0.10.0.4.sql extsql/bdr--0.10.0.4--0.10.0.5.sql + mkdir -p extsql + cat $^ > $@ + bdr_resetxlog: pg_resetxlog.o $(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(libpq_pgport) $(LIBS) -o $@$(X) diff --git a/bdr.control b/bdr.control index f1a54470b9..f513dff24c 100644 --- a/bdr.control +++ b/bdr.control @@ -1,6 +1,6 @@ # bdr extension comment = 'Bi-directional replication for PostgreSQL' -default_version = '0.10.0.4' +default_version = '0.10.0.5' module_pathname = '$libdir/bdr' relocatable = false requires = btree_gist diff --git a/bdr_executor.c b/bdr_executor.c index 66c1a126c1..494c9e556f 100644 --- a/bdr_executor.c +++ b/bdr_executor.c @@ -74,8 +74,10 @@ PG_FUNCTION_INFO_V1(bdr_queue_dropped_objects); #endif PGDLLEXPORT Datum bdr_replicate_ddl_command(PG_FUNCTION_ARGS); PG_FUNCTION_INFO_V1(bdr_replicate_ddl_command); -PGDLLEXPORT Datum bdr_(PG_FUNCTION_ARGS); +PGDLLEXPORT Datum bdr_truncate_trigger_add(PG_FUNCTION_ARGS); PG_FUNCTION_INFO_V1(bdr_truncate_trigger_add); +PGDLLEXPORT Datum bdr_internal_create_truncate_trigger(PG_FUNCTION_ARGS); +PG_FUNCTION_INFO_V1(bdr_internal_create_truncate_trigger); PGDLLEXPORT Datum bdr_node_set_read_only(PG_FUNCTION_ARGS); PG_FUNCTION_INFO_V1(bdr_node_set_read_only); @@ -374,11 +376,67 @@ bdr_queue_ddl_command(char *command_tag, char *command) heap_close(queuedcmds, RowExclusiveLock); } +/* + * Create a TRUNCATE trigger for a persistent table and mark + * it tgisinternal so that it's not dumped by pg_dump. + * + * We create such triggers automatically on restore or + * bdr_group_create so dumping the triggers isn't necessary, and + * also makes restores to non-BDR databases harder. + */ +static void +bdr_create_truncate_trigger(const char *relname, const char *nspname) +{ + char *query; + int res; + + SPI_connect(); + + query = psprintf("CREATE TRIGGER truncate_trigger AFTER TRUNCATE " + "ON %s.%s FOR EACH STATEMENT EXECUTE PROCEDURE " + "bdr.queue_truncate()", + quote_identifier(nspname), + quote_identifier(relname)); + res = SPI_execute(query, false, 0); + if (res != SPI_OK_UTILITY) + elog(ERROR, "SPI failure: %d", res); + + /* + * If this is inside manually replicated DDL, the + * bdr_queue_ddl_commands will skip queueing the CREATE TRIGGER + * command, so we have to do it ourselves. + * + * XXX: The whole in_bdr_replicate_ddl_command concept is not very nice + */ + if (in_bdr_replicate_ddl_command) + bdr_queue_ddl_command("CREATE TRIGGER", query); + + SPI_finish(); +} + +/* + * Wrapper to call bdr_create_truncate_trigger from SQL for + * during bdr_group_create(...). + */ +Datum +bdr_internal_create_truncate_trigger(PG_FUNCTION_ARGS) +{ + Oid relid = PG_GETARG_OID(0); + Relation rel = heap_open(relid, AccessExclusiveLock); + const char * relname = RelationGetRelationName(rel); + char * nspname = get_namespace_name(RelationGetNamespace(rel)); + bdr_create_truncate_trigger(relname, nspname); + pfree(nspname); + heap_close(rel, AccessExclusiveLock); + PG_RETURN_VOID(); +} + /* * bdr_truncate_trigger_add * - * This function adds TRUNCATE trigger to newly created tables. + * This function, which is called as an event trigger handler, adds TRUNCATE + * trigger to newly created tables where appropriate. * * Note: it's important that this function be named so that it comes * after bdr_queue_ddl_commands when triggers are alphabetically sorted. @@ -414,38 +472,15 @@ bdr_truncate_trigger_add(PG_FUNCTION_ARGS) IsA(trigdata->parsetree, CreateStmt)) { CreateStmt *stmt = (CreateStmt *)trigdata->parsetree; - char *nspname; - char *query; - int res; + char * nspname; /* Skip temporary and unlogged tables */ if (stmt->relation->relpersistence != RELPERSISTENCE_PERMANENT) PG_RETURN_VOID(); nspname = get_namespace_name(RangeVarGetCreationNamespace(stmt->relation)); - - SPI_connect(); - - query = psprintf("CREATE TRIGGER truncate_trigger AFTER TRUNCATE " - "ON %s.%s FOR EACH STATEMENT EXECUTE PROCEDURE " - "bdr.queue_truncate()", - quote_identifier(nspname), - quote_identifier(stmt->relation->relname)); - res = SPI_execute(query, false, 0); - if (res != SPI_OK_UTILITY) - elog(ERROR, "SPI failure: %d", res); - - /* - * If this is inside manually replicated DDL, the - * bdr_queue_ddl_commands will skip queueing the CREATE TRIGGER - * command, so we have to do it ourselves. - * - * XXX: The whole in_bdr_replicate_ddl_command concept is not very nice - */ - if (in_bdr_replicate_ddl_command) - bdr_queue_ddl_command("CREATE TRIGGER", query); - - SPI_finish(); + bdr_create_truncate_trigger(stmt->relation->relname, nspname); + pfree(nspname); } PG_RETURN_VOID(); diff --git a/expected/init_bdr.out b/expected/init_bdr.out index 3f7b1e0343..7cf2b6a492 100644 --- a/expected/init_bdr.out +++ b/expected/init_bdr.out @@ -4,6 +4,9 @@ SELECT bdr.bdr_group_create( node_external_dsn := 'dbname=postgres', replication_sets := ARRAY['default', 'important', 'for-node-1'] ); +WARNING: Secondary unique constraint(s) exist on replicated table(s) +DETAIL: Table public.some_local_tbl has secondary unique constraint some_local_tbl_key_key. This may cause unhandled replication conflicts. +HINT: Drop the secondary unique constraint(s), change the table(s) to UNLOGGED if they don't need to be replicated, or exclude the table(s) from the active replication set(s). bdr_group_create ------------------ diff --git a/expected/upgrade.out b/expected/upgrade.out index 4b3bdd2252..44a91f70a5 100644 --- a/expected/upgrade.out +++ b/expected/upgrade.out @@ -47,6 +47,8 @@ CREATE EXTENSION bdr VERSION '0.10.0.3'; DROP EXTENSION bdr; CREATE EXTENSION bdr VERSION '0.10.0.4'; DROP EXTENSION bdr; +CREATE EXTENSION bdr VERSION '0.10.0.5'; +DROP EXTENSION bdr; -- evolve version one by one from the oldest to the newest one CREATE EXTENSION bdr VERSION '0.8.0'; ALTER EXTENSION bdr UPDATE TO '0.8.0.1'; @@ -68,14 +70,15 @@ ALTER EXTENSION bdr UPDATE TO '0.10.0.1'; ALTER EXTENSION bdr UPDATE TO '0.10.0.2'; ALTER EXTENSION bdr UPDATE TO '0.10.0.3'; ALTER EXTENSION bdr UPDATE TO '0.10.0.4'; +ALTER EXTENSION bdr UPDATE TO '0.10.0.5'; -- Should never have to do anything: You missed adding the new version above. ALTER EXTENSION bdr UPDATE; -NOTICE: version "0.10.0.4" of extension "bdr" is already installed +NOTICE: version "0.10.0.5" of extension "bdr" is already installed \dx bdr List of installed extensions Name | Version | Schema | Description ------+----------+------------+------------------------------------------- - bdr | 0.10.0.4 | pg_catalog | Bi-directional replication for PostgreSQL + bdr | 0.10.0.5 | pg_catalog | Bi-directional replication for PostgreSQL (1 row) \c postgres diff --git a/extsql/bdr--0.10.0.4--0.10.0.5.sql b/extsql/bdr--0.10.0.4--0.10.0.5.sql new file mode 100644 index 0000000000..9be559e6f8 --- /dev/null +++ b/extsql/bdr--0.10.0.4--0.10.0.5.sql @@ -0,0 +1,273 @@ +SET LOCAL search_path = bdr; +SET bdr.permit_unsafe_ddl_commands = true; +SET bdr.skip_ddl_replication = true; + +CREATE FUNCTION bdr.bdr_internal_create_truncate_trigger(regclass) +RETURNS void LANGUAGE c AS 'MODULE_PATHNAME'; + +CREATE OR REPLACE FUNCTION bdr.bdr_group_create( + local_node_name text, + node_external_dsn text, + node_local_dsn text DEFAULT NULL, + apply_delay integer DEFAULT NULL, + replication_sets text[] DEFAULT ARRAY['default'] + ) +RETURNS void LANGUAGE plpgsql VOLATILE +SET search_path = bdr, pg_catalog +SET bdr.permit_unsafe_ddl_commands = on +SET bdr.skip_ddl_replication = on +SET bdr.skip_ddl_locking = on +AS $body$ +DECLARE + t record; +BEGIN + + -- Prohibit enabling BDR where exclusion constraints exist + FOR t IN + SELECT n.nspname, r.relname, c.conname, c.contype + FROM pg_constraint c + INNER JOIN pg_namespace n ON c.connamespace = n.oid + INNER JOIN pg_class r ON c.conrelid = r.oid + INNER JOIN LATERAL unnest(bdr.table_get_replication_sets(c.conrelid)) rs(rsname) ON (rs.rsname = ANY(replication_sets)) + WHERE c.contype = 'x' + AND r.relpersistence = 'p' + AND r.relkind = 'r' + AND n.nspname NOT IN ('pg_catalog', 'bdr', 'information_schema') + LOOP + RAISE USING + MESSAGE = 'BDR can''t be enabled because exclusion constraints exist on persistent tables that are not excluded from replication', + ERRCODE = 'object_not_in_prerequisite_state', + DETAIL = format('Table %I.%I has exclusion constraint %I', t.nspname, t.relname, t.conname), + HINT = 'Drop the exclusion constraint(s), change the table(s) to UNLOGGED if they don''t need to be replicated, or exclude the table(s) from the active replication set(s).'; + END LOOP; + + -- Warn users about secondary unique indexes + FOR t IN + SELECT n.nspname, r.relname, c.conname, c.contype + FROM pg_constraint c + INNER JOIN pg_namespace n ON c.connamespace = n.oid + INNER JOIN pg_class r ON c.conrelid = r.oid + INNER JOIN LATERAL unnest(bdr.table_get_replication_sets(c.conrelid)) rs(rsname) ON (rs.rsname = ANY(replication_sets)) + WHERE c.contype = 'u' + AND r.relpersistence = 'p' + AND r.relkind = 'r' + AND n.nspname NOT IN ('pg_catalog', 'bdr', 'information_schema') + LOOP + RAISE WARNING USING + MESSAGE = 'Secondary unique constraint(s) exist on replicated table(s)', + DETAIL = format('Table %I.%I has secondary unique constraint %I. This may cause unhandled replication conflicts.', t.nspname, t.relname, t.conname), + HINT = 'Drop the secondary unique constraint(s), change the table(s) to UNLOGGED if they don''t need to be replicated, or exclude the table(s) from the active replication set(s).'; + END LOOP; + + -- Warn users about missing primary keys + FOR t IN + SELECT n.nspname, r.relname, c.conname + FROM pg_class r INNER JOIN pg_namespace n ON r.relnamespace = n.oid + LEFT OUTER JOIN pg_constraint c ON (c.conrelid = r.oid AND c.contype = 'p') + WHERE n.nspname NOT IN ('pg_catalog', 'bdr', 'information_schema') + AND relkind = 'r' + AND relpersistence = 'p' + AND c.oid IS NULL + LOOP + RAISE WARNING USING + MESSAGE = 'Table %I.%I has no PRIMARY KEY', + HINT = 'Tables without a PRIMARY KEY cannot be UPDATEd or DELETEd from, only INSERTed into. Add a PRIMARY KEY.'; + END LOOP; + + -- Create ON TRUNCATE triggers for BDR on existing tables + -- See bdr_truncate_trigger_add for the matching event trigger for tables + -- created after join. + -- + -- The triggers may be created already because the bdr event trigger + -- runs when the bdr extension is created, even if there's no active + -- bdr connections yet, so tables created after the extension is created + -- will get the trigger already. So skip tables that have a tg named + -- 'truncate_trigger' calling proc 'bdr.queue_truncate'. + FOR t IN + SELECT r.oid AS relid + FROM pg_class r + INNER JOIN pg_namespace n ON (r.relnamespace = n.oid) + LEFT JOIN pg_trigger tg ON (r.oid = tg.tgrelid AND tgname = 'truncate_trigger') + LEFT JOIN pg_proc p ON (p.oid = tg.tgfoid AND p.proname = 'queue_truncate') + LEFT JOIN pg_namespace pn ON (pn.oid = p.pronamespace AND pn.nspname = 'bdr') + WHERE r.relpersistence = 'p' + AND r.relkind = 'r' + AND n.nspname NOT IN ('pg_catalog', 'bdr', 'information_schema') + AND tg.oid IS NULL AND p.oid IS NULL and pn.oid IS NULL + LOOP + -- We use a C function here because in addition to trigger creation + -- we must also mark it tgisinternal. + RAISE NOTICE 'Relation id is %', t.relid; + PERFORM bdr.bdr_internal_create_truncate_trigger(t.relid); + END LOOP; + + PERFORM bdr.bdr_group_join( + local_node_name := local_node_name, + node_external_dsn := node_external_dsn, + join_using_dsn := null, + node_local_dsn := node_local_dsn, + apply_delay := apply_delay, + replication_sets := replication_sets); +END; +$body$; + +-- Setup that's common to BDR and UDR joins +-- Add a check for bdr_test_replication_connection to the upstream, fixing #81 +CREATE OR REPLACE FUNCTION bdr.internal_begin_join( + caller text, local_node_name text, node_local_dsn text, remote_dsn text, + remote_sysid OUT text, remote_timeline OUT oid, remote_dboid OUT oid +) +RETURNS record LANGUAGE plpgsql VOLATILE +SET search_path = bdr, pg_catalog +SET bdr.permit_unsafe_ddl_commands = on +SET bdr.skip_ddl_replication = on +SET bdr.skip_ddl_locking = on +AS $body$ +DECLARE + localid RECORD; + localid_from_dsn RECORD; + remote_nodeinfo RECORD; + remote_nodeinfo_r RECORD; +BEGIN + -- Only one tx can be adding connections + LOCK TABLE bdr.bdr_connections IN EXCLUSIVE MODE; + LOCK TABLE bdr.bdr_nodes IN EXCLUSIVE MODE; + LOCK TABLE pg_catalog.pg_shseclabel IN EXCLUSIVE MODE; + + SELECT sysid, timeline, dboid INTO localid + FROM bdr.bdr_get_local_nodeid(); + + -- If there's already an entry for ourselves in bdr.bdr_connections + -- then we know this node is part of an active BDR group and cannot + -- be joined to another group. Unidirectional connections are ignored. + PERFORM 1 FROM bdr_connections + WHERE conn_sysid = localid.sysid + AND conn_timeline = localid.timeline + AND conn_dboid = localid.dboid + AND (conn_origin_sysid = '0' + AND conn_origin_timeline = 0 + AND conn_origin_dboid = 0) + AND conn_is_unidirectional = 'f'; + + IF FOUND THEN + RAISE USING + MESSAGE = 'This node is already a member of a BDR group', + HINT = 'Connect to the node you wish to add and run '||caller||' from it instead', + ERRCODE = 'object_not_in_prerequisite_state'; + END IF; + + -- Validate that the local connection is usable and matches + -- the node identity of the node we're running on. + -- + -- For BDR this will NOT check the 'dsn' if 'node_local_dsn' + -- gets supplied. We don't know if 'dsn' is even valid + -- for loopback connections and can't assume it is. That'll + -- get checked later by BDR specific code. + SELECT * INTO localid_from_dsn + FROM bdr_get_remote_nodeinfo(node_local_dsn); + + IF localid_from_dsn.sysid <> localid.sysid + OR localid_from_dsn.timeline <> localid.timeline + OR localid_from_dsn.dboid <> localid.dboid + THEN + RAISE USING + MESSAGE = 'node identity for local dsn does not match current node', + DETAIL = format($$The dsn '%s' connects to a node with identity (%s,%s,%s) but the local node is (%s,%s,%s)$$, + node_local_dsn, localid_from_dsn.sysid, localid_from_dsn.timeline, + localid_from_dsn.dboid, localid.sysid, localid.timeline, localid.dboid), + HINT = 'The node_local_dsn (or, for bdr, dsn if node_local_dsn is null) parameter must refer to the node you''re running this function from', + ERRCODE = 'object_not_in_prerequisite_state'; + END IF; + + IF NOT localid_from_dsn.is_superuser THEN + RAISE USING + MESSAGE = 'local dsn does not have superuser rights', + DETAIL = format($$The dsn '%s' connects successfully but does not grant superuser rights$$, node_local_dsn), + ERRCODE = 'object_not_in_prerequisite_state'; + END IF; + + -- Now interrogate the remote node, if specified, and sanity + -- check its connection too. The discovered node identity is + -- returned if found. + -- + -- This will error out if there are issues with the remote + -- node. + IF remote_dsn IS NOT NULL THEN + SELECT * INTO remote_nodeinfo + FROM bdr_get_remote_nodeinfo(remote_dsn); + + remote_sysid := remote_nodeinfo.sysid; + remote_timeline := remote_nodeinfo.timeline; + remote_dboid := remote_nodeinfo.dboid; + + IF NOT remote_nodeinfo.is_superuser THEN + RAISE USING + MESSAGE = 'connection to remote node does not have superuser rights', + DETAIL = format($$The dsn '%s' connects successfully but does not grant superuser rights$$, remote_dsn), + ERRCODE = 'object_not_in_prerequisite_state'; + END IF; + + IF remote_nodeinfo.version_num < bdr_min_remote_version_num() THEN + RAISE USING + MESSAGE = 'remote node''s BDR version is too old', + DETAIL = format($$The dsn '%s' connects successfully but the remote node version %s is less than the required version %s$$, + remote_dsn, remote_nodeinfo.version_num, bdr_min_remote_version_num()), + ERRCODE = 'object_not_in_prerequisite_state'; + END IF; + + IF remote_nodeinfo.min_remote_version_num > bdr_version_num() THEN + RAISE USING + MESSAGE = 'remote node''s BDR version is too new or this node''s version is too old', + DETAIL = format($$The dsn '%s' connects successfully but the remote node version %s requires this node to run at least bdr %s, not the current %s$$, + remote_dsn, remote_nodeinfo.version_num, remote_nodeinfo.min_remote_version_num, + bdr_min_remote_version_num()), + ERRCODE = 'object_not_in_prerequisite_state'; + + END IF; + + END IF; + + -- Verify that we can make a replication connection to the remote node + -- so that pg_hba.conf issues get caught early. + IF remote_dsn IS NOT NULL THEN + -- Returns (sysid, timeline, dboid) on success, else ERRORs + SELECT * FROM bdr_test_replication_connection(remote_dsn || ' replication=database') + INTO remote_nodeinfo_r; + + IF (remote_nodeinfo_r.sysid, remote_nodeinfo_r.timeline, remote_nodeinfo_r.dboid) + IS DISTINCT FROM + (remote_sysid, remote_timeline, remote_dboid) + THEN + -- This just shouldn't happen, so no fancy error + RAISE USING + MESSAGE = 'Replication and non-replication connections to remote node reported different node id'; + END IF; + END IF; + + -- Create local node record if needed + PERFORM 1 FROM bdr_nodes + WHERE node_sysid = localid.sysid + AND node_timeline = localid.timeline + AND node_dboid = localid.dboid; + + IF NOT FOUND THEN + INSERT INTO bdr_nodes ( + node_name, + node_sysid, node_timeline, node_dboid, + node_status, node_local_dsn, node_init_from_dsn + ) VALUES ( + local_node_name, + localid.sysid, localid.timeline, localid.dboid, + 'b', node_local_dsn, remote_dsn + ); + END IF; + + PERFORM bdr.internal_update_seclabel(); +END; +$body$; + + + +RESET bdr.permit_unsafe_ddl_commands; +RESET bdr.skip_ddl_replication; +RESET search_path; diff --git a/sql/upgrade.sql b/sql/upgrade.sql index 0d67523756..11b7cd180d 100644 --- a/sql/upgrade.sql +++ b/sql/upgrade.sql @@ -69,6 +69,9 @@ DROP EXTENSION bdr; CREATE EXTENSION bdr VERSION '0.10.0.4'; DROP EXTENSION bdr; +CREATE EXTENSION bdr VERSION '0.10.0.5'; +DROP EXTENSION bdr; + -- evolve version one by one from the oldest to the newest one CREATE EXTENSION bdr VERSION '0.8.0'; ALTER EXTENSION bdr UPDATE TO '0.8.0.1'; @@ -90,6 +93,7 @@ ALTER EXTENSION bdr UPDATE TO '0.10.0.1'; ALTER EXTENSION bdr UPDATE TO '0.10.0.2'; ALTER EXTENSION bdr UPDATE TO '0.10.0.3'; ALTER EXTENSION bdr UPDATE TO '0.10.0.4'; +ALTER EXTENSION bdr UPDATE TO '0.10.0.5'; -- Should never have to do anything: You missed adding the new version above. ALTER EXTENSION bdr UPDATE; -- 2.39.5