diff --git a/sql/ddl_experimental.sql b/sql/ddl_experimental.sql index d3243ea68..427b4ee91 100644 --- a/sql/ddl_experimental.sql +++ b/sql/ddl_experimental.sql @@ -35,6 +35,10 @@ CREATE OR REPLACE PROCEDURE timescaledb_experimental.copy_chunk( operation_id NAME = NULL) AS '@MODULE_PATHNAME@', 'ts_copy_chunk_proc' LANGUAGE C; +CREATE OR REPLACE FUNCTION timescaledb_experimental.subscription_exec( + subscription_command TEXT +) RETURNS VOID AS '@MODULE_PATHNAME@', 'ts_subscription_exec' LANGUAGE C VOLATILE; + -- A copy_chunk or move_chunk procedure call involves multiple nodes and -- depending on the data size can take a long time. Failures are possible -- when this long running activity is ongoing. We need to be able to recover diff --git a/sql/updates/latest-dev.sql b/sql/updates/latest-dev.sql index a87b14ae9..1388c6ae8 100644 --- a/sql/updates/latest-dev.sql +++ b/sql/updates/latest-dev.sql @@ -90,3 +90,7 @@ ALTER TABLE _timescaledb_catalog.continuous_agg DROP PROCEDURE IF EXISTS timescaledb_experimental.move_chunk(REGCLASS, NAME, NAME); DROP PROCEDURE IF EXISTS timescaledb_experimental.copy_chunk(REGCLASS, NAME, NAME); + +CREATE OR REPLACE FUNCTION timescaledb_experimental.subscription_exec( + subscription_command TEXT +) RETURNS VOID AS '@MODULE_PATHNAME@', 'ts_subscription_exec' LANGUAGE C VOLATILE; diff --git a/sql/updates/reverse-dev.sql b/sql/updates/reverse-dev.sql index 3425fff0c..984d330a8 100644 --- a/sql/updates/reverse-dev.sql +++ b/sql/updates/reverse-dev.sql @@ -105,3 +105,4 @@ ANALYZE _timescaledb_catalog.continuous_agg; DROP PROCEDURE timescaledb_experimental.move_chunk(REGCLASS, NAME, NAME, NAME); DROP PROCEDURE timescaledb_experimental.copy_chunk(REGCLASS, NAME, NAME, NAME); +DROP FUNCTION IF EXISTS timescaledb_experimental.subscription_exec(TEXT); diff --git a/src/cross_module_fn.c b/src/cross_module_fn.c index 884cda77f..3f452f09f 100644 --- a/src/cross_module_fn.c +++ b/src/cross_module_fn.c @@ -47,6 +47,7 @@ CROSSMODULE_WRAPPER(move_chunk); CROSSMODULE_WRAPPER(move_chunk_proc); CROSSMODULE_WRAPPER(copy_chunk_proc); CROSSMODULE_WRAPPER(copy_chunk_cleanup_proc); +CROSSMODULE_WRAPPER(subscription_exec); /* partialize/finalize aggregate */ CROSSMODULE_WRAPPER(partialize_agg); @@ -374,6 +375,7 @@ TSDLLEXPORT CrossModuleFunctions ts_cm_functions_default = { .move_chunk_proc = error_no_default_fn_pg_community, .copy_chunk_proc = error_no_default_fn_pg_community, .copy_chunk_cleanup_proc = error_no_default_fn_pg_community, + .subscription_exec = error_no_default_fn_pg_community, .reorder_chunk = error_no_default_fn_pg_community, .partialize_agg = error_no_default_fn_pg_community, diff --git a/src/cross_module_fn.h b/src/cross_module_fn.h index 29b52d1be..29bebed35 100644 --- a/src/cross_module_fn.h +++ b/src/cross_module_fn.h @@ -81,6 +81,7 @@ typedef struct CrossModuleFunctions PGFunction move_chunk; PGFunction move_chunk_proc; PGFunction copy_chunk_proc; + PGFunction subscription_exec; PGFunction copy_chunk_cleanup_proc; void (*ddl_command_start)(ProcessUtilityArgs *args); void (*ddl_command_end)(EventTriggerData *command); diff --git a/tsl/src/chunk_copy.c b/tsl/src/chunk_copy.c index 832cd5848..4d8c02add 100644 --- a/tsl/src/chunk_copy.c +++ b/tsl/src/chunk_copy.c @@ -5,6 +5,7 @@ */ #include #include +#include #include #include #include @@ -260,10 +261,11 @@ chunk_copy_setup(ChunkCopy *cc, Oid chunk_relid, const char *src_node, const cha Cache *hcache; MemoryContext old, mcxt; - if (!superuser()) + if (!superuser() && !has_rolreplication(GetUserId())) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), - (errmsg("must be superuser to copy/move chunk to data node")))); + (errmsg( + "must be superuser or replication role to copy/move chunk to data node")))); if (dist_util_membership() != DIST_MEMBER_ACCESS_NODE) ereport(ERROR, @@ -431,7 +433,7 @@ chunk_copy_stage_create_publication(ChunkCopy *cc) /* Create publication on the source data node */ cmd = psprintf("CREATE PUBLICATION %s FOR TABLE %s", - NameStr(cc->fd.operation_id), + quote_identifier(NameStr(cc->fd.operation_id)), quote_qualified_identifier(NameStr(cc->chunk->fd.schema_name), NameStr(cc->chunk->fd.table_name))); @@ -449,7 +451,7 @@ chunk_copy_stage_create_replication_slot(ChunkCopy *cc) * create the replication slot separately before creating the subscription */ cmd = psprintf("SELECT pg_create_logical_replication_slot('%s', 'pgoutput')", - NameStr(cc->fd.operation_id)); + quote_identifier(NameStr(cc->fd.operation_id))); ts_dist_cmd_run_on_data_nodes(cmd, list_make1(NameStr(cc->fd.source_node_name)), true); } @@ -463,7 +465,7 @@ chunk_copy_stage_create_replication_slot_cleanup(ChunkCopy *cc) /* Check if the slot exists on the source data node */ cmd = psprintf("SELECT 1 FROM pg_catalog.pg_replication_slots WHERE slot_name = '%s'", - NameStr(cc->fd.operation_id)); + quote_identifier(NameStr(cc->fd.operation_id))); dist_res = ts_dist_cmd_invoke_on_data_nodes(cmd, list_make1(NameStr(cc->fd.source_node_name)), true); res = ts_dist_cmd_get_result_by_node_name(dist_res, NameStr(cc->fd.source_node_name)); @@ -475,7 +477,8 @@ chunk_copy_stage_create_replication_slot_cleanup(ChunkCopy *cc) /* Drop replication slot on the source data node only if it exists */ if (PQntuples(res) != 0) { - cmd = psprintf("SELECT pg_drop_replication_slot('%s')", NameStr(cc->fd.operation_id)); + cmd = psprintf("SELECT pg_drop_replication_slot('%s')", + quote_identifier(NameStr(cc->fd.operation_id))); ts_dist_cmd_run_on_data_nodes(cmd, list_make1(NameStr(cc->fd.source_node_name)), true); } @@ -498,7 +501,7 @@ chunk_copy_stage_create_publication_cleanup(ChunkCopy *cc) /* Check if the publication exists on the source data node */ cmd = psprintf("SELECT 1 FROM pg_catalog.pg_publication WHERE pubname = '%s'", - NameStr(cc->fd.operation_id)); + quote_identifier(NameStr(cc->fd.operation_id))); dist_res = ts_dist_cmd_invoke_on_data_nodes(cmd, list_make1(NameStr(cc->fd.source_node_name)), true); res = ts_dist_cmd_get_result_by_node_name(dist_res, NameStr(cc->fd.source_node_name)); @@ -510,7 +513,7 @@ chunk_copy_stage_create_publication_cleanup(ChunkCopy *cc) /* Drop publication on the source node only if it exists */ if (PQntuples(res) != 0) { - cmd = psprintf("DROP PUBLICATION %s", NameStr(cc->fd.operation_id)); + cmd = psprintf("DROP PUBLICATION %s", quote_identifier(NameStr(cc->fd.operation_id))); /* Drop the publication */ ts_dist_cmd_run_on_data_nodes(cmd, list_make1(NameStr(cc->fd.source_node_name)), true); @@ -519,10 +522,21 @@ chunk_copy_stage_create_publication_cleanup(ChunkCopy *cc) ts_dist_cmd_close_response(dist_res); } +/* Execute a logical SUBSCRIPTION related command on the data node */ +static void +chunk_copy_exec_subscription_command(const char *command, List *data_nodes) +{ + char *cmd; + + cmd = psprintf("SELECT timescaledb_experimental.subscription_exec($sql$%s$sql$)", command); + ts_dist_cmd_run_on_data_nodes(cmd, data_nodes, true); + pfree(cmd); +} + static void chunk_copy_stage_create_subscription(ChunkCopy *cc) { - const char *cmd; + char *cmd; const char *connection_string; /* Prepare connection string to the source node */ @@ -530,10 +544,11 @@ chunk_copy_stage_create_subscription(ChunkCopy *cc) cmd = psprintf("CREATE SUBSCRIPTION %s CONNECTION '%s' PUBLICATION %s" " WITH (create_slot = false, enabled = false)", - NameStr(cc->fd.operation_id), + quote_identifier(NameStr(cc->fd.operation_id)), connection_string, - NameStr(cc->fd.operation_id)); - ts_dist_cmd_run_on_data_nodes(cmd, list_make1(NameStr(cc->fd.dest_node_name)), true); + quote_identifier(NameStr(cc->fd.operation_id))); + chunk_copy_exec_subscription_command(cmd, list_make1(NameStr(cc->fd.dest_node_name))); + pfree(cmd); } static void @@ -545,7 +560,7 @@ chunk_copy_stage_create_subscription_cleanup(ChunkCopy *cc) /* Check if the subscription exists on the destination data node */ cmd = psprintf("SELECT 1 FROM pg_catalog.pg_subscription WHERE subname = '%s'", - NameStr(cc->fd.operation_id)); + quote_identifier(NameStr(cc->fd.operation_id))); dist_res = ts_dist_cmd_invoke_on_data_nodes(cmd, list_make1(NameStr(cc->fd.dest_node_name)), true); res = ts_dist_cmd_get_result_by_node_name(dist_res, NameStr(cc->fd.dest_node_name)); @@ -557,17 +572,24 @@ chunk_copy_stage_create_subscription_cleanup(ChunkCopy *cc) /* Cleanup only if the subscription exists */ if (PQntuples(res) != 0) { - List *nodes = list_make1(NameStr(cc->fd.dest_node_name)); + List *dest_dn_list = list_make1(NameStr(cc->fd.dest_node_name)); + + /* Stop data transfer on the destination node */ + cmd = psprintf("ALTER SUBSCRIPTION %s DISABLE", + quote_identifier(NameStr(cc->fd.operation_id))); + chunk_copy_exec_subscription_command(cmd, dest_dn_list); + pfree(cmd); /* Disassociate the subscription from the replication slot first */ - cmd = - psprintf("ALTER SUBSCRIPTION %s SET (slot_name = NONE)", NameStr(cc->fd.operation_id)); - ts_dist_cmd_run_on_data_nodes(cmd, nodes, true); + cmd = psprintf("ALTER SUBSCRIPTION %s SET (slot_name = NONE)", + quote_identifier(NameStr(cc->fd.operation_id))); + chunk_copy_exec_subscription_command(cmd, dest_dn_list); + pfree(cmd); /* Drop the subscription now */ + cmd = psprintf("DROP SUBSCRIPTION %s", quote_identifier(NameStr(cc->fd.operation_id))); + chunk_copy_exec_subscription_command(cmd, dest_dn_list); pfree(cmd); - cmd = psprintf("DROP SUBSCRIPTION %s", NameStr(cc->fd.operation_id)); - ts_dist_cmd_run_on_data_nodes(cmd, nodes, true); } ts_dist_cmd_close_response(dist_res); @@ -576,11 +598,12 @@ chunk_copy_stage_create_subscription_cleanup(ChunkCopy *cc) static void chunk_copy_stage_sync_start(ChunkCopy *cc) { - const char *cmd; + char *cmd; /* Start data transfer on the destination node */ - cmd = psprintf("ALTER SUBSCRIPTION %s ENABLE", NameStr(cc->fd.operation_id)); - ts_dist_cmd_run_on_data_nodes(cmd, list_make1(NameStr(cc->fd.dest_node_name)), true); + cmd = psprintf("ALTER SUBSCRIPTION %s ENABLE", quote_identifier(NameStr(cc->fd.operation_id))); + chunk_copy_exec_subscription_command(cmd, list_make1(NameStr(cc->fd.dest_node_name))); + pfree(cmd); } static void @@ -592,7 +615,7 @@ chunk_copy_stage_sync_start_cleanup(ChunkCopy *cc) /* Check if the subscription exists on the destination data node */ cmd = psprintf("SELECT 1 FROM pg_catalog.pg_subscription WHERE subname = '%s'", - NameStr(cc->fd.operation_id)); + quote_identifier(NameStr(cc->fd.operation_id))); dist_res = ts_dist_cmd_invoke_on_data_nodes(cmd, list_make1(NameStr(cc->fd.dest_node_name)), true); res = ts_dist_cmd_get_result_by_node_name(dist_res, NameStr(cc->fd.dest_node_name)); @@ -605,7 +628,8 @@ chunk_copy_stage_sync_start_cleanup(ChunkCopy *cc) if (PQntuples(res) != 0) { /* Stop data transfer on the destination node */ - cmd = psprintf("ALTER SUBSCRIPTION %s DISABLE", NameStr(cc->fd.operation_id)); + cmd = psprintf("ALTER SUBSCRIPTION %s DISABLE", + quote_identifier(NameStr(cc->fd.operation_id))); ts_dist_cmd_run_on_data_nodes(cmd, list_make1(NameStr(cc->fd.dest_node_name)), true); } @@ -640,20 +664,22 @@ static void chunk_copy_stage_drop_subscription(ChunkCopy *cc) { char *cmd; + List *dest_dn_list = list_make1(NameStr(cc->fd.dest_node_name)); /* Stop data transfer on the destination node */ - cmd = psprintf("ALTER SUBSCRIPTION %s DISABLE", NameStr(cc->fd.operation_id)); - ts_dist_cmd_run_on_data_nodes(cmd, list_make1(NameStr(cc->fd.dest_node_name)), true); + cmd = psprintf("ALTER SUBSCRIPTION %s DISABLE", quote_identifier(NameStr(cc->fd.operation_id))); + chunk_copy_exec_subscription_command(cmd, dest_dn_list); pfree(cmd); /* Disassociate the subscription from the replication slot first */ - cmd = psprintf("ALTER SUBSCRIPTION %s SET (slot_name = NONE)", NameStr(cc->fd.operation_id)); - ts_dist_cmd_run_on_data_nodes(cmd, list_make1(NameStr(cc->fd.dest_node_name)), true); + cmd = psprintf("ALTER SUBSCRIPTION %s SET (slot_name = NONE)", + quote_identifier(NameStr(cc->fd.operation_id))); + chunk_copy_exec_subscription_command(cmd, dest_dn_list); pfree(cmd); /* Drop the subscription now */ - cmd = psprintf("DROP SUBSCRIPTION %s", NameStr(cc->fd.operation_id)); - ts_dist_cmd_run_on_data_nodes(cmd, list_make1(NameStr(cc->fd.dest_node_name)), true); + cmd = psprintf("DROP SUBSCRIPTION %s", quote_identifier(NameStr(cc->fd.operation_id))); + chunk_copy_exec_subscription_command(cmd, dest_dn_list); pfree(cmd); } @@ -662,10 +688,11 @@ chunk_copy_stage_drop_publication(ChunkCopy *cc) { char *cmd; - cmd = psprintf("SELECT pg_drop_replication_slot('%s')", NameStr(cc->fd.operation_id)); + cmd = psprintf("SELECT pg_drop_replication_slot('%s')", + quote_identifier(NameStr(cc->fd.operation_id))); ts_dist_cmd_run_on_data_nodes(cmd, list_make1(NameStr(cc->fd.source_node_name)), true); - cmd = psprintf("DROP PUBLICATION %s", NameStr(cc->fd.operation_id)); + cmd = psprintf("DROP PUBLICATION %s", quote_identifier(NameStr(cc->fd.operation_id))); ts_dist_cmd_run_on_data_nodes(cmd, list_make1(NameStr(cc->fd.source_node_name)), true); } @@ -948,10 +975,11 @@ chunk_copy_cleanup(const char *operation_id) bool found = false; int stage_idx; - if (!superuser()) + if (!superuser() && !has_rolreplication(GetUserId())) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), - (errmsg("must be superuser to cleanup a chunk copy operation")))); + (errmsg( + "must be superuser or replication role to cleanup a chunk copy operation")))); if (dist_util_membership() != DIST_MEMBER_ACCESS_NODE) ereport(ERROR, diff --git a/tsl/src/init.c b/tsl/src/init.c index 13b8e9248..9bdaa74ca 100644 --- a/tsl/src/init.c +++ b/tsl/src/init.c @@ -127,6 +127,7 @@ CrossModuleFunctions tsl_cm_functions = { .move_chunk_proc = tsl_move_chunk_proc, .copy_chunk_proc = tsl_copy_chunk_proc, .copy_chunk_cleanup_proc = tsl_copy_chunk_cleanup_proc, + .subscription_exec = tsl_subscription_exec, /* Continuous Aggregates */ .partialize_agg = tsl_partialize_agg, diff --git a/tsl/src/reorder.c b/tsl/src/reorder.c index 654297217..6e6f29122 100644 --- a/tsl/src/reorder.c +++ b/tsl/src/reorder.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -40,7 +41,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -259,6 +262,80 @@ tsl_copy_chunk_proc(PG_FUNCTION_ARGS) PG_RETURN_VOID(); } +Datum +tsl_subscription_exec(PG_FUNCTION_ARGS) +{ + Oid save_userid; + int save_sec_context; + const char *subscription_cmd = PG_ARGISNULL(0) ? NULL : text_to_cstring(PG_GETARG_TEXT_P(0)); + int res; + List *parsetree_list; + ListCell *parsetree_item; + + /* + * Subscription command needs a superuser + * so switch to that context. But first check that the passed in user has atleast + * REPLICATION privileges to justify the use of this function + */ + if (!superuser() && !has_rolreplication(GetUserId())) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + (errmsg("must be superuser or replication role to use this function")))); + + GetUserIdAndSecContext(&save_userid, &save_sec_context); + SetUserIdAndSecContext(BOOTSTRAP_SUPERUSERID, save_sec_context | SECURITY_LOCAL_USERID_CHANGE); + + /* + * Parse the SQL string into a list of raw parse trees. + */ + parsetree_list = pg_parse_query(subscription_cmd); + + /* + * Check that we have received a "SUBSCRIPTION" related command only. Anything else + * needs to error out + */ + foreach (parsetree_item, parsetree_list) + { + RawStmt *parsetree = lfirst_node(RawStmt, parsetree_item); + + /* We are only interested in "CREATE/DROP SUBSCRIPTION" and "ALTER SUBSCRIPTION" stmts */ + switch (nodeTag(parsetree->stmt)) + { + case T_CreateSubscriptionStmt: + break; + + case T_AlterSubscriptionStmt: + break; + + case T_DropSubscriptionStmt: + break; + + default: + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("this function only accepts SUBSCRIPTION commands"))); + } + } + + if (SPI_connect() != SPI_OK_CONNECT) + elog(ERROR, "could not connect to SPI"); + + res = SPI_execute(subscription_cmd, false /* read_only */, 0 /*count*/); + + if (res < 0) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + (errmsg("error in subscription cmd \"%s\"", subscription_cmd)))); + + res = SPI_finish(); + Assert(res == SPI_OK_FINISH); + + /* Restore the earlier user */ + SetUserIdAndSecContext(save_userid, save_sec_context); + + PG_RETURN_VOID(); +} + Datum tsl_copy_chunk_cleanup_proc(PG_FUNCTION_ARGS) { diff --git a/tsl/src/reorder.h b/tsl/src/reorder.h index 09c4a3bf3..597099577 100644 --- a/tsl/src/reorder.h +++ b/tsl/src/reorder.h @@ -14,6 +14,7 @@ extern Datum tsl_move_chunk(PG_FUNCTION_ARGS); extern Datum tsl_move_chunk_proc(PG_FUNCTION_ARGS); extern Datum tsl_copy_chunk_proc(PG_FUNCTION_ARGS); extern Datum tsl_copy_chunk_cleanup_proc(PG_FUNCTION_ARGS); +extern Datum tsl_subscription_exec(PG_FUNCTION_ARGS); extern void reorder_chunk(Oid chunk_id, Oid index_id, bool verbose, Oid wait_id, Oid destination_tablespace, Oid index_tablespace); diff --git a/tsl/test/expected/dist_move_chunk.out b/tsl/test/expected/dist_move_chunk.out index ef55cbaee..4c3206529 100644 --- a/tsl/test/expected/dist_move_chunk.out +++ b/tsl/test/expected/dist_move_chunk.out @@ -133,7 +133,7 @@ ROLLBACK; SET ROLE :ROLE_DEFAULT_PERM_USER; \set ON_ERROR_STOP 0 CALL timescaledb_experimental.move_chunk(chunk=>'_timescaledb_internal._dist_hyper_1_1_chunk', source_node=> 'data_node_1', destination_node => 'data_node_2'); -ERROR: must be superuser to copy/move chunk to data node +ERROR: must be superuser or replication role to copy/move chunk to data node \set ON_ERROR_STOP 1 SET ROLE :ROLE_1; -- can't run copy/move chunk on a data node diff --git a/tsl/test/shared/expected/extension.out b/tsl/test/shared/expected/extension.out index 755f45046..bf231621c 100644 --- a/tsl/test/shared/expected/extension.out +++ b/tsl/test/shared/expected/extension.out @@ -209,6 +209,7 @@ ORDER BY pronamespace::regnamespace::text COLLATE "C", p.oid::regprocedure::text timescaledb_experimental.cleanup_copy_chunk_operation(name) timescaledb_experimental.copy_chunk(regclass,name,name,name) timescaledb_experimental.move_chunk(regclass,name,name,name) + timescaledb_experimental.subscription_exec(text) timescaledb_experimental.time_bucket_ng(interval,date) timescaledb_experimental.time_bucket_ng(interval,date,date) timescaledb_experimental.time_bucket_ng(interval,timestamp with time zone) diff --git a/tsl/test/t/002_chunk_copy_move.pl b/tsl/test/t/002_chunk_copy_move.pl index 2ad76d936..2a00d9d99 100644 --- a/tsl/test/t/002_chunk_copy_move.pl +++ b/tsl/test/t/002_chunk_copy_move.pl @@ -8,7 +8,7 @@ use warnings; use AccessNode; use DataNode; use TestLib; -use Test::More tests => 272; +use Test::More tests => 274; #Initialize all the multi-node instances my $an = AccessNode->create('an'); @@ -83,9 +83,45 @@ while ($curr_index < $arrSize) $curr_index++; } +for my $node ($an, $dn1, $dn2) +{ + $node->safe_psql('postgres', "CREATE ROLE testrole LOGIN"); +} + +#Error out the move if user doesn't have superuser nor replication perms +($ret, $stdout, $stderr) = $an->psql('postgres', + "SET ROLE testrole; CALL timescaledb_experimental.move_chunk(chunk=>'_timescaledb_internal._dist_hyper_1_1_chunk', source_node=> 'dn1', destination_node => 'dn2')" +); + +like( + $stderr, + qr/must be superuser or replication role to copy\/move chunk to data node/, + 'Expected failure due to no credentials'); + +#Provide REPLICATION creds to this user now +for my $node ($an, $dn1, $dn2) +{ + $node->safe_psql('postgres', "ALTER ROLE testrole REPLICATION;"); +} + +#Check that function errors out if any non SUBSCRIPTON command is passed to it +($ret, $stdout, $stderr) = $an->psql('postgres', + "SET ROLE testrole; SELECT timescaledb_experimental.subscription_exec('DROP ROLE testrole')" +); + +like( + $stderr, + qr/this function only accepts SUBSCRIPTION commands/, + 'Expected failure due to wrong command to function'); + +#Also grant it ownership of the table and related permissions +$an->safe_psql('postgres', + "ALTER TABLE test OWNER TO testrole; GRANT ALL PRIVILEGES ON DATABASE postgres TO testrole; GRANT USAGE, SELECT ON SEQUENCE _timescaledb_catalog.chunk_copy_operation_id_seq TO testrole; GRANT USAGE ON FOREIGN SERVER dn1, dn2 TO testrole;" +); + #Move chunk _timescaledb_internal._dist_hyper_1_1_chunk to DN2 from AN $an->safe_psql('postgres', - "CALL timescaledb_experimental.move_chunk(chunk=>'_timescaledb_internal._dist_hyper_1_1_chunk', source_node=> 'dn1', destination_node => 'dn2')" + "SET ROLE testrole; CALL timescaledb_experimental.move_chunk(chunk=>'_timescaledb_internal._dist_hyper_1_1_chunk', source_node=> 'dn1', destination_node => 'dn2')" ); #Query datanode1 after the above move