Possible solution for masking chosen columns when using pg_dump

Started by Олег Целебровскийover 3 years ago6 messages
#1Олег Целебровский
oleg_tselebrovskiy@mail.ru
1 attachment(s)

Hello, here's my take on masking data when using pg_dump
 
The main idea is using PostgreSQL functions to replace data during a SELECT.
When table data is dumped SELECT a,b,c,d ... from ... query is generated, the columns that are marked for masking are replaced with result of functions on those columns
Example: columns name, count are to be masked, so the query will look as such: SELECT id, mask_text(name), mask_int(count), date from ...
 
So about the interface: I added 2 more command-line options: 
 
--mask-columns, which specifies what columns from what tables will be masked 
    usage example:
            --mask-columns "t1.name, t2.description" - both columns will be masked with the same corresponding function
            or --mask-columns name - ALL columns with name "name" from all dumped tables will be masked with correspoding function
 
--mask-function, which specifies what functions will mask data
    usage example:
            --mask-function mask_int - corresponding columns will be masked with function named "mask_int" from default schema (public)
            or --mask-function my_schema.mask_varchar - same as above but with specified schema where the function is stored
            or --mask-function somedir/filename - the function is "defined" here - more on the structure below
 
Structure of the file with function description:
 
First row - function name (with or without schema name)
Second row - type of in and out value (the design is to only work with same input/output type so no int-to-text shenanigans)
Third row - language of function
Forth and later rows - body of a function
 
Example of such file:
 
mask_text
text
plpgsql
res := '***';
 
First iteration of using file-described functions used just plain SQL query, but since it executed during read-write connection, some things such as writing "DROP TABLE t1;" after the CREATE FUNCTION ...; were possible.
Now even if something harmful is written in function body, it will be executed during dump-read-only connection, where it will just throw an error
 
About "corresponding columns and functions" - masking functions and columns are paired with eachother based on the input order, but --masking-columns and --masking-functions don't have to be subsequent.
Example: pg_dump -t table_name --mask-columns name --mask-colums count --mask-function mask_text --mask-function mask_int - here 'name' will be paired with function 'mask_text' and 'count' with 'mask_int' 
 
Patch includes regression tests
 
I'm open to discussion of this patch
 
Best regards,
 
Oleg Tselebrovskiy

Attachments:

masking_for_pg_dump_v1.patchtext/x-diff; name="=?UTF-8?B?bWFza2luZ19mb3JfcGdfZHVtcF92MS5wYXRjaA==?="Download
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index bd9b066e4e..457290064d 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -97,6 +97,14 @@ typedef enum OidOptions
 	zeroAsNone = 4
 } OidOptions;
 
+typedef struct
+{
+	char* column; 	/* name of masked column */
+	char* table;	/* name of table where masked column is stored */
+	char* func;		/* name of masking function */
+	char* schema;	/* name of schema where masking function is stored */
+} MaskColumnInfo;
+
 /* global decls */
 static bool dosync = true;		/* Issue fsync() to make dump durable on disk. */
 
@@ -105,6 +113,14 @@ static Oid	g_last_builtin_oid; /* value of the last builtin oid */
 /* The specified names/patterns should to match at least one entity */
 static int	strict_names = 0;
 
+/*
+* mask_column_info_list contains info about every to-be-masked column:
+* its name, a name its table (if nothing is specified - mask all columns with this name),
+* name of masking function and name of schema containing this function (public if not specified)
+*/
+
+static SimplePtrList mask_column_info_list = {NULL, NULL};
+
 /*
  * Object inclusion/exclusion lists
  *
@@ -160,6 +176,8 @@ static int	nseclabels = 0;
 				   (obj)->dobj.name)
 
 static void help(const char *progname);
+static void addFuncToDatabase(MaskColumnInfo* cur_mask_column_info, 
+							 FILE* mask_func_file, DumpOptions* dopt);
 static void setup_connection(Archive *AH,
 							 const char *dumpencoding, const char *dumpsnapshot,
 							 char *use_role);
@@ -184,6 +202,8 @@ static void prohibit_crossdb_refs(PGconn *conn, const char *dbname,
 
 static NamespaceInfo *findNamespace(Oid nsoid);
 static void dumpTableData(Archive *fout, const TableDataInfo *tdinfo);
+static void maskColumns(TableInfo *tbinfo, char* current_column_name,
+						PQExpBuffer* q, SimpleStringList* column_names);
 static void refreshMatViewData(Archive *fout, const TableDataInfo *tdinfo);
 static const char *getRoleName(const char *roleoid_str);
 static void collectRoleNames(Archive *fout);
@@ -342,6 +362,19 @@ main(int argc, char **argv)
 	int			numWorkers = 1;
 	int			compressLevel = -1;
 	int			plainText = 0;
+
+	/* needed for masking */
+	SimpleStringList mask_columns_list = {NULL, NULL};
+	SimpleStringList mask_func_list = {NULL, NULL};
+	SimpleStringListCell *mask_func_cell;
+	SimpleStringListCell *mask_columns_cell;
+	SimplePtrListCell	 *mask_column_info_cell;
+	char		*column_name_buffer;
+	char		*table_name_buffer;
+	char		*func_name_buffer;
+	char		*schema_name_buffer;
+	FILE 		*mask_func_file;
+
 	ArchiveFormat archiveFormat = archUnknown;
 	ArchiveMode archiveMode;
 
@@ -413,6 +446,8 @@ main(int argc, char **argv)
 		{"on-conflict-do-nothing", no_argument, &dopt.do_nothing, 1},
 		{"rows-per-insert", required_argument, NULL, 10},
 		{"include-foreign-data", required_argument, NULL, 11},
+		{"mask-columns", required_argument, NULL, 12},
+		{"mask-function", required_argument, NULL, 13},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -623,6 +658,14 @@ main(int argc, char **argv)
 										  optarg);
 				break;
 
+			case 12:			/* columns for masking */
+				simple_string_list_append(&mask_columns_list, optarg);
+				break;
+
+			case 13:			/* function for masking - can be SQL function from .sql file,
+								   declared in CLI or declared in DB*/
+				simple_string_list_append(&mask_func_list, optarg);
+				break;
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -658,6 +701,101 @@ main(int argc, char **argv)
 	if (dopt.binary_upgrade)
 		dopt.sequence_data = 1;
 
+	/*
+	* Add all columns and funcions to list of MaskColumnInfo structures,
+	*/
+
+	mask_func_cell = mask_func_list.head;
+	mask_columns_cell = mask_columns_list.head;
+
+	while (mask_columns_cell && mask_func_cell)
+	{
+		char* func = mask_func_cell->val;
+		char* column = strtok(mask_columns_cell->val, " ,\'\"");
+		char* table = (char*) pg_malloc(256 * sizeof(char));
+		char* schema = (char*) pg_malloc(256 * sizeof(char));
+		while (column != NULL)
+		{
+			MaskColumnInfo* new_mask_column = (MaskColumnInfo*) pg_malloc(sizeof(MaskColumnInfo));
+			new_mask_column->column = column;
+			new_mask_column->table = table;
+			new_mask_column->func = func;
+			new_mask_column->schema = schema;
+			simple_ptr_list_append(&mask_column_info_list, new_mask_column);
+			table = (char*) pg_malloc(256 * sizeof(char));
+			column = strtok(NULL, " ,\'\"");
+		}
+		mask_columns_cell = mask_columns_cell->next;
+		mask_func_cell = mask_func_cell->next;
+	}
+
+	/*
+	* If there is not enough params of one type throw error
+	*/
+
+	if (mask_columns_cell != NULL || mask_func_cell != NULL)
+		pg_fatal("amount of --mask-columns and --mask-function doesn't match");
+
+	/*
+	* Extract tablenames from list of columns - done here so that strtok isn't
+	* disturbed in previous cycle
+	*/
+
+	mask_column_info_cell = mask_column_info_list.head;
+
+	while (mask_column_info_cell != NULL)
+	{
+		MaskColumnInfo* cur_mask_column_info = (MaskColumnInfo*) mask_column_info_cell->ptr;
+		table_name_buffer = strtok(cur_mask_column_info->column, ".");
+		column_name_buffer = strtok(NULL, ".");
+		if (column_name_buffer == NULL) /* found column without tablename */
+		{
+			strcpy(cur_mask_column_info->table, "");
+			strcpy(cur_mask_column_info->column, table_name_buffer);
+		}
+		else
+		{
+			strcpy(cur_mask_column_info->table, table_name_buffer);
+			strcpy(cur_mask_column_info->column, column_name_buffer);
+		}
+		mask_column_info_cell = mask_column_info_cell->next;
+	}
+
+	/*
+	* Check if --mask-function is a name of function or a filepath
+	* if filepath - open file, start connection, execute script, close connection
+	*/
+
+	mask_column_info_cell = mask_column_info_list.head;
+
+	while (mask_column_info_cell != NULL)
+	{
+		MaskColumnInfo* cur_mask_column_info = (MaskColumnInfo*) mask_column_info_cell->ptr;
+		func_name_buffer = pg_strdup(cur_mask_column_info->func);
+		canonicalize_path(func_name_buffer);
+		mask_func_file = fopen(func_name_buffer, "r");
+		if (mask_func_file != NULL) /* then it is a file with function*/
+		{
+			addFuncToDatabase(cur_mask_column_info, mask_func_file, &dopt);
+		}
+		else /* function stored in DB*/
+		{
+			schema_name_buffer = strtok(cur_mask_column_info->func, ".");
+			func_name_buffer = strtok(NULL, ".");
+			if (func_name_buffer == NULL) /* found function without schemaname */
+			{
+				strcpy(cur_mask_column_info->schema, "public");
+				strcpy(cur_mask_column_info->func, schema_name_buffer);
+			}
+			else
+			{
+				strcpy(cur_mask_column_info->schema, schema_name_buffer);
+				strcpy(cur_mask_column_info->func, func_name_buffer);
+			}
+		}
+		mask_column_info_cell = mask_column_info_cell->next;
+	}
+
 	if (dopt.dataOnly && dopt.schemaOnly)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
 
@@ -1034,6 +1172,11 @@ help(const char *progname)
 			 "                               servers matching PATTERN\n"));
 	printf(_("  --inserts                    dump data as INSERT commands, rather than COPY\n"));
 	printf(_("  --load-via-partition-root    load partitions via the root table\n"));
+	printf(_("  --mask-columns               names of columns that will be masked \n"
+			 "                               if table name is not specified, mask in all tables\n"));
+	printf(_("  --mask-function              name of function that will mask corresponding columns\n"
+			 "                               can specify schema in which function is stored\n"
+			 "								 can use filepath to file with function arguments\n"));
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
@@ -1069,6 +1212,77 @@ help(const char *progname)
 	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
 }
 
+/*
+* addFuncToDatabase - parses file specified in command line, executes query from it
+* adding masking function to database
+*/
+
+//TODO - поменять структуру файла, и сделать составление запроса внутри кода
+//структура файла - func_name\n argument_type\n func_language\n function_body\n
+
+static void
+addFuncToDatabase(MaskColumnInfo* cur_mask_column_info, FILE* mask_func_file, DumpOptions* dopt)
+{
+	PGconn *connection;
+	PQExpBufferData query;
+	char* conn_params = (char*) pg_malloc(256 * sizeof(char));
+	char* common_buff = (char*) pg_malloc(64 * sizeof(char));
+	char* func_name_buff = (char*) pg_malloc(64 * sizeof(char));
+	char* argument_type_buff = (char*) pg_malloc(64 * sizeof(char));
+	char* func_language_buff = (char*) pg_malloc(64 * sizeof(char));
+	char* func_body_buff = (char*) pg_malloc(512 * sizeof(char));
+	char* schema_name_buff = (char*) pg_malloc(64 * sizeof(char));
+
+	fgets(common_buff, 64, mask_func_file);
+	func_name_buff = strdup(strtok(common_buff, " ,\n\t"));
+	fgets(common_buff, 64, mask_func_file);
+	argument_type_buff = strdup(strtok(common_buff, " .,\n\t"));
+	fgets(common_buff, 64, mask_func_file);
+	func_language_buff = strdup(strtok(common_buff, " ,\n\t"));
+	free(common_buff);
+	common_buff = (char*) pg_malloc(512 * sizeof(char));
+	while(fgets(common_buff, 512, mask_func_file))
+	{
+		func_body_buff = psprintf("%s%s", func_body_buff, common_buff);
+	}
+	
+	initPQExpBuffer(&query);
+	appendPQExpBuffer(&query, "CREATE OR REPLACE FUNCTION %s (IN elem %s, OUT res %s) RETURNS %s AS $BODY$ \nBEGIN\n%s\nRETURN;\nEND\n$BODY$ LANGUAGE %s;",
+	 						func_name_buff, argument_type_buff, argument_type_buff, argument_type_buff,
+							func_body_buff, func_language_buff);
+
+	/* Establishing connection to execute CREATE FUNCTION script */
+
+	strcpy(conn_params, "");
+	if(dopt->cparams.override_dbname)
+		conn_params = psprintf("%s dbname=%s", conn_params, dopt->cparams.override_dbname);
+	else
+		if(dopt->cparams.dbname)
+			conn_params = psprintf("%s dbname=%s", conn_params, dopt->cparams.dbname);
+	if(dopt->cparams.pghost)
+		conn_params = psprintf("%s host=%s", conn_params, dopt->cparams.pghost);
+	if(dopt->cparams.pgport)
+		conn_params = psprintf("%s port=%s", conn_params, dopt->cparams.pgport);
+	if(dopt->cparams.username)
+		conn_params = psprintf("%s user=%s", conn_params, dopt->cparams.username);
+	connection = PQconnectdb(conn_params);
+	PQexec(connection, query.data);
+	PQfinish(connection);
+	schema_name_buff = strtok(pg_strdup(func_name_buff), ".");
+	func_name_buff = strtok(NULL, ".");
+	if (func_name_buff == NULL) /* found function without schemaname */
+	{
+		strcpy(cur_mask_column_info->schema, "public");
+		strcpy(cur_mask_column_info->func, schema_name_buff);
+	}
+	else
+	{
+		strcpy(cur_mask_column_info->schema, schema_name_buff);
+		strcpy(cur_mask_column_info->func, func_name_buff);
+	}
+	free(conn_params);
+}
+
 static void
 setup_connection(Archive *AH, const char *dumpencoding,
 				 const char *dumpsnapshot, char *use_role)
@@ -1977,6 +2191,7 @@ dumpTableData_copy(Archive *fout, const void *dcontext)
 	int			ret;
 	char	   *copybuf;
 	const char *column_list;
+	char	   *temp_string = (char*)malloc(256 * sizeof(char));
 
 	pg_log_info("dumping contents of table \"%s.%s\"",
 				tbinfo->dobj.namespace->dobj.name, classname);
@@ -1991,20 +2206,31 @@ dumpTableData_copy(Archive *fout, const void *dcontext)
 
 	/*
 	 * Use COPY (SELECT ...) TO when dumping a foreign table's data, and when
-	 * a filter condition was specified.  For other cases a simple COPY
-	 * suffices.
+	 * a filter condition was specified. OR masking of some columns is needed
+	 * For other cases a simple COPY suffices.
 	 */
-	if (tdinfo->filtercond || tbinfo->relkind == RELKIND_FOREIGN_TABLE)
+	if (tdinfo->filtercond || tbinfo->relkind == RELKIND_FOREIGN_TABLE
+		|| mask_column_info_list.head)
 	{
 		appendPQExpBufferStr(q, "COPY (SELECT ");
 		/* klugery to get rid of parens in column list */
 		if (strlen(column_list) > 2)
 		{
-			appendPQExpBufferStr(q, column_list + 1);
-			q->data[q->len - 1] = ' ';
+			if (mask_column_info_list.head != NULL)
+			{
+				maskColumns(tbinfo, pg_strdup(column_list), &q, NULL);
+				appendPQExpBufferStr(q, " ");
+			}
+			else
+			{
+				appendPQExpBufferStr(q, column_list + 1);
+				q->data[q->len - 1] = ' ';
+			}
 		}
 		else
+		{
 			appendPQExpBufferStr(q, "* ");
+		}
 
 		appendPQExpBuffer(q, "FROM %s %s) TO stdout;",
 						  fmtQualifiedDumpable(tbinfo),
@@ -2016,6 +2242,9 @@ dumpTableData_copy(Archive *fout, const void *dcontext)
 						  fmtQualifiedDumpable(tbinfo),
 						  column_list);
 	}
+
+	free(temp_string);
+
 	res = ExecuteSqlQuery(fout, q->data, PGRES_COPY_OUT);
 	PQclear(res);
 	destroyPQExpBuffer(clistBuf);
@@ -2132,6 +2361,10 @@ dumpTableData_insert(Archive *fout, const void *dcontext)
 				i;
 	int			rows_per_statement = dopt->dump_inserts;
 	int			rows_this_statement = 0;
+	/*for masking*/
+
+	SimpleStringList column_names = {NULL, NULL};
+	SimpleStringListCell *current_column;
 
 	/*
 	 * If we're going to emit INSERTs with column names, the most efficient
@@ -2152,12 +2385,29 @@ dumpTableData_insert(Archive *fout, const void *dcontext)
 		if (nfields > 0)
 			appendPQExpBufferStr(q, ", ");
 		if (tbinfo->attgenerated[i])
+		{
 			appendPQExpBufferStr(q, "NULL");
+			simple_string_list_append(&column_names, "NULL");
+		}
 		else
-			appendPQExpBufferStr(q, fmtId(tbinfo->attnames[i]));
+		{
+			if (mask_column_info_list.head != NULL)
+			{
+				/*taking columns that should be masked */
+				/*char* copy_column_list = pg_strdup(tbinfo->attnames[i]);
+				char* current_column_name = strtok(copy_column_list, " ,()");*/
+				maskColumns(tbinfo, tbinfo->attnames[i], &q, &column_names);
+			}
+			else
+			{
+				appendPQExpBufferStr(q, fmtId(tbinfo->attnames[i]));
+				simple_string_list_append(&column_names, fmtId(tbinfo->attnames[i]));
+			}
+		}
 		attgenerated[nfields] = tbinfo->attgenerated[i];
 		nfields++;
 	}
+
 	/* Servers before 9.4 will complain about zero-column SELECT */
 	if (nfields == 0)
 		appendPQExpBufferStr(q, "NULL");
@@ -2215,13 +2465,14 @@ dumpTableData_insert(Archive *fout, const void *dcontext)
 				/* append the list of column names if required */
 				if (dopt->column_inserts)
 				{
+					current_column = column_names.head;
 					appendPQExpBufferChar(insertStmt, '(');
 					for (int field = 0; field < nfields; field++)
 					{
 						if (field > 0)
 							appendPQExpBufferStr(insertStmt, ", ");
-						appendPQExpBufferStr(insertStmt,
-											 fmtId(PQfname(res, field)));
+						appendPQExpBufferStr(insertStmt, current_column->val);
+						current_column = current_column->next;
 					}
 					appendPQExpBufferStr(insertStmt, ") ");
 				}
@@ -2373,6 +2624,58 @@ dumpTableData_insert(Archive *fout, const void *dcontext)
 	return 1;
 }
 
+/*
+* maskColumns - modifies SELECT queries to mask columns that need masking
+* last argument is only for INSERT case, not used in COPY case.
+*/
+
+static void
+maskColumns(TableInfo *tbinfo, char* column_list, PQExpBuffer* q, SimpleStringList* column_names)
+{
+	char* copy_column_list = pg_strdup(column_list);
+	char* current_column_name = strtok(copy_column_list, " ,()");
+	char* masked_query = (char*)pg_malloc(sizeof(char));
+
+	while (current_column_name != NULL)
+	{
+		SimplePtrListCell* mask_column_info_cell = mask_column_info_list.head;
+		MaskColumnInfo* cur_mask_column_info = (MaskColumnInfo*) mask_column_info_cell->ptr;
+		while (mask_column_info_cell != NULL &&
+			  (strcmp(cur_mask_column_info->column, current_column_name) ||
+				strcmp(cur_mask_column_info->table, tbinfo->dobj.name)))
+			{
+				if (!strcmp(cur_mask_column_info->table, "") &&
+					!strcmp(cur_mask_column_info->column, current_column_name))
+					break;
+
+				mask_column_info_cell = mask_column_info_cell->next;
+				if (mask_column_info_cell)
+					cur_mask_column_info = (MaskColumnInfo*) mask_column_info_cell->ptr;
+			}
+
+		if (mask_column_info_cell != NULL)
+		{
+			/*current table name is stored in tbinfo->dobj.name*/
+			if (!strcmp(cur_mask_column_info->table, "") ||
+				!strcmp(cur_mask_column_info->table, tbinfo->dobj.name))
+				masked_query = psprintf("%s.%s(%s)", cur_mask_column_info->schema,
+										cur_mask_column_info->func, current_column_name);
+			else
+				masked_query = psprintf("%s", current_column_name);
+		}
+		else
+			masked_query = psprintf("%s", current_column_name);
+
+		if (column_names)
+			simple_string_list_append(column_names, current_column_name);
+		current_column_name = strtok(NULL, " ,()");
+		if (current_column_name != NULL)
+			masked_query = psprintf("%s, ", masked_query);
+		appendPQExpBufferStr(*q, masked_query);
+	}
+	free(masked_query);
+}
+
 /*
  * getRootTableInfo:
  *     get the root TableInfo for the given partition table.
diff --git a/src/bin/pg_dump/t/030_pg_dump_masking.pl b/src/bin/pg_dump/t/030_pg_dump_masking.pl
new file mode 100644
index 0000000000..086ad272aa
--- /dev/null
+++ b/src/bin/pg_dump/t/030_pg_dump_masking.pl
@@ -0,0 +1,253 @@
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $tempdir = PostgreSQL::Test::Utils::tempdir;
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->start;
+
+$node->safe_psql("postgres", "CREATE TABLE t0(id int, t text)");
+$node->safe_psql("postgres", "CREATE TABLE t1(id int, d timestamp)");
+$node->safe_psql("postgres", "CREATE TABLE t2(id int, r real)");
+$node->safe_psql("postgres", "CREATE TABLE t3(id int)");
+
+$node->safe_psql("postgres", "INSERT INTO t0 SELECT generate_series(1,3) AS id, md5(random()::text) AS t");
+$node->safe_psql("postgres", "INSERT INTO t1 SELECT generate_series(1,3) AS id,
+															NOW() + (random() * (interval '90 days')) + '30 days' AS d");
+$node->safe_psql("postgres", "INSERT INTO t2 SELECT generate_series(1,3) AS id, random() * 100 AS r");
+$node->safe_psql("postgres", "INSERT INTO t3 SELECT generate_series(1,3) AS id");
+
+$node->safe_psql("postgres", "CREATE SCHEMA test_schema");
+
+#masking functions
+
+my %functions = (
+	'mask_int' => {
+		func_name => 'mask_int',
+		code => 'res := -1',
+		param_type => 'integer',
+		},
+	'mask_int_with_schema' => {
+		func_name => 'test_schema.mask_int_with_schema',
+		code => 'res := -2',
+		param_type => 'integer',
+		},
+	'mask_real' => {
+		func_name => 'mask_real',
+		code => 'res := -1.5',
+		param_type => 'real',
+		},
+	'mask_text' => {
+		func_name => 'mask_text',
+		code => 'res := \'*****\'',
+		param_type => 'text',
+		},
+	'mask_timestamp' => {
+		func_name => 'mask_timestamp',
+		code => 'res := \'1970-01-01 00:00:00\'',
+		param_type => 'timestamp',
+		},
+);
+
+foreach my $function (sort keys %functions)
+{
+	my $query = sprintf "CREATE OR REPLACE FUNCTION %s (IN elem %s, OUT res %s) RETURNS %s AS
+			\$BODY\$
+			BEGIN   				
+				%s;
+				RETURN;
+			END
+			\$BODY\$ LANGUAGE plpgsql;", $functions{$function}->{func_name}, $functions{$function}->{param_type},
+			$functions{$function}->{param_type}, $functions{$function}->{param_type}, $functions{$function}->{code};
+	$node->safe_psql("postgres", $query);
+}
+
+my %tests = (
+    'test_mask_all_ids' => {
+		regexp => qr/^
+			\QCOPY public.t0 (id, t) FROM stdin;\E\n
+			(-1\s*\w*\n){3}
+			\Q\.\E\n
+			(.|\n)*
+			\QCOPY public.t1 (id, d) FROM stdin;\E\n
+			(-1\s*\d{4}-\d{2}-\d{2}\ \d{2}:\d{2}:\d{2}\.\d*\n){3}
+			\Q\.\E\n
+			(.|\n)*
+			\QCOPY public.t2 (id, r) FROM stdin;\E\n
+			(-1\s*\d*\.\d*\n){3}
+			\Q\.\E\n
+			(.|\n)*
+			\QCOPY public.t3 (id) FROM stdin;\E\n
+			(-1\s*\n){3}
+			\Q\.\E
+		/xm,
+		dump => [
+            'pg_dump',
+            'postgres',
+            '-f', "$tempdir/test_mask_all_ids.sql",
+            '--mask-columns', '"id"',
+			'--mask-function', 'mask_int']
+            },
+    'test_mask_some_ids' => {
+		regexp => qr/^
+            \QCOPY public.t0 (id, t) FROM stdin;\E\n
+			(-1\s*\w*\n){3}
+			\Q\.\E\n
+			(.|\n)*
+			\QCOPY public.t1 (id, d) FROM stdin;\E\n
+			(-1\s*\d{4}-\d{2}-\d{2}\ \d{2}:\d{2}:\d{2}\.\d*\n){3}
+			\Q\.\E\n
+			(.|\n)*
+			\QCOPY public.t2 (id, r) FROM stdin;\E\n
+			1\s*\d*\.\d*\n2\s*\d*\.\d*\n3\s*\d*\.\d*\n
+            \Q\.\E\n
+			(.|\n)*
+			\QCOPY public.t3 (id) FROM stdin;\E\n
+			1\s*\n2\s*\n3\s*\n
+			\Q\.\E
+        /xm,
+		dump => [
+            'pg_dump',
+            'postgres',
+            '-f', "$tempdir/test_mask_some_ids.sql",
+            '--mask-columns', '"t0.id, t1.id"',
+			'--mask-function', 'mask_int']
+            },
+    'test_mask_different_types' => {
+		regexp => qr/^
+            \QCOPY public.t0 (id, t) FROM stdin;\E\n
+            1\s*\*{5}\n2\s*\*{5}\n3\s*\*{5}\n
+            \Q\.\E\n
+			(.|\n)*
+			\QCOPY public.t1 (id, d) FROM stdin;\E\n
+			1\s*\Q1970-01-01 00:00:00\E\n2\s*\Q1970-01-01 00:00:00\E\n3\s*\Q1970-01-01 00:00:00\E\n
+            \Q\.\E\n
+			(.|\n)*
+			\QCOPY public.t2 (id, r) FROM stdin;\E\n
+			1\s*\Q-1.5\E\n2\s*\Q-1.5\E\n3\s*\Q-1.5\E\n
+            \Q\.\E\n
+			(.|\n)*
+			\QCOPY public.t3 (id) FROM stdin;\E\n
+			1\s*\n2\s*\n3\s*\n
+			\Q\.\E
+        /xm,
+		dump => [
+			'pg_dump',
+			'postgres',
+			'-f', "$tempdir/test_mask_different_types.sql",
+			'--mask-columns', 't',
+			'--mask-function', 'mask_text',
+			'--mask-columns', 'd',
+			'--mask-function', 'mask_timestamp',
+			'--mask-columns', 'r',
+			'--mask-function', 'mask_real']
+        },
+    'test_mask_ids_with_schema' => {
+		regexp => qr/^
+			\QCOPY public.t0 (id, t) FROM stdin;\E\n
+			(-2\s*\w*\n){3}
+			\Q\.\E\n
+			(.|\n)*
+			\QCOPY public.t1 (id, d) FROM stdin;\E\n
+			(-2\s*\d{4}-\d{2}-\d{2}\ \d{2}:\d{2}:\d{2}\.\d*\n){3}
+			\Q\.\E\n
+			(.|\n)*
+			\QCOPY public.t2 (id, r) FROM stdin;\E\n
+			(-2\s*\d*\.\d*\n){3}
+			\Q\.\E\n
+			(.|\n)*
+			\QCOPY public.t3 (id) FROM stdin;\E\n
+			(-2\s*\n){3}
+			\Q\.\E
+		/xm,
+		dump => [
+            'pg_dump',
+            'postgres',
+            '-f', "$tempdir/test_mask_ids_with_schema.sql",
+            '--mask-columns', 'id',
+			'--mask-function', 'test_schema.mask_int_with_schema']
+            },
+	'test_mask_ids_file' => {
+		regexp => qr/^
+            \QCOPY public.t0 (id, t) FROM stdin;\E\n
+            (-3\s*\w*\n){3}
+			\Q\.\E
+        /xm,
+		dump => [
+            'pg_dump',
+            'postgres',
+            '-f', "$tempdir/test_mask_ids_file.sql",
+			'-t', 't0',
+            '--mask-columns', 'id',
+			'--mask-function', "$tempdir/mask_ids.sql"]
+            },
+	'test_mask_ids_insert' => {
+		regexp => qr/^
+			(\QINSERT INTO public.t0 (id, t) VALUES (-1, \E\'\w*\'\Q);\E\n){3}
+		/xm,
+		dump => [
+            'pg_dump',
+            'postgres',
+            '-f', "$tempdir/test_mask_ids_insert.sql",
+			'-t', 't0',
+			'--column-insert',
+            '--mask-columns', 'id',
+			'--mask-function', 'mask_int']
+            },
+);
+
+open my $fileHandle, ">", "$tempdir/mask_ids.sql";
+print $fileHandle "f_int\ninteger\nplpgsql\nres := -3;";
+close ($fileHandle);
+
+open $fileHandle, ">", "$tempdir/mask_drop_table.sql";
+print $fileHandle "f_int\ninteger\nplpgsql\nDROP TABLE t0;\nres := -3;";
+close ($fileHandle);
+
+open $fileHandle, ">", "$tempdir/mask_grant.sql";
+print $fileHandle "f_int\ninteger\nplpgsql\nres := -3;\nGRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO tester;";
+close ($fileHandle);
+
+foreach my $test (sort keys %tests)
+{
+	$node->command_ok(\@{ $tests{$test}->{dump} },"$test: pg_dump runs");
+
+	my $output_file = slurp_file("$tempdir/${test}.sql");
+
+    ok($output_file =~ $tests{$test}->{regexp}, "$test: should be dumped");
+}
+
+#security test - it shouldn't be possible to execute DROP TABLE during dump
+
+$node->command_fails_like(
+	['pg_dump', 'postgres', '-f', "$tempdir/test_mask_ids_file.sql",
+	 '-t', 't0', '--mask-columns', 'id', '--mask-function', "$tempdir/mask_drop_table.sql" ],
+	qr/\Qg_dump: error: Dumping the contents of table "t0" failed: PQgetResult() failed.
+pg_dump: detail: Error message from server: ERROR:  cannot execute DROP TABLE in a read-only transaction
+CONTEXT:  SQL statement "DROP TABLE t0"
+PL\/pgSQL function public.f_int(integer) line 3 at SQL statement
+pg_dump: detail: Command was: COPY (SELECT public.f_int(id), t FROM public.t0 ) TO stdout;\E/,
+	'trying to drop table during dump');
+
+#security test - it shouldn't be possible to execute GRANT during dump
+
+$node->safe_psql("postgres", "CREATE USER tester");
+
+$node->command_fails_like(
+	['pg_dump', 'postgres', '-f', "$tempdir/test_mask_ids_file.sql",
+	 '-t', 't0', '--mask-columns', 'id', '--mask-function', "$tempdir/mask_grant.sql" ],
+	qr/\Qpg_dump: error: Dumping the contents of table "t0" failed: PQgetResult() failed.
+pg_dump: detail: Error message from server: ERROR:  cannot execute GRANT in a read-only transaction
+CONTEXT:  SQL statement "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO tester"
+PL\/pgSQL function public.f_int(integer) line 4 at SQL statement
+pg_dump: detail: Command was: COPY (SELECT public.f_int(id), t FROM public.t0 ) TO stdout;\E/,
+	'trying to drop table during dump');
+
+
+done_testing();
\ No newline at end of file
#2Julien Rouhaud
rjuju123@gmail.com
In reply to: Олег Целебровский (#1)
Re: Possible solution for masking chosen columns when using pg_dump

Hi,

On Mon, Oct 03, 2022 at 06:30:17PM +0300, Олег Целебровский wrote:

Hello, here's my take on masking data when using pg_dump
 
The main idea is using PostgreSQL functions to replace data during a SELECT.
When table data is dumped SELECT a,b,c,d ... from ... query is generated, the columns that are marked for masking are replaced with result of functions on those columns
Example: columns name, count are to be masked, so the query will look as such: SELECT id, mask_text(name), mask_int(count), date from ...
 
So about the interface: I added 2 more command-line options: 
 
--mask-columns, which specifies what columns from what tables will be masked 
    usage example:
            --mask-columns "t1.name, t2.description" - both columns will be masked with the same corresponding function
            or --mask-columns name - ALL columns with name "name" from all dumped tables will be masked with correspoding function
 
--mask-function, which specifies what functions will mask data
    usage example:
            --mask-function mask_int - corresponding columns will be masked with function named "mask_int" from default schema (public)
            or --mask-function my_schema.mask_varchar - same as above but with specified schema where the function is stored
            or --mask-function somedir/filename - the function is "defined" here - more on the structure below

FTR I wrote an extension POC [1]https://github.com/rjuju/pg_anonymize last weekend that does that but on the backend
side. The main advantage is that it's working with any existing versions of
pg_dump (or any client relying on COPY or even plain interactive SQL
statements), and that the DBA can force a dedicated role to only get a masked
dump, even if they forgot to ask for it.

I only had a quick look at your patch but it seems that you left some todo in
russian, which isn't helpful at least to me.

[1]: https://github.com/rjuju/pg_anonymize

In reply to: Julien Rouhaud (#2)
Re: Possible solution for masking chosen columns when using pg_dump

Hi,
I took a look, here are several suggestions for improvement:

- Masking is not a main functionality of pg_dump and it is better to write
most of the connected things in a separate file like parallel.c or
dumputils.c. This will help slow down the growth of an already huge pg_dump
file.

- Also it can be hard to use a lot of different functions for different
fields, maybe it would be better to set up functions in a file.

- How will it work for the same field and tables in the different schemas?
Can we set up the exact schema for the field?

- misspelling in a word

/*
* Add all columns and funcions to list of MaskColumnInfo structures,
*/

- Why did you use 256 here?

char* table = (char*) pg_malloc(256 * sizeof(char));

Also for malloc you need malloc on 1 symbol more because you have to store
'\0' symbol.

- Instead of addFuncToDatabase you can run your query using something
already defined from fe_utils/query_utils.c. And It will be better to set
up a connection only once and create all functions. Establishing a
connection is a resource-intensive procedure. There are a lot of magic
numbers, better to leave some comments explaining why there are 64 or 512.

- It seems that you are not using temp_string

char *temp_string = (char*)malloc(256 * sizeof(char));

- Grammar issues

/*
* mask_column_info_list contains info about every to-be-masked column:
* its name, a name its table (if nothing is specified - mask all columns

with this name),

* name of masking function and name of schema containing this function

(public if not specified)

*/

the name of its table

пн, 3 окт. 2022 г. в 20:45, Julien Rouhaud <rjuju123@gmail.com>:

Show quoted text

Hi,

On Mon, Oct 03, 2022 at 06:30:17PM +0300, Олег Целебровский wrote:

Hello, here's my take on masking data when using pg_dump

The main idea is using PostgreSQL functions to replace data during a

SELECT.

When table data is dumped SELECT a,b,c,d ... from ... query is

generated, the columns that are marked for masking are replaced with result
of functions on those columns

Example: columns name, count are to be masked, so the query will look as

such: SELECT id, mask_text(name), mask_int(count), date from ...

So about the interface: I added 2 more command-line options:

--mask-columns, which specifies what columns from what tables will be

masked

usage example:
--mask-columns "t1.name, t2.description" - both columns

will be masked with the same corresponding function

or --mask-columns name - ALL columns with name "name" from

all dumped tables will be masked with correspoding function

--mask-function, which specifies what functions will mask data
usage example:
--mask-function mask_int - corresponding columns will be

masked with function named "mask_int" from default schema (public)

or --mask-function my_schema.mask_varchar - same as above

but with specified schema where the function is stored

or --mask-function somedir/filename - the function is

"defined" here - more on the structure below

FTR I wrote an extension POC [1] last weekend that does that but on the
backend
side. The main advantage is that it's working with any existing versions
of
pg_dump (or any client relying on COPY or even plain interactive SQL
statements), and that the DBA can force a dedicated role to only get a
masked
dump, even if they forgot to ask for it.

I only had a quick look at your patch but it seems that you left some todo
in
russian, which isn't helpful at least to me.

[1] https://github.com/rjuju/pg_anonymize

#4Олег Целебровский
oleg_tselebrovskiy@mail.ru
In reply to: Виктория Шепард (#3)
1 attachment(s)
Re[2]: Possible solution for masking chosen columns when using pg_dump

Hi,

I applied most of suggestions: used separate files for most of added code, fixed typos/mistakes, got rid of that pesky TODO that was already implemented, just not deleted.

Added tests (and functionality) for cases when you need to mask columns in tables with the same name in different schemas. If schema is not specified, then columns in all tables with specified name are masked (Example - pg_dump -t ‘t0’ --mask-columns id --mask-function mask_int will mask all ids in all tables with names ‘t0’ in all existing schemas).

Wrote comments for all ‘magic numbers’

About that

- Also it can be hard to use a lot of different functions for different fields, maybe it would be better to set up functions in a file.

I agree with that, but I know about at least 2 other patches (both are WIP, but still) that are interacting with reading command-line options from file. And if everyone will write their own version of reading command-line options from file, it will quickly get confusing.

A solution to that problem is another patch that will put all options from file (one file for any possible options, from existing to future ones) into **argv in main, so that pg_dump can process them as if they came form command line.
 

Пятница, 7 октября 2022, 8:01 +07:00 от Виктория Шепард <we.viktory@gmail.com>:
 
Hi,
I took a look, here are several suggestions for improvement:
 
- Masking is not a main functionality of pg_dump and it is better to write most of the connected things in a separate file like parallel.c or dumputils.c. This will help slow down the growth of an already huge pg_dump file.
 
- Also it can be hard to use a lot of different functions for different fields, maybe it would be better to set up functions in a file.
 
- How will it work for the same field and tables in the different schemas? Can we set up the exact schema for the field?
 
- misspelling in a word

/*
* Add all columns and funcions to list of MaskColumnInfo structures,
*/

 
- Why did you use 256 here?

char* table = (char*) pg_malloc(256 * sizeof(char));

Also for malloc you need malloc on 1 symbol more because you have to store '\0' symbol.
 
- Instead of addFuncToDatabase you can run your query using something already defined from fe_utils/query_utils.c. And It will be better to set up a connection only once and create all functions. Establishing a connection is a resource-intensive procedure. There are a lot of magic numbers, better to leave some comments explaining why there are 64 or 512.
 
- It seems that you are not using temp_string

char   *temp_string = (char*)malloc(256 * sizeof(char));

 
- Grammar issues

/*
* mask_column_info_list contains info about every to-be-masked column:
* its name, a name its table (if nothing is specified - mask all columns with this name),
* name of masking function and name of schema containing this function (public if not specified)
*/

the name of its table
   
пн, 3 окт. 2022 г. в 20:45, Julien Rouhaud < rjuju123@gmail.com >:

Hi,

On Mon, Oct 03, 2022 at 06:30:17PM +0300, Олег Целебровский wrote:

Hello, here's my take on masking data when using pg_dump
 
The main idea is using PostgreSQL functions to replace data during a SELECT.
When table data is dumped SELECT a,b,c,d ... from ... query is generated, the columns that are marked for masking are replaced with result of functions on those columns
Example: columns name, count are to be masked, so the query will look as such: SELECT id, mask_text(name), mask_int(count), date from ...
 
So about the interface: I added 2 more command-line options: 
 
--mask-columns, which specifies what columns from what tables will be masked 
    usage example:
            --mask-columns " t1.name , t2.description" - both columns will be masked with the same corresponding function
            or --mask-columns name - ALL columns with name "name" from all dumped tables will be masked with correspoding function
 
--mask-function, which specifies what functions will mask data
    usage example:
            --mask-function mask_int - corresponding columns will be masked with function named "mask_int" from default schema (public)
            or --mask-function my_schema.mask_varchar - same as above but with specified schema where the function is stored
            or --mask-function somedir/filename - the function is "defined" here - more on the structure below

FTR I wrote an extension POC [1] last weekend that does that but on the backend
side.  The main advantage is that it's working with any existing versions of
pg_dump (or any client relying on COPY or even plain interactive SQL
statements), and that the DBA can force a dedicated role to only get a masked
dump, even if they forgot to ask for it.

I only had a quick look at your patch but it seems that you left some todo in
russian, which isn't helpful at least to me.

[1] https://github.com/rjuju/pg_anonymize

 

 

Attachments:

masking_for_pg_dump_v2.patchtext/x-diff; name="=?UTF-8?B?bWFza2luZ19mb3JfcGdfZHVtcF92Mi5wYXRjaA==?="Download
diff --git a/src/bin/pg_dump/masking.c b/src/bin/pg_dump/masking.c
new file mode 100644
index 0000000000..747a46003d
--- /dev/null
+++ b/src/bin/pg_dump/masking.c
@@ -0,0 +1,269 @@
+/*-------------------------------------------------------------------------
+ *
+ * masking.c
+ *
+ *	Masking functionality for pg_dump
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_dump/masking.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "masking.h"
+
+/*
+ * addFuncToDatabase - parses file specified in command line, executes query from it
+ * adding masking function to database
+ */
+
+static void formMaskingLists(DumpOptions* dopt)
+{
+	/* needed for masking */
+	SimpleStringListCell *mask_func_cell;
+	SimpleStringListCell *mask_columns_cell;
+	SimplePtrListCell	 *mask_column_info_cell;
+	char		*column_name_buffer;
+	char		*table_name_buffer;
+	char		*schema_table_name_buffer;
+	char		*func_name_buffer;
+	char		*schema_name_buffer;
+	FILE 		*mask_func_file;
+	PGconn 		*connection;
+	/* 256 is pretty arbitrary, but it is enough for dbname, host, port and user*/
+	char		*conn_params = (char*) pg_malloc(256 * sizeof(char)); 
+
+    /*
+	* Add all columns and functions to list of MaskColumnInfo structures,
+	*/
+
+	mask_func_cell = mask_func_list.head;
+	mask_columns_cell = mask_columns_list.head;
+
+	while (mask_columns_cell && mask_func_cell)
+	{
+		char* func = mask_func_cell->val;
+		char* column = strtok(mask_columns_cell->val, " ,\'\"");
+		char* table = (char*) pg_malloc(128 * sizeof(char)); /*enough to store schema_name.table_name (63 + 1 + 63 + 1)*/
+		char* schema = (char*) pg_malloc(64 * sizeof(char)); /*enough to store schema name (63 + 1)*/
+		while (column != NULL)
+		{
+			MaskColumnInfo* new_mask_column = (MaskColumnInfo*) pg_malloc(sizeof(MaskColumnInfo));
+			new_mask_column->column = column;
+			new_mask_column->table = table;
+			new_mask_column->func = func;
+			new_mask_column->schema = schema;
+			simple_ptr_list_append(&mask_column_info_list, new_mask_column);
+			table = (char*) pg_malloc(128 * sizeof(char));
+			column = strtok(NULL, " ,\'\"");
+		}
+		mask_columns_cell = mask_columns_cell->next;
+		mask_func_cell = mask_func_cell->next;
+	}
+
+	/*
+	* If there is not enough params of one type throw error
+	*/
+
+	if (mask_columns_cell != NULL || mask_func_cell != NULL)
+		pg_fatal("amount of --mask-columns and --mask-function doesn't match");
+
+	/*
+	* Extract tablenames from list of columns - done here so that strtok isn't
+	* disturbed in previous cycle
+	*/
+
+	mask_column_info_cell = mask_column_info_list.head;
+
+	while (mask_column_info_cell != NULL)
+	{
+		MaskColumnInfo* cur_mask_column_info = (MaskColumnInfo*) mask_column_info_cell->ptr;
+		schema_table_name_buffer = strtok(cur_mask_column_info->column, ".");
+		table_name_buffer = strtok(NULL, ".");
+		column_name_buffer = strtok(NULL, ".");
+		if (table_name_buffer == NULL) /* found column without tablename */
+		{
+			strcpy(cur_mask_column_info->table, "");
+			strcpy(cur_mask_column_info->column, schema_table_name_buffer);
+		}
+		else
+			if (column_name_buffer == NULL) /* name of schema for table isn't specified */
+			{
+				strcpy(cur_mask_column_info->table, schema_table_name_buffer);
+				strcpy(cur_mask_column_info->column, table_name_buffer);
+			}
+			else
+			{
+				strcat(schema_table_name_buffer, table_name_buffer);
+				strcpy(cur_mask_column_info->table, schema_table_name_buffer);
+				strcpy(cur_mask_column_info->column, column_name_buffer);	
+			}
+		mask_column_info_cell = mask_column_info_cell->next;
+	}
+
+	/*
+	* Check if --mask-function is a name of function or a filepath
+	* A connection is opened before processing any functions; 
+	* If a filepath is found - add function through connection;
+	* Connection is closed when all functions are processed 
+	*/
+
+	mask_column_info_cell = mask_column_info_list.head;
+
+	/* Establishing connection to execute CREATE FUNCTION script */
+	strcpy(conn_params, "");
+	if(dopt->cparams.override_dbname)
+		conn_params = psprintf("%s dbname=%s", conn_params, dopt->cparams.override_dbname);
+	else
+		if(dopt->cparams.dbname)
+			conn_params = psprintf("%s dbname=%s", conn_params, dopt->cparams.dbname);
+	if(dopt->cparams.pghost)
+		conn_params = psprintf("%s host=%s", conn_params, dopt->cparams.pghost);
+	if(dopt->cparams.pgport)
+		conn_params = psprintf("%s port=%s", conn_params, dopt->cparams.pgport);
+	if(dopt->cparams.username)
+		conn_params = psprintf("%s user=%s", conn_params, dopt->cparams.username);
+	connection = PQconnectdb(conn_params);
+
+	while (mask_column_info_cell != NULL)
+	{
+		MaskColumnInfo* cur_mask_column_info = (MaskColumnInfo*) mask_column_info_cell->ptr;
+		func_name_buffer = pg_strdup(cur_mask_column_info->func);
+		canonicalize_path(func_name_buffer);
+		mask_func_file = fopen(func_name_buffer, "r");
+		if (mask_func_file != NULL) /* then it is a file with function*/
+		{
+			addFuncToDatabase(cur_mask_column_info, mask_func_file, connection);
+		}
+		else /* function stored in DB*/
+		{
+			schema_name_buffer = strtok(cur_mask_column_info->func, ".");
+			func_name_buffer = strtok(NULL, ".");
+			if (func_name_buffer == NULL) /* found function without schemaname */
+			{
+				strcpy(cur_mask_column_info->schema, "public");
+				strcpy(cur_mask_column_info->func, schema_name_buffer);
+			}
+			else
+			{
+				strcpy(cur_mask_column_info->schema, schema_name_buffer);
+				strcpy(cur_mask_column_info->func, func_name_buffer);
+			}
+		}
+		mask_column_info_cell = mask_column_info_cell->next;
+	}
+
+	PQfinish(connection);
+	free(conn_params);
+}
+
+static void
+addFuncToDatabase(MaskColumnInfo* cur_mask_column_info, FILE* mask_func_file, PGconn *connection)
+{
+	/*
+	 * All buffers are the length of 64 because in PostgreSQL length of identifier
+	 * be it name of column, table, etc are 63 chars long, + 64th for \0 
+	 */
+
+	PQExpBufferData query;
+	char* common_buff = (char*) pg_malloc(64 * sizeof(char));
+	char* func_name_buff = (char*) pg_malloc(64 * sizeof(char));
+	char* argument_type_buff = (char*) pg_malloc(64 * sizeof(char));
+	char* func_language_buff = (char*) pg_malloc(64 * sizeof(char));
+	char* func_body_buff = (char*) pg_malloc(sizeof(char));
+	char* schema_name_buff = (char*) pg_malloc(64 * sizeof(char));
+
+	func_body_buff[0] = 0;
+	fgets(common_buff, 64, mask_func_file);
+	func_name_buff = strdup(strtok(common_buff, " ,\n\t"));
+	fgets(common_buff, 64, mask_func_file);
+	argument_type_buff = strdup(strtok(common_buff, " .,\n\t"));
+	fgets(common_buff, 64, mask_func_file);
+	func_language_buff = strdup(strtok(common_buff, " ,\n\t"));
+	free(common_buff);
+
+	/*
+	 * Body of a function can be big, so we choose 512 as buffer size.
+	 */
+
+	common_buff = (char*) pg_malloc(512 * sizeof(char)); 
+	while(fgets(common_buff, 512, mask_func_file))
+	{
+		func_body_buff = psprintf("%s%s", func_body_buff, common_buff);
+	}
+	
+	initPQExpBuffer(&query);
+	appendPQExpBuffer(&query, "CREATE OR REPLACE FUNCTION %s (IN elem %s, OUT res %s) RETURNS %s AS $BODY$ \nBEGIN\n%s\nRETURN;\nEND\n$BODY$ LANGUAGE %s;",
+	 						func_name_buff, argument_type_buff, argument_type_buff, argument_type_buff,
+							func_body_buff, func_language_buff);
+
+	PQexec(connection, query.data);
+
+	schema_name_buff = strtok(pg_strdup(func_name_buff), ".");
+	func_name_buff = strtok(NULL, ".");
+	if (func_name_buff == NULL) /* found function without schemaname */
+	{
+		strcpy(cur_mask_column_info->schema, "public");
+		strcpy(cur_mask_column_info->func, schema_name_buff);
+	}
+	else
+	{
+		strcpy(cur_mask_column_info->schema, schema_name_buff);
+		strcpy(cur_mask_column_info->func, func_name_buff);
+	}
+}
+
+/*
+* maskColumns - modifies SELECT queries to mask columns that need masking
+* last argument is only for INSERT case, not used in COPY case.
+*/
+
+static void
+maskColumns(TableInfo *tbinfo, char* column_list, PQExpBuffer* q, SimpleStringList* column_names)
+{
+	char* copy_column_list = pg_strdup(column_list);
+	char* current_column_name = strtok(copy_column_list, " ,()");
+	char* masked_query = (char*)pg_malloc(sizeof(char));
+
+	while (current_column_name != NULL)
+	{
+		SimplePtrListCell* mask_column_info_cell = mask_column_info_list.head;
+		MaskColumnInfo* cur_mask_column_info = (MaskColumnInfo*) mask_column_info_cell->ptr;
+		while (mask_column_info_cell != NULL &&
+			  (strcmp(cur_mask_column_info->column, current_column_name) ||
+				strcmp(cur_mask_column_info->table, tbinfo->dobj.name)))
+			{
+				if (!strcmp(cur_mask_column_info->table, "") &&
+					!strcmp(cur_mask_column_info->column, current_column_name))
+					break;
+
+				mask_column_info_cell = mask_column_info_cell->next;
+				if (mask_column_info_cell)
+					cur_mask_column_info = (MaskColumnInfo*) mask_column_info_cell->ptr;
+			}
+
+		if (mask_column_info_cell != NULL)
+		{
+			/*current table name is stored in tbinfo->dobj.name*/
+			if (!strcmp(cur_mask_column_info->table, "") ||
+				!strcmp(cur_mask_column_info->table, tbinfo->dobj.name))
+				masked_query = psprintf("%s.%s(%s)", cur_mask_column_info->schema,
+										cur_mask_column_info->func, current_column_name);
+			else
+				masked_query = psprintf("%s", current_column_name);
+		}
+		else
+			masked_query = psprintf("%s", current_column_name);
+
+		if (column_names)
+			simple_string_list_append(column_names, current_column_name);
+		current_column_name = strtok(NULL, " ,()");
+		if (current_column_name != NULL)
+			masked_query = psprintf("%s, ", masked_query);
+		appendPQExpBufferStr(*q, masked_query);
+	}
+	free(masked_query);
+}
diff --git a/src/bin/pg_dump/masking.h b/src/bin/pg_dump/masking.h
new file mode 100644
index 0000000000..a116b68eb5
--- /dev/null
+++ b/src/bin/pg_dump/masking.h
@@ -0,0 +1,47 @@
+/*-------------------------------------------------------------------------
+ *
+ * masking.h
+ *
+ *	Masking functionality for pg_dump
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_dump/masking.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "dumputils.h"
+
+#ifndef MASKING_H
+#define MASKING_H
+
+typedef struct
+{
+	char* column; 	/* name of masked column */
+	char* table;	/* name of table where masked column is stored */
+	char* func;		/* name of masking function */
+	char* schema;	/* name of schema where masking function is stored */
+} MaskColumnInfo;
+
+
+/*
+* mask_column_info_list contains info about every to-be-masked column:
+* its name, a name of its table (if nothing is specified - mask all columns with this name),
+* name of masking function and name of schema containing this function (public if not specified)
+*/
+
+static SimplePtrList mask_column_info_list = {NULL, NULL};
+SimpleStringList mask_columns_list = {NULL, NULL};
+SimpleStringList mask_func_list = {NULL, NULL};
+
+static void formMaskingLists(DumpOptions* dopt);
+static void addFuncToDatabase(MaskColumnInfo* cur_mask_column_info, 
+							 FILE* mask_func_file, PGconn *connection);
+static void maskColumns(TableInfo *tbinfo, char* current_column_name,
+						PQExpBuffer* q, SimpleStringList* column_names);
+
+
+#endif							/* MASKING_H */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index bd9b066e4e..3b189bd611 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -67,6 +67,8 @@
 #include "pg_dump.h"
 #include "storage/block.h"
 
+#include "masking.c"
+
 typedef struct
 {
 	Oid			roleoid;		/* role's OID */
@@ -413,6 +415,8 @@ main(int argc, char **argv)
 		{"on-conflict-do-nothing", no_argument, &dopt.do_nothing, 1},
 		{"rows-per-insert", required_argument, NULL, 10},
 		{"include-foreign-data", required_argument, NULL, 11},
+		{"mask-columns", required_argument, NULL, 12},
+		{"mask-function", required_argument, NULL, 13},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -623,6 +627,14 @@ main(int argc, char **argv)
 										  optarg);
 				break;
 
+			case 12:			/* columns for masking */
+				simple_string_list_append(&mask_columns_list, optarg);
+				break;
+
+			case 13:			/* function for masking - can be SQL function from .sql file,
+								   declared in CLI or declared in DB*/
+				simple_string_list_append(&mask_func_list, optarg);
+				break;
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -658,6 +670,8 @@ main(int argc, char **argv)
 	if (dopt.binary_upgrade)
 		dopt.sequence_data = 1;
 
+	formMaskingLists(&dopt);
+
 	if (dopt.dataOnly && dopt.schemaOnly)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
 
@@ -1034,6 +1048,11 @@ help(const char *progname)
 			 "                               servers matching PATTERN\n"));
 	printf(_("  --inserts                    dump data as INSERT commands, rather than COPY\n"));
 	printf(_("  --load-via-partition-root    load partitions via the root table\n"));
+	printf(_("  --mask-columns               names of columns that will be masked \n"
+			 "                               if table name is not specified, mask in all tables\n"));
+	printf(_("  --mask-function              name of function that will mask corresponding columns\n"
+			 "                               can specify schema in which function is stored\n"
+			 "								 can use filepath to file with function arguments\n"));
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
@@ -1991,17 +2010,26 @@ dumpTableData_copy(Archive *fout, const void *dcontext)
 
 	/*
 	 * Use COPY (SELECT ...) TO when dumping a foreign table's data, and when
-	 * a filter condition was specified.  For other cases a simple COPY
-	 * suffices.
+	 * a filter condition was specified. OR masking of some columns is needed
+	 * For other cases a simple COPY suffices.
 	 */
-	if (tdinfo->filtercond || tbinfo->relkind == RELKIND_FOREIGN_TABLE)
+	if (tdinfo->filtercond || tbinfo->relkind == RELKIND_FOREIGN_TABLE
+		|| mask_column_info_list.head)
 	{
 		appendPQExpBufferStr(q, "COPY (SELECT ");
 		/* klugery to get rid of parens in column list */
 		if (strlen(column_list) > 2)
 		{
-			appendPQExpBufferStr(q, column_list + 1);
-			q->data[q->len - 1] = ' ';
+			if (mask_column_info_list.head != NULL)
+			{
+				maskColumns(tbinfo, pg_strdup(column_list), &q, NULL);
+				appendPQExpBufferStr(q, " ");
+			}
+			else
+			{
+				appendPQExpBufferStr(q, column_list + 1);
+				q->data[q->len - 1] = ' ';
+			}
 		}
 		else
 			appendPQExpBufferStr(q, "* ");
@@ -2132,6 +2160,10 @@ dumpTableData_insert(Archive *fout, const void *dcontext)
 				i;
 	int			rows_per_statement = dopt->dump_inserts;
 	int			rows_this_statement = 0;
+	/*for masking*/
+
+	SimpleStringList column_names = {NULL, NULL};
+	SimpleStringListCell *current_column;
 
 	/*
 	 * If we're going to emit INSERTs with column names, the most efficient
@@ -2152,9 +2184,25 @@ dumpTableData_insert(Archive *fout, const void *dcontext)
 		if (nfields > 0)
 			appendPQExpBufferStr(q, ", ");
 		if (tbinfo->attgenerated[i])
+		{
 			appendPQExpBufferStr(q, "NULL");
+			simple_string_list_append(&column_names, "NULL");
+		}
 		else
-			appendPQExpBufferStr(q, fmtId(tbinfo->attnames[i]));
+		{
+			if (mask_column_info_list.head != NULL)
+			{
+				/*taking columns that should be masked */
+				/*char* copy_column_list = pg_strdup(tbinfo->attnames[i]);
+				char* current_column_name = strtok(copy_column_list, " ,()");*/
+				maskColumns(tbinfo, tbinfo->attnames[i], &q, &column_names);
+			}
+			else
+			{
+				appendPQExpBufferStr(q, fmtId(tbinfo->attnames[i]));
+				simple_string_list_append(&column_names, fmtId(tbinfo->attnames[i]));
+			}
+		}
 		attgenerated[nfields] = tbinfo->attgenerated[i];
 		nfields++;
 	}
@@ -2215,13 +2263,14 @@ dumpTableData_insert(Archive *fout, const void *dcontext)
 				/* append the list of column names if required */
 				if (dopt->column_inserts)
 				{
+					current_column = column_names.head;
 					appendPQExpBufferChar(insertStmt, '(');
 					for (int field = 0; field < nfields; field++)
 					{
 						if (field > 0)
 							appendPQExpBufferStr(insertStmt, ", ");
-						appendPQExpBufferStr(insertStmt,
-											 fmtId(PQfname(res, field)));
+						appendPQExpBufferStr(insertStmt, current_column->val);
+						current_column = current_column->next;
 					}
 					appendPQExpBufferStr(insertStmt, ") ");
 				}
diff --git a/src/bin/pg_dump/t/030_pg_dump_masking.pl b/src/bin/pg_dump/t/030_pg_dump_masking.pl
new file mode 100644
index 0000000000..30b2cc2021
--- /dev/null
+++ b/src/bin/pg_dump/t/030_pg_dump_masking.pl
@@ -0,0 +1,289 @@
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $tempdir = PostgreSQL::Test::Utils::tempdir;
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->start;
+
+$node->safe_psql("postgres", "CREATE TABLE t0(id int, t text)");
+$node->safe_psql("postgres", "CREATE TABLE t1(id int, d timestamp)");
+$node->safe_psql("postgres", "CREATE TABLE t2(id int, r real)");
+$node->safe_psql("postgres", "CREATE TABLE t3(id int)");
+
+$node->safe_psql("postgres", "INSERT INTO t0 SELECT generate_series(1,3) AS id, md5(random()::text) AS t");
+$node->safe_psql("postgres", "INSERT INTO t1 SELECT generate_series(1,3) AS id,
+															NOW() + (random() * (interval '90 days')) + '30 days' AS d");
+$node->safe_psql("postgres", "INSERT INTO t2 SELECT generate_series(1,3) AS id, random() * 100 AS r");
+$node->safe_psql("postgres", "INSERT INTO t3 SELECT generate_series(1,3) AS id");
+
+$node->safe_psql("postgres", "CREATE SCHEMA test_schema");
+
+$node->safe_psql("postgres", "CREATE TABLE test_schema.t0(id int)");
+$node->safe_psql("postgres", "INSERT INTO test_schema.t0 SELECT generate_series(1,3) AS id");
+
+#masking functions
+
+my %functions = (
+	'mask_int' => {
+		func_name => 'mask_int',
+		code => 'res := -1',
+		param_type => 'integer',
+		},
+	'mask_int_with_schema' => {
+		func_name => 'test_schema.mask_int_with_schema',
+		code => 'res := -2',
+		param_type => 'integer',
+		},
+	'mask_real' => {
+		func_name => 'mask_real',
+		code => 'res := -1.5',
+		param_type => 'real',
+		},
+	'mask_text' => {
+		func_name => 'mask_text',
+		code => 'res := \'*****\'',
+		param_type => 'text',
+		},
+	'mask_timestamp' => {
+		func_name => 'mask_timestamp',
+		code => 'res := \'1970-01-01 00:00:00\'',
+		param_type => 'timestamp',
+		},
+);
+
+foreach my $function (sort keys %functions)
+{
+	my $query = sprintf "CREATE OR REPLACE FUNCTION %s (IN elem %s, OUT res %s) RETURNS %s AS
+			\$BODY\$
+			BEGIN   				
+				%s;
+				RETURN;
+			END
+			\$BODY\$ LANGUAGE plpgsql;", $functions{$function}->{func_name}, $functions{$function}->{param_type},
+			$functions{$function}->{param_type}, $functions{$function}->{param_type}, $functions{$function}->{code};
+	$node->safe_psql("postgres", $query);
+}
+
+my %tests = (
+    'test_mask_all_ids' => {
+		regexp => qr/^
+			\QCOPY public.t0 (id, t) FROM stdin;\E\n
+			(-1\s*\w*\n){3}
+			\Q\.\E\n
+			(.|\n)*
+			\QCOPY public.t1 (id, d) FROM stdin;\E\n
+			(-1\s*\d{4}-\d{2}-\d{2}\ \d{2}:\d{2}:\d{2}\.\d*\n){3}
+			\Q\.\E\n
+			(.|\n)*
+			\QCOPY public.t2 (id, r) FROM stdin;\E\n
+			(-1\s*\d*\.\d*\n){3}
+			\Q\.\E\n
+			(.|\n)*
+			\QCOPY public.t3 (id) FROM stdin;\E\n
+			(-1\s*\n){3}
+			\Q\.\E\n
+			(.|\n)*
+			\QCOPY test_schema.t0 (id) FROM stdin;\E\n
+			(-1\s*\n){3}
+			\Q\.\E\
+		/xm,
+		dump => [
+            'pg_dump',
+            'postgres',
+            '-f', "$tempdir/test_mask_all_ids.sql",
+            '--mask-columns', '"id"',
+			'--mask-function', 'mask_int']
+            },
+    'test_mask_some_ids' => {
+		regexp => qr/^
+            \QCOPY public.t0 (id, t) FROM stdin;\E\n
+			(-1\s*\w*\n){3}
+			\Q\.\E\n
+			(.|\n)*
+			\QCOPY public.t1 (id, d) FROM stdin;\E\n
+			(-1\s*\d{4}-\d{2}-\d{2}\ \d{2}:\d{2}:\d{2}\.\d*\n){3}
+			\Q\.\E\n
+			(.|\n)*
+			\QCOPY public.t2 (id, r) FROM stdin;\E\n
+			1\s*\d*\.\d*\n2\s*\d*\.\d*\n3\s*\d*\.\d*\n
+            \Q\.\E\n
+			(.|\n)*
+			\QCOPY public.t3 (id) FROM stdin;\E\n
+			1\s*\n2\s*\n3\s*\n
+			\Q\.\E\n
+			(.|\n)*
+			\QCOPY test_schema.t0 (id) FROM stdin;\E\n
+			(-1\s*\w*\n){3}
+			\Q\.\E\
+        /xm,
+		dump => [
+            'pg_dump',
+            'postgres',
+            '-f', "$tempdir/test_mask_some_ids.sql",
+            '--mask-columns', '"t0.id, t1.id"',
+			'--mask-function', 'mask_int']
+            },
+    'test_mask_different_types' => {
+		regexp => qr/^
+            \QCOPY public.t0 (id, t) FROM stdin;\E\n
+            1\s*\*{5}\n2\s*\*{5}\n3\s*\*{5}\n
+            \Q\.\E\n
+			(.|\n)*
+			\QCOPY public.t1 (id, d) FROM stdin;\E\n
+			1\s*\Q1970-01-01 00:00:00\E\n2\s*\Q1970-01-01 00:00:00\E\n3\s*\Q1970-01-01 00:00:00\E\n
+            \Q\.\E\n
+			(.|\n)*
+			\QCOPY public.t2 (id, r) FROM stdin;\E\n
+			1\s*\Q-1.5\E\n2\s*\Q-1.5\E\n3\s*\Q-1.5\E\n
+            \Q\.\E\n
+			(.|\n)*
+			\QCOPY public.t3 (id) FROM stdin;\E\n
+			1\s*\n2\s*\n3\s*\n
+			\Q\.\E\n
+			(.|\n)*
+			\QCOPY test_schema.t0 (id) FROM stdin;\E\n
+			1\s*\n2\s*\n3\s*\n
+			\Q\.\E\
+        /xm,
+		dump => [
+			'pg_dump',
+			'postgres',
+			'-f', "$tempdir/test_mask_different_types.sql",
+			'--mask-columns', 't',
+			'--mask-function', 'mask_text',
+			'--mask-columns', 'd',
+			'--mask-function', 'mask_timestamp',
+			'--mask-columns', 'r',
+			'--mask-function', 'mask_real']
+        },
+    'test_mask_ids_with_schema' => {
+		regexp => qr/^
+			\QCOPY public.t0 (id, t) FROM stdin;\E\n
+			(-2\s*\w*\n){3}
+			\Q\.\E\n
+			(.|\n)*
+			\QCOPY public.t1 (id, d) FROM stdin;\E\n
+			(-2\s*\d{4}-\d{2}-\d{2}\ \d{2}:\d{2}:\d{2}\.\d*\n){3}
+			\Q\.\E\n
+			(.|\n)*
+			\QCOPY public.t2 (id, r) FROM stdin;\E\n
+			(-2\s*\d*\.\d*\n){3}
+			\Q\.\E\n
+			(.|\n)*
+			\QCOPY public.t3 (id) FROM stdin;\E\n
+			(-2\s*\n){3}
+			\Q\.\E\n
+			(.|\n)*
+			\QCOPY test_schema.t0 (id) FROM stdin;\E\n
+			(-2\s*\n){3}
+			\Q\.\E\
+		/xm,
+		dump => [
+            'pg_dump',
+            'postgres',
+            '-f', "$tempdir/test_mask_ids_with_schema.sql",
+            '--mask-columns', 'id',
+			'--mask-function', 'test_schema.mask_int_with_schema']
+            },
+	'test_mask_ids_file' => {
+		regexp => qr/^
+            \QCOPY public.t0 (id, t) FROM stdin;\E\n
+            (-3\s*\w*\n){3}
+			\Q\.\E
+        /xm,
+		dump => [
+            'pg_dump',
+            'postgres',
+            '-f', "$tempdir/test_mask_ids_file.sql",
+			'-t', 't0',
+            '--mask-columns', 'id',
+			'--mask-function', "$tempdir/mask_ids.sql"]
+            },
+	'test_mask_ids_insert' => {
+		regexp => qr/^
+			(\QINSERT INTO public.t0 (id, t) VALUES (-1, \E\'\w*\'\Q);\E\n){3}
+		/xm,
+		dump => [
+            'pg_dump',
+            'postgres',
+            '-f', "$tempdir/test_mask_ids_insert.sql",
+			'-t', 't0',
+			'--column-insert',
+            '--mask-columns', 'id',
+			'--mask-function', 'mask_int']
+            },
+	'test_mask_some_ids_with_schema' => {
+		regexp => qr/^
+            \QCOPY public.t0 (id, t) FROM stdin;\E\n
+			(-1\s*\w*\n){3}
+			\Q\.\E\n
+			(.|\n)*
+			\QCOPY test_schema.t0 (id) FROM stdin;\E\n
+			(-1\s*\n){3}
+			\Q\.\E\
+        /xm,
+		dump => [
+            'pg_dump',
+            'postgres',
+            '-f', "$tempdir/test_mask_some_ids_with_schema.sql",
+            '--mask-columns', '"t0.id, test_schema.t0.id"',
+			'--mask-function', 'mask_int']
+            },
+);
+
+open my $fileHandle, ">", "$tempdir/mask_ids.sql";
+print $fileHandle "f_int\ninteger\nplpgsql\nres := -3;";
+close ($fileHandle);
+
+open $fileHandle, ">", "$tempdir/mask_drop_table.sql";
+print $fileHandle "f_int\ninteger\nplpgsql\nDROP TABLE t0;\nres := -3;";
+close ($fileHandle);
+
+open $fileHandle, ">", "$tempdir/mask_grant.sql";
+print $fileHandle "f_int\ninteger\nplpgsql\nres := -3;\nGRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO tester;";
+close ($fileHandle);
+
+foreach my $test (sort keys %tests)
+{
+	$node->command_ok(\@{ $tests{$test}->{dump} },"$test: pg_dump runs");
+
+	my $output_file = slurp_file("$tempdir/${test}.sql");
+
+    ok($output_file =~ $tests{$test}->{regexp}, "$test: should be dumped");
+}
+
+#security test - it shouldn't be possible to execute DROP TABLE during dump
+
+$node->command_fails_like(
+	['pg_dump', 'postgres', '-f', "$tempdir/test_mask_ids_file.sql",
+	 '-t', 't0', '--mask-columns', 'id', '--mask-function', "$tempdir/mask_drop_table.sql" ],
+	qr/\Qg_dump: error: Dumping the contents of table "t0" failed: PQgetResult() failed.
+pg_dump: detail: Error message from server: ERROR:  cannot execute DROP TABLE in a read-only transaction
+CONTEXT:  SQL statement "DROP TABLE t0"
+PL\/pgSQL function public.f_int(integer) line 3 at SQL statement
+pg_dump: detail: Command was: COPY (SELECT public.f_int(id), t FROM public.t0 ) TO stdout;\E/,
+	'trying to drop table during dump');
+
+#security test - it shouldn't be possible to execute GRANT during dump
+
+$node->safe_psql("postgres", "CREATE USER tester");
+
+$node->command_fails_like(
+	['pg_dump', 'postgres', '-f', "$tempdir/test_mask_ids_file.sql",
+	 '-t', 't0', '--mask-columns', 'id', '--mask-function', "$tempdir/mask_grant.sql" ],
+	qr/\Qpg_dump: error: Dumping the contents of table "t0" failed: PQgetResult() failed.
+pg_dump: detail: Error message from server: ERROR:  cannot execute GRANT in a read-only transaction
+CONTEXT:  SQL statement "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO tester"
+PL\/pgSQL function public.f_int(integer) line 4 at SQL statement
+pg_dump: detail: Command was: COPY (SELECT public.f_int(id), t FROM public.t0 ) TO stdout;\E/,
+	'trying to drop table during dump');
+
+
+done_testing();
In reply to: Олег Целебровский (#4)
1 attachment(s)
Re: Re[2]: Possible solution for masking chosen columns when using pg_dump

Hi,

Here is an idea of how to read masking options from a file. Please, take a
look.

пн, 10 окт. 2022 г. в 14:54, Олег Целебровский <oleg_tselebrovskiy@mail.ru>:

Show quoted text

Hi,

I applied most of suggestions: used separate files for most of added code,
fixed typos/mistakes, got rid of that pesky TODO that was already
implemented, just not deleted.

Added tests (and functionality) for cases when you need to mask columns in
tables with the same name in different schemas. If schema is not specified,
then columns in all tables with specified name are masked (Example -
pg_dump -t ‘t0’ --mask-columns id --mask-function mask_int will mask all
ids in all tables with names ‘t0’ in all existing schemas).

Wrote comments for all ‘magic numbers’

About that

- Also it can be hard to use a lot of different functions for different

fields, maybe it would be better to set up functions in a file.

I agree with that, but I know about at least 2 other patches (both are
WIP, but still) that are interacting with reading command-line options from
file. And if everyone will write their own version of reading command-line
options from file, it will quickly get confusing.

A solution to that problem is another patch that will put all options from
file (one file for any possible options, from existing to future ones) into
**argv in main, so that pg_dump can process them as if they came form
command line.

Пятница, 7 октября 2022, 8:01 +07:00 от Виктория Шепард <
we.viktory@gmail.com>:

Hi,
I took a look, here are several suggestions for improvement:

- Masking is not a main functionality of pg_dump and it is better to write
most of the connected things in a separate file like parallel.c or
dumputils.c. This will help slow down the growth of an already huge pg_dump
file.

- Also it can be hard to use a lot of different functions for different
fields, maybe it would be better to set up functions in a file.

- How will it work for the same field and tables in the different schemas?
Can we set up the exact schema for the field?

- misspelling in a word

/*
* Add all columns and funcions to list of MaskColumnInfo structures,
*/

- Why did you use 256 here?

char* table = (char*) pg_malloc(256 * sizeof(char));

Also for malloc you need malloc on 1 symbol more because you have to store
'\0' symbol.

- Instead of addFuncToDatabase you can run your query using something
already defined from fe_utils/query_utils.c. And It will be better to set
up a connection only once and create all functions. Establishing a
connection is a resource-intensive procedure. There are a lot of magic
numbers, better to leave some comments explaining why there are 64 or 512.

- It seems that you are not using temp_string

char *temp_string = (char*)malloc(256 * sizeof(char));

- Grammar issues

/*
* mask_column_info_list contains info about every to-be-masked column:
* its name, a name its table (if nothing is specified - mask all columns

with this name),

* name of masking function and name of schema containing this function

(public if not specified)

*/

the name of its table

пн, 3 окт. 2022 г. в 20:45, Julien Rouhaud <rjuju123@gmail.com
<//e.mail.ru/compose/?mailto=mailto%3arjuju123@gmail.com>>:

Hi,

On Mon, Oct 03, 2022 at 06:30:17PM +0300, Олег Целебровский wrote:

Hello, here's my take on masking data when using pg_dump

The main idea is using PostgreSQL functions to replace data during a

SELECT.

When table data is dumped SELECT a,b,c,d ... from ... query is

generated, the columns that are marked for masking are replaced with result
of functions on those columns

Example: columns name, count are to be masked, so the query will look as

such: SELECT id, mask_text(name), mask_int(count), date from ...

So about the interface: I added 2 more command-line options:

--mask-columns, which specifies what columns from what tables will be

masked

usage example:
--mask-columns "t1.name, t2.description" - both columns

will be masked with the same corresponding function

or --mask-columns name - ALL columns with name "name" from

all dumped tables will be masked with correspoding function

--mask-function, which specifies what functions will mask data
usage example:
--mask-function mask_int - corresponding columns will be

masked with function named "mask_int" from default schema (public)

or --mask-function my_schema.mask_varchar - same as above

but with specified schema where the function is stored

or --mask-function somedir/filename - the function is

"defined" here - more on the structure below

FTR I wrote an extension POC [1] last weekend that does that but on the
backend
side. The main advantage is that it's working with any existing versions
of
pg_dump (or any client relying on COPY or even plain interactive SQL
statements), and that the DBA can force a dedicated role to only get a
masked
dump, even if they forgot to ask for it.

I only had a quick look at your patch but it seems that you left some todo
in
russian, which isn't helpful at least to me.

[1] https://github.com/rjuju/pg_anonymize

Attachments:

0001-C4a-pg-dump-masking-option.patchtext/x-patch; charset=US-ASCII; name=0001-C4a-pg-dump-masking-option.patchDownload
From 0ac0027defe4b312481cd18280fa417e6aa6d5e1 Mon Sep 17 00:00:00 2001
From: Victoria Shepard <5807469+demonolock@users.noreply.github.com>
Date: Wed, 12 Oct 2022 04:42:24 +0500
Subject: [PATCH] C4a pg dump masking option

---
 doc/src/sgml/ref/pg_dump.sgml         |  59 +++
 src/bin/pg_dump/Makefile              |   7 +-
 src/bin/pg_dump/masking.c             | 731 ++++++++++++++++++++++++++
 src/bin/pg_dump/masking.h             |  79 +++
 src/bin/pg_dump/pg_dump.c             | 183 +++++--
 src/bin/pg_dump/t/001_basic.pl        |   5 +
 src/bin/pg_dump/t/011_dump_masking.pl | 456 ++++++++++++++++
 src/include/fe_utils/simple_list.h    |   2 +
 src/tools/msvc/Mkvcbuild.pm           |   1 +
 9 files changed, 1488 insertions(+), 35 deletions(-)
 create mode 100644 src/bin/pg_dump/masking.c
 create mode 100644 src/bin/pg_dump/masking.h
 create mode 100644 src/bin/pg_dump/t/011_dump_masking.pl

diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 8b9d9f4cad..fe9503e6a4 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -881,6 +881,65 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--masking=<replaceable class="parameter">filename</replaceable></option></term>
+      <listitem>
+       <para>
+        Specify a filename from which to read masking patterns for using masking functions
+        to fields. You have ability to use user-defined functions from file, functions that
+        already defined in the cluster and default functions.
+
+        Default masking functions - are full masking according to the data types.
+        Returns 'XXXX' for string data types [text, varchar, character].
+        Returns 0 for numeric data types [int, numeric, real, smallint, bigint].
+        Returns '1900-01-01' for date and '1900-01-01 00:00:00' for timestamp.
+
+        Here is an example of masking pattern file:
+
+        masking_file.txt
+        <programlisting>
+        Schema1 {
+            Table1 {
+                  field11 : function_name11
+                , field12 : function_name12
+                , field13 : function_name13
+            }
+
+            Table2 {
+                  field21 : function_name21
+                , field22 : function_name22
+                /* This function will be created by script from the path 'path_to_file_with_function'
+                   and used for 'field23' */
+                , field23 : "path_to_file_with_function"
+            }
+        }
+
+        /* Functions inside this block will be used for all schemes */
+        default {
+            /* Functions inside this block will be used for all tables */
+            default {
+                  /* Function 'for_all_fields' will be used for all fields
+                     did not covered by exact functions */
+                  default: for_all_fields,
+                  field1: value1,
+                  /* Using default masking function for field2 */
+                  field2: default
+            }
+
+            /* Functions inside this block will be used for tables with name 'Table'
+               in the all schemes */
+            Table {
+                   field : function_name
+            }
+        }
+        </programlisting>
+
+        Masking using only with <command>INSERT</command> command. You can't use it with
+        <command>COPY</command> command.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-comments</option></term>
       <listitem>
diff --git a/src/bin/pg_dump/Makefile b/src/bin/pg_dump/Makefile
index 9dc5a784dd..e5e46327fe 100644
--- a/src/bin/pg_dump/Makefile
+++ b/src/bin/pg_dump/Makefile
@@ -19,13 +19,14 @@ include $(top_builddir)/src/Makefile.global
 export GZIP_PROGRAM=$(GZIP)
 export with_icu
 
-override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
+override CPPFLAGS := -g -I$(libpq_srcdir) $(CPPFLAGS)
 LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport)
 
 OBJS = \
 	$(WIN32RES) \
 	compress_io.o \
 	dumputils.o \
+	masking.o \
 	parallel.o \
 	pg_backup_archiver.o \
 	pg_backup_custom.o \
@@ -43,8 +44,8 @@ pg_dump: pg_dump.o common.o pg_dump_sort.o $(OBJS) | submake-libpq submake-libpg
 pg_restore: pg_restore.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_restore.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
-pg_dumpall: pg_dumpall.o dumputils.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
-	$(CC) $(CFLAGS) pg_dumpall.o dumputils.o $(WIN32RES) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+pg_dumpall: pg_dumpall.o dumputils.o masking.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) pg_dumpall.o dumputils.o masking.o $(WIN32RES) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_dump$(X) '$(DESTDIR)$(bindir)'/pg_dump$(X)
diff --git a/src/bin/pg_dump/masking.c b/src/bin/pg_dump/masking.c
new file mode 100644
index 0000000000..962d5b6d43
--- /dev/null
+++ b/src/bin/pg_dump/masking.c
@@ -0,0 +1,731 @@
+/*-------------------------------------------------------------------------
+ *
+ * masking.c
+ *
+ * Data masking tool for pg_dump
+ *
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/bin/pg_dump/masking.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include <ctype.h>
+#include "masking.h"
+#include "common/logging.h"
+
+#define REL_SIZE 64 * 8 /* Length of relation name - 64 bytes */
+#define DEFAULT_NAME "default"
+#define COL_WITH_FUNC_SIZE 3 * REL_SIZE + 3 /* schema_name.function name + '(' + column_name + ') */
+
+char REL_SEP = '.'; /* Relation separator */
+
+/* Initialise masking map */
+MaskingMap *
+newMaskingMap()
+{
+    MaskingMap *map = malloc(sizeof(MaskingMap));
+    map->size = 0;
+    map->capacity = 8;
+    map->data = malloc(sizeof(Pair) * map->capacity);
+    memset(map->data, 0, sizeof(Pair) * map->capacity);
+    return map;
+}
+
+int
+getMapIndexByKey(MaskingMap *map, char *key)
+{
+    int index = 0;
+    while (map->data[index] != NULL)
+    {
+        if (strcmp(map->data[index]->key, key) == 0)
+        {
+            return index;
+        }
+        index++;
+    }
+    return -1;
+}
+
+void
+cleanMap(MaskingMap *map)
+{
+    if (map != NULL && map->data != NULL)
+    {
+        for (int i = 0; map->data[i] != NULL; i++)
+        {
+            free(map->data[i]->key);
+            free(map->data[i]->value);
+            free(map->data[i]);
+        }
+        free(map->data);
+        free(map);
+    }
+}
+
+/*
+ * Add value to map or rewrite, if key already exists
+ */
+void
+setMapValue(MaskingMap *map, char *key, char *value)
+{
+    if (key != NULL)
+    {
+        int index = getMapIndexByKey(map, key);
+        if (index != -1) /* Already have key in map */
+        {
+            free(map->data[index]->value);
+            map->data[index]->value = malloc(strlen(value) + 1);
+            strcpy(map->data[index]->value, value);
+        }
+        else
+        {
+            Pair *pair = malloc(sizeof(Pair));
+            pair->key = malloc(strlen(key) + 1);
+            pair->value = malloc(strlen(value) + 1);
+            memset(pair->key, 0, strlen(key));
+            memset(pair->value, 0, strlen(value));
+            strcpy(pair->key, key);
+            strcpy(pair->value, value);
+
+            map->data[map->size] = malloc(sizeof(Pair));
+            *map->data[map->size] = *pair;
+            map->size++;
+            free(pair);
+        }
+        if (map->size == map->capacity) /* Increase capacity */
+        {
+            map->capacity *= 1.5;
+            map->data = realloc(map->data, sizeof(Pair) * map->capacity);
+        }
+    }
+    free(key);
+}
+
+void
+printParsingError(struct MaskingDebugDetails *md, char *message, char current_symbol)
+{
+    pg_log_error("Error position (symbol '%c'): line: %d pos: %d. %s\n", current_symbol, md->line_num, md->symbol_num,
+           message);
+}
+
+bool
+isTerminal(char c)
+{
+    return c == ':' || c == ',' || c == '{' || c == '}' || c == EOF;
+}
+
+bool
+isSpace(char c)
+{
+    return c == ' ' || c == '\t' || c == '\n' || c == EOF;
+}
+
+/*
+ * Read symbol and change place of cursor in MaskingDebugDetails
+ * md->line_num - increasing when we meet '\n'
+ * md->symbol_num - increasing after reading any symbol and reset
+ * when we meet '\n'
+ */
+char
+readNextSymbol(struct MaskingDebugDetails *md, FILE *fin)
+{
+    char c = fgetc(fin);
+    if (c == '\n')
+    {
+        md->line_num++;
+        md->symbol_num = 1;
+    }
+    else
+    {
+        md->symbol_num++;
+    }
+    return c;
+}
+
+/* Read relation name */
+char
+nameReader(char *rel_name, char c, struct MaskingDebugDetails *md, FILE *fin, int size)
+{
+    memset(rel_name, 0,  size);
+    while (!isTerminal(c))
+    {
+        switch (c)
+        {
+            case ' ':
+            case '\t':
+            case '\n':
+                break; /* Skip space symbols */
+            case EOF:
+                return c; /* Handling `EOF` outside the function */
+
+            default:
+                strncat(rel_name, &c, 1);
+                break;
+        }
+        c = readNextSymbol(md, fin);
+    }
+    return c;
+}
+
+/* Concat schema name, table name and column name */
+char *
+getFullRelName(char *schema_name, char *table_name, char *column_name)
+{
+    char *full_name = malloc(REL_SIZE * 3); /* Schema.Table.Field */
+    memset(full_name, 0, REL_SIZE * 3);
+    strcpy(full_name, schema_name);
+    strncat(full_name, &REL_SEP, 1);
+    strcat(full_name, table_name);
+    strncat(full_name, &REL_SEP, 1);
+    strcat(full_name, column_name);
+    return full_name;
+}
+
+/**
+ * Parsing file with masking pattern
+ * ------------------------------------
+ * Schema1
+ * {
+ *      Table1
+ *      {
+ *            field11 : function_name11
+ *          , field12 : function_name12
+ *          , field13 : function_name13
+ *      }
+ *
+ *      Table2
+ *      {
+ *            field21 : function_name21
+ *          , field22 : function_name22
+ *      --This function will be stored in `masking_func_query_path` list, and these functions will be
+ *      --created by script from the path 'path_to_file_with_function' - `pg_dump.c:createMaskingFunctions`
+ *      --and used for 'field23' - `masking.c:addFunctionToColumn`
+ *          , field23 : "path_to_file_with_function"
+ *      }
+ *  }
+ *
+ *  --Functions inside this block will be used for all schemes
+ * default
+ * {
+ *      --Functions inside this block will be used for all tables
+ *      default
+ *      {
+ *          --Function 'for_all_fields' will be used for all fields did not covered by exact functions
+ *          default: for_all_fields,
+ *          field1: value1,
+ *          field2: value2
+ *      }
+ *      --Functions inside this block will be used for tables with name 'Table' in the all schemes
+ *      Table
+ *      {
+ *          field : function_name
+ *      }
+ * }
+ */
+int
+readMaskingPatternFromFile(FILE *fin, MaskingMap *map, SimpleStringList *masking_func_query_path)
+{
+    int exit_status;
+    int brace_counter;
+    int close_brace_counter;
+    char *schema_name;
+    char *table_name;
+    char *column_name;
+    char *func_name;
+    bool skip_reading;
+    char c;
+
+    struct MaskingDebugDetails md;
+    md.line_num = 1;
+    md.symbol_num = 0;
+    md.parsing_state = SCHEMA_NAME;
+    exit_status = EXIT_SUCCESS;
+
+    schema_name = malloc(REL_SIZE + 1);
+    table_name = malloc(REL_SIZE + 1);
+    column_name = malloc(REL_SIZE + 1);
+    func_name = malloc(PATH_MAX + 1); /* We can get function name or path to file with a creating function query */
+
+    brace_counter = 0;
+    close_brace_counter = 0;
+    skip_reading = false;
+
+    c = ' ';
+    while (c != EOF)
+    {
+        if (skip_reading)
+        {
+            skip_reading = false;
+        }
+        else if (!isTerminal(c))
+        {
+            c = readNextSymbol(&md, fin);
+        }
+        switch (md.parsing_state)
+        {
+            case SCHEMA_NAME:
+                c = nameReader(schema_name, c, &md, fin, REL_SIZE);
+                md.parsing_state = WAIT_OPEN_BRACE;
+                memset(table_name, 0, sizeof REL_SIZE);
+                break;
+
+            case TABLE_NAME:
+                c = nameReader(table_name, c, &md, fin, REL_SIZE);
+                md.parsing_state = WAIT_OPEN_BRACE;
+                break;
+
+            case COLUMN_NAME:
+                c = nameReader(column_name, c, &md, fin, REL_SIZE);
+                md.parsing_state = WAIT_COLON;
+                break;
+
+            case FUNCTION_NAME:
+                c = nameReader(func_name, c, &md, fin, PATH_MAX);
+                extractFuncNameIfPath(func_name, masking_func_query_path);
+                setMapValue(map, getFullRelName(schema_name, table_name, column_name), func_name);
+                md.parsing_state = WAIT_COMMA;
+                break;
+
+            case WAIT_COLON:
+                if (isSpace(c))
+                    break;
+                if (c != ':')
+                {
+                    printParsingError(&md, "Waiting symbol ':'", c);
+                    exit_status = EXIT_FAILURE;
+                    goto clear_resources;
+                }
+                md.parsing_state = FUNCTION_NAME;
+                c = readNextSymbol(&md, fin);
+                skip_reading = true;
+                break;
+
+            case WAIT_OPEN_BRACE:
+                if (isSpace(c))
+                    break;
+                if (c == '}' && brace_counter > 0)
+                {
+                    md.parsing_state = WAIT_CLOSE_BRACE;
+                    break;
+                }
+                if (c != '{')
+                {
+                    printParsingError(&md, "Waiting symbol '{'", c);
+                    exit_status = EXIT_FAILURE;
+                    goto clear_resources;
+                }
+                if (table_name[0] != '\0') /* we have already read table_name */
+                {
+                    md.parsing_state = COLUMN_NAME;
+                }
+                else
+                {
+                    md.parsing_state = TABLE_NAME;
+                }
+                c = readNextSymbol(&md, fin);
+                skip_reading = true;
+                brace_counter++;
+                break;
+
+            case WAIT_CLOSE_BRACE:
+                if (isSpace(c))
+                    break;
+                if (c != '}')
+                {
+                    printParsingError(&md, "Waiting symbol '}'", c);
+                    exit_status = EXIT_FAILURE;
+                    goto clear_resources;
+                }
+                md.parsing_state = TABLE_NAME;
+                c = readNextSymbol(&md, fin);
+                brace_counter--;
+                break;
+
+            case WAIT_COMMA:
+                if (isSpace(c))
+                    break;
+                if (c == '}')
+                {
+                    c = readNextSymbol(&md, fin);
+                    skip_reading = true;
+                    close_brace_counter++;
+                    break;
+                }
+                if (c != ',' && !isTerminal(c)) /* Schema_name or Table_name */
+                {
+                    if (close_brace_counter == 1)
+                    {
+                        md.parsing_state = TABLE_NAME;
+                    }
+                    else if (close_brace_counter == 2)
+                    {
+                        md.parsing_state = SCHEMA_NAME;
+                    }
+                    else
+                    {
+                        printParsingError(&md, "Too many symbols '}'", c);
+                        exit_status = EXIT_FAILURE;
+                        goto clear_resources;
+                    }
+                    skip_reading = true;
+                    close_brace_counter = 0;
+                    break;
+                }
+                else if (c != ',')
+                {
+                    printParsingError(&md, "Waiting symbol ','", c);
+                    exit_status = EXIT_FAILURE;
+                    goto clear_resources;
+                }
+                md.parsing_state = COLUMN_NAME;
+                c = readNextSymbol(&md, fin);
+                skip_reading = true;
+                break;
+        }
+    }
+    clear_resources:
+    free(schema_name);
+    free(table_name);
+    free(column_name);
+    free(func_name);
+    return exit_status;
+}
+
+/* Creating string in format `schema_name.function name(column_name)` */
+void
+concatFunctionAndColumn(char *col_with_func, char *schema_name, char *column_name, char *function_name)
+{
+    /* Default function */
+    if (strcmp(function_name, DEFAULT_NAME)==0)
+	{
+	  strcpy(col_with_func, "_masking_function.");
+	  strcat(col_with_func, function_name);
+	}
+	/* Function name already contains schema name. If not, then add the same scheme */
+	else if (strrchr(function_name, '.') != NULL)
+    {
+        strcpy(col_with_func, function_name);
+    }
+    else
+    {
+        strcpy(col_with_func, schema_name);
+        strcat(col_with_func, ".");
+        strcat(col_with_func, function_name);
+    }
+    strcat(col_with_func, "(");
+    strcat(col_with_func, column_name);
+    strcat(col_with_func, ")");
+}
+
+/*
+ * Wrapping columns with functions
+ * schema_name.function_name(schema_name.table_name.column_name)
+ * return ' ' - if
+ */
+char *
+addFunctionToColumn(char *schema_name, char *table_name, char *column_name, MaskingMap *map)
+{
+    char *col_with_func;
+    /* Try to find for exact schema, table and column */
+    int index = getMapIndexByKey(map, getFullRelName(schema_name, table_name, column_name));
+    if (index == -1) /* If didn't find, try to find function, that used for all schemas */
+    {
+        /* Try to find for exact table and column [default.table.field] */
+        index = getMapIndexByKey(map, getFullRelName(DEFAULT_NAME, table_name, column_name));
+        if (index == -1) /* If didn't find, try to find function, that used for all schemas and all tables */
+        {
+            /* Try to find for exact column [default.default.field] */
+            index = getMapIndexByKey(map, getFullRelName(DEFAULT_NAME, DEFAULT_NAME, column_name));
+            if (index == -1) /* If didn't find, try to find function, that used for all schemas and all tables and all columns */
+            {
+                /* Try to find function that used for all fields in all schemas and tables [default.default.default] */
+                index = getMapIndexByKey(map, getFullRelName(DEFAULT_NAME, DEFAULT_NAME, DEFAULT_NAME));
+            }
+        }
+    }
+    col_with_func = malloc(COL_WITH_FUNC_SIZE + 1);
+    memset(col_with_func, 0, COL_WITH_FUNC_SIZE + 1);
+    if (index != -1)
+    {
+        char *function_name = map->data[index]->value;
+        concatFunctionAndColumn(col_with_func, schema_name, column_name, function_name);
+    }
+    return col_with_func;
+}
+
+/* Remove the first and the last symbol in func_name */
+void
+removeQuotes(char *func_name)
+{
+    char *new_func_name = malloc(PATH_MAX + 1);
+    strncpy(new_func_name, func_name + 1, strlen(func_name) - 2);
+    memset(func_name, 0, PATH_MAX);
+    strcpy(func_name, new_func_name);
+    free(new_func_name);
+}
+
+
+/* Read a word from a query */
+char *
+readWord(FILE *fin, char *word)
+{
+    char c;
+    memset(word, 0, strlen(word));
+    do
+    {
+        c = tolower(getc(fin));
+        if (isSpace(c) || c == '(') /* Space or open brace before function arguments */
+        {
+            if (word[0] == '\0') /* Spaces before the word */
+                continue;
+            else
+                break; /* Spaces after the word */
+        }
+        else
+        {
+            strncat(word, &c, 1);
+        }
+    } while (c != EOF);
+    return word;
+}
+
+/**
+ * Extract function name from query. During extracting we also check
+ * the query, but only the start of it. We expecting the pattern:
+ * `create [or replace] function {func_name}`
+ * If something is wrong we will not use function and leave
+ * the field without transforming.
+ */
+int
+extractFunctionNameFromQueryFile(char *filename, char *func_name)
+{
+    FILE *fin;
+    char *word;
+
+    memset(func_name, 0, REL_SIZE);
+    fin = NULL;
+    if (filename[0] != '\0')
+    {
+        fin = fopen(filename, "r");
+    }
+    if (fin == NULL)
+    {
+        pg_log_warning("Problem with file \'%s\"", filename);
+    }
+    else
+    {
+        word = malloc(REL_SIZE + 1);
+        memset(word, 0, REL_SIZE);
+        if (strcmp(readWord(fin, word), "create") == 0) /* reading 'create' */
+        {
+            if (strcmp(readWord(fin, word), "or") == 0) /* reading 'or' | 'function' */
+            {
+                if (strcmp(readWord(fin, word), "replace") != 0) /* reading 'replace' */
+                {
+                    pg_log_warning("Keyword 'replace' was expected, but found '%s'. Check query for creating a function '%s'.\n",
+                           word, filename);
+                    goto free_resources;
+                }
+                else
+                {
+                    readWord(fin, word); /* reading 'function' */
+                }
+            }
+        }
+        else
+        {
+            pg_log_warning("Keyword 'create' was expected, but found '%s'. Check query for creating a function '%s'.\n", word,
+                   filename);
+            goto free_resources;
+        }
+        if (strcmp(word, "function") == 0)
+        {
+            strcpy(func_name, readWord(fin, word));
+        }
+        else
+        {
+            pg_log_warning("Keyword 'function' was expected, but found '%s'. Check query for creating a function '%s'.\n", word,
+                   filename);
+            goto free_resources;
+        }
+        free_resources:
+        free(word);
+        fclose(fin);
+    }
+    return func_name[0] != '\0'; /* If we got a function name, then - return 0, else - return 1 */
+}
+
+/**
+ * If there is a path (the first symbol is a quote '"'), then store this path in masking_func_query_path
+ * and write to the first argument (func_path) name of the function from the query in the file
+ * If there is not a path - do nothing
+*/
+void
+extractFuncNameIfPath(char *func_path, SimpleStringList *masking_func_query_path)
+{
+    char *func_name;
+    if (func_path[0] == '"')
+    {
+        func_name = malloc(REL_SIZE + 1);
+        removeQuotes(func_path);
+        if (extractFunctionNameFromQueryFile(func_path, func_name) != 0) /* Read function name from query and store in func_name */
+        {
+            if (!simple_string_list_member(masking_func_query_path, func_path))
+            {
+                simple_string_list_append(masking_func_query_path, func_path);
+            }
+            strcpy(func_path, func_name); /* Store func_name in func_path to throw it to upper function */
+        }
+        free(func_name);
+    }
+}
+
+/* Read whole script from the file `filename` */
+char *
+readQueryForCreatingFunction(char *filename)
+{
+    FILE *fin;
+    char *query;
+    long fsize;
+    query = malloc(sizeof(char));
+    memset(query, 0, sizeof(char));
+    fin = fopen(filename, "r");
+    if (fin != NULL)
+    {
+        fseek(fin, 0L, SEEK_END);
+        fsize = ftell(fin);
+        fseek(fin, 0L, SEEK_SET);
+
+        query = (char *) calloc(fsize + 1, sizeof(char));
+
+        fsize = (int) fread(query, sizeof(char), fsize, fin);
+        if (fsize==0)
+        {
+            pg_log_error("File is empty `%s`", filename);
+        }
+        fclose(fin);
+    }
+    return query;
+}
+
+/**
+ * Default masking function
+ * Full masking according to the data types. Returns 'XXXX' for string data types [text, varchar, character].
+ * Returns 0 for numeric data types [int, numeric, real, smallint, bigint]
+ * Returns '1900-01-01' for date and '1900-01-01 00:00:00' for timestamp
+ */
+char *
+default_functions()
+{
+  return "CREATE SCHEMA IF NOT EXISTS _masking_function;\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in text, out text)\n"
+		 "    AS $$ SELECT 'XXXX' $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in real, out real)\n"
+		 "    AS $$ SELECT 0 $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in date, out date)\n"
+		 "    AS $$ SELECT DATE '1900-01-01' $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in timestamp, out timestamp)\n"
+		 "    AS $$ SELECT TIMESTAMP '1900-01-01 00:00:00' $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "   \n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in timestamptz, out timestamptz)\n"
+		 "    AS $$ SELECT TIMESTAMPTZ '1900-01-01 00:00:00-00' $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "   \n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in time, out time)\n"
+		 "    AS $$ SELECT TIME '00:00:00' $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "   \n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in timetz, out timetz)\n"
+		 "    AS $$ SELECT TIMETZ '00:00:00-00' $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in interval, out interval)\n"
+		 "    AS $$ SELECT INTERVAL '1 year 2 months 3 days' $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in box, out box)\n"
+		 "    AS $$ SELECT box(circle '((0,0),2.0)') $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in circle, out circle)\n"
+		 "    AS $$ SELECT circle(point '(0,0)', 0) $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in path, out path)\n"
+		 "    AS $$ SELECT '[ ( 0 , 1 ) , ( 1 , 2 ) ]'::path $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in point, out point)\n"
+		 "    AS $$ SELECT '(0, 0)'::point $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in polygon , out polygon)\n"
+		 "    AS $$ SELECT '( ( 0 , 0 ) , ( 0 , 0 ) )'::polygon $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in bytea, out bytea)\n"
+		 "    AS $$ SELECT '\\000'::bytea $$\n"
+		 "    LANGUAGE sql;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in inet, out inet)\n"
+		 "    AS $$ SELECT '0.0.0.0'::inet $$\n"
+		 "    LANGUAGE sql;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in cidr, out cidr)\n"
+		 "    AS $$ SELECT '0.0.0.0'::cidr $$\n"
+		 "    LANGUAGE sql;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in macaddr, out macaddr)\n"
+		 "    AS $$ SELECT macaddr '0:0:0:0:0:ab' $$\n"
+		 "    LANGUAGE sql;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in json, out json)\n"
+		 "    AS $$ SELECT '{\"a\":\"foo\", \"b\":\"bar\"}'::json $$\n"
+		 "    LANGUAGE sql;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in jsonb, out jsonb)\n"
+		 "    AS $$ SELECT '{\"a\":1, \"b\":2}'::jsonb $$\n"
+		 "    LANGUAGE sql;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in line, out line)\n"
+		 "    AS $$ SELECT '{1,2,3}'::line $$\n"
+		 "    LANGUAGE sql;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in lseg, out lseg)\n"
+		 "    AS $$ SELECT '((0,0),(0,0))'::lseg $$\n"
+		 "    LANGUAGE sql;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in bit, out bit)\n"
+		 "    AS $$ SELECT '0'::bit $$\n"
+		 "    LANGUAGE sql; \n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in boolean, out boolean)\n"
+		 "    AS $$ SELECT true $$\n"
+		 "    LANGUAGE sql;   \n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in money, out money)\n"
+		 "    AS $$ SELECT 0 $$\n"
+		 "    LANGUAGE sql;   \n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in pg_lsn, out pg_lsn)\n"
+		 "    AS $$ SELECT '0/0'::pg_lsn $$\n"
+		 "    LANGUAGE sql;  \n"
+		 "   \n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in uuid, out uuid)\n"
+		 "    AS $$ SELECT '00000000-0000-0000-0000-000000000000'::uuid $$\n"
+		 "    LANGUAGE sql;  \n"
+		 "   \n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in tsvector, out tsvector)\n"
+		 "    AS $$ SELECT 'a:1'::tsvector $$\n"
+		 "    LANGUAGE sql;     ";
+}
diff --git a/src/bin/pg_dump/masking.h b/src/bin/pg_dump/masking.h
new file mode 100644
index 0000000000..0634f58078
--- /dev/null
+++ b/src/bin/pg_dump/masking.h
@@ -0,0 +1,79 @@
+/*-------------------------------------------------------------------------
+ *
+ * masking.h
+ *
+ *	Data masking tool for pg_dump
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_dump/masking.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef MASKING_H
+#define MASKING_H
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+#include <limits.h>
+#include "fe_utils/simple_list.h"
+
+typedef struct _pair
+{
+  char *key;
+  char *value;
+} Pair;
+
+typedef struct MaskingMap
+{
+  Pair **data;
+  int size;
+  int capacity;
+} MaskingMap;
+
+enum
+ParsingState
+{
+  SCHEMA_NAME,
+  TABLE_NAME,
+  COLUMN_NAME,
+  FUNCTION_NAME,
+  WAIT_COLON,
+  WAIT_OPEN_BRACE,
+  WAIT_CLOSE_BRACE,
+  WAIT_COMMA
+};
+
+struct
+MaskingDebugDetails
+{
+  int line_num;
+  int symbol_num;
+  enum ParsingState parsing_state;
+};
+
+MaskingMap *newMaskingMap(void);
+void cleanMap(MaskingMap *map);
+void setMapValue(MaskingMap *map, char *key, char *value);
+void printParsingError(struct MaskingDebugDetails *md, char *message, char current_symbol);
+bool isTerminal(char c);
+bool isSpace(char c);
+char readNextSymbol(struct MaskingDebugDetails *md, FILE *fin);
+char nameReader(char *rel_name, char c, struct MaskingDebugDetails *md, FILE *fin, int size);
+int getMapIndexByKey(MaskingMap *map, char *key);
+extern int readMaskingPatternFromFile(FILE *fin, MaskingMap *map, SimpleStringList *masking_func_query_path);
+char *addFunctionToColumn(char *schema_name, char *table_name, char *column_name, MaskingMap *map);
+char *getFullRelName(char *schema_name, char *table_name, char *column_name);
+void concatFunctionAndColumn(char *col_with_func, char *schema_name, char *column_name, char *function_name);
+char *readQueryForCreatingFunction(char *filename);
+extern void extractFuncNameIfPath(char *func_path, SimpleStringList *masking_func_query_path);
+void removeQuotes(char *func_name);
+char *readWord(FILE *fin, char *word);
+int extractFunctionNameFromQueryFile(char *filename, char *func_name);
+char *default_functions(void);
+
+#endif                            /* MASKING_H */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index bd9b066e4e..cb82d63a64 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -59,8 +59,11 @@
 #include "dumputils.h"
 #include "fe_utils/option_utils.h"
 #include "fe_utils/string_utils.h"
+#include "fe_utils/query_utils.h"
+#include "fe_utils/simple_list.h"
 #include "getopt_long.h"
 #include "libpq/libpq-fs.h"
+#include "masking.h"
 #include "parallel.h"
 #include "pg_backup_db.h"
 #include "pg_backup_utils.h"
@@ -127,6 +130,8 @@ static SimpleOidList foreign_servers_include_oids = {NULL, NULL};
 
 static SimpleStringList extension_include_patterns = {NULL, NULL};
 static SimpleOidList extension_include_oids = {NULL, NULL};
+static SimpleStringList masking_func_query_path = {NULL, NULL}; /* List of path to query with masking functions, that must be created before starting dump */
+static MaskingMap *masking_map; /* Map of fields and functions for data masking */
 
 static const CatalogId nilCatalogId = {0, 0};
 
@@ -318,7 +323,8 @@ static void appendReloptionsArrayAH(PQExpBuffer buffer, const char *reloptions,
 static char *get_synchronized_snapshot(Archive *fout);
 static void setupDumpWorker(Archive *AH);
 static TableInfo *getRootTableInfo(const TableInfo *tbinfo);
-
+static int getMaskingPatternFromFile(const char *filename);
+static int createMaskingFunctions(Archive *AH, SimpleStringList *masking_func_query_path);
 
 int
 main(int argc, char **argv)
@@ -397,7 +403,8 @@ main(int argc, char **argv)
 		{"no-tablespaces", no_argument, &dopt.outputNoTablespaces, 1},
 		{"quote-all-identifiers", no_argument, &quote_all_identifiers, 1},
 		{"load-via-partition-root", no_argument, &dopt.load_via_partition_root, 1},
-		{"role", required_argument, NULL, 3},
+        {"masking", required_argument, NULL, 13},
+        {"role", required_argument, NULL, 3},
 		{"section", required_argument, NULL, 5},
 		{"serializable-deferrable", no_argument, &dopt.serializable_deferrable, 1},
 		{"snapshot", required_argument, NULL, 6},
@@ -414,7 +421,7 @@ main(int argc, char **argv)
 		{"rows-per-insert", required_argument, NULL, 10},
 		{"include-foreign-data", required_argument, NULL, 11},
 
-		{NULL, 0, NULL, 0}
+        {NULL, 0, NULL, 0}
 	};
 
 	pg_logging_init(argv[0]);
@@ -623,6 +630,12 @@ main(int argc, char **argv)
 										  optarg);
 				break;
 
+            case 13:			/* masking */
+                getMaskingPatternFromFile(optarg);
+                if (dopt.dump_inserts == 0) /* Masking works only with inserts */
+                    dopt.dump_inserts = DUMP_DEFAULT_ROWS_PER_INSERT;
+                break;
+
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -743,6 +756,10 @@ main(int argc, char **argv)
 	 * death.
 	 */
 	ConnectDatabase(fout, &dopt.cparams, false);
+    if (masking_map)
+    {
+        createMaskingFunctions(fout, &masking_func_query_path);
+    }
 	setup_connection(fout, dumpencoding, dumpsnapshot, use_role);
 
 	/*
@@ -820,7 +837,7 @@ main(int argc, char **argv)
 		dopt.outputBlobs = true;
 
 	/*
-	 * Collect role names so we can map object owner OIDs to names.
+	 * Collect role names, so we can map object owner OIDs to names.
 	 */
 	collectRoleNames(fout);
 
@@ -1035,6 +1052,7 @@ help(const char *progname)
 	printf(_("  --inserts                    dump data as INSERT commands, rather than COPY\n"));
 	printf(_("  --load-via-partition-root    load partitions via the root table\n"));
 	printf(_("  --no-comments                do not dump comments\n"));
+    printf(_("  --masking    				 data masking, help with hiding sensitive data\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
@@ -1777,7 +1795,7 @@ selectDumpableType(TypeInfo *tyinfo, Archive *fout)
  *		Mark a default ACL as to be dumped or not
  *
  * For per-schema default ACLs, dump if the schema is to be dumped.
- * Otherwise dump if we are dumping "everything".  Note that dataOnly
+ * Otherwise, dump if we are dumping "everything".  Note that dataOnly
  * and aclsSkip are checked separately.
  */
 static void
@@ -1918,7 +1936,7 @@ selectDumpableExtension(ExtensionInfo *extinfo, DumpOptions *dopt)
  *		Mark a publication object as to be dumped or not
  *
  * A publication can have schemas and tables which have schemas, but those are
- * ignored in decision making, because publications are only dumped when we are
+ * ignored in decision-making, because publications are only dumped when we are
  * dumping everything.
  */
 static void
@@ -2010,13 +2028,13 @@ dumpTableData_copy(Archive *fout, const void *dcontext)
 						  fmtQualifiedDumpable(tbinfo),
 						  tdinfo->filtercond ? tdinfo->filtercond : "");
 	}
-	else
-	{
-		appendPQExpBuffer(q, "COPY %s %s TO stdout;",
-						  fmtQualifiedDumpable(tbinfo),
-						  column_list);
-	}
-	res = ExecuteSqlQuery(fout, q->data, PGRES_COPY_OUT);
+    else
+    {
+        appendPQExpBuffer(q, "COPY %s %s TO stdout;",
+                          fmtQualifiedDumpable(tbinfo),
+                          column_list);
+    }
+    res = ExecuteSqlQuery(fout, q->data, PGRES_COPY_OUT);
 	PQclear(res);
 	destroyPQExpBuffer(clistBuf);
 
@@ -2036,9 +2054,9 @@ dumpTableData_copy(Archive *fout, const void *dcontext)
 		/* ----------
 		 * THROTTLE:
 		 *
-		 * There was considerable discussion in late July, 2000 regarding
+		 * There was considerable discussion in late July 2000 regarding
 		 * slowing down pg_dump when backing up large tables. Users with both
-		 * slow & fast (multi-processor) machines experienced performance
+		 * slow & fast (multiprocessor) machines experienced performance
 		 * degradation when doing a backup.
 		 *
 		 * Initial attempts based on sleeping for a number of ms for each ms
@@ -2061,7 +2079,7 @@ dumpTableData_copy(Archive *fout, const void *dcontext)
 		 * Most of the hard work is done in the backend, and this solution
 		 * still did not work particularly well: on slow machines, the ratio
 		 * was 50:1, and on medium paced machines, 1:1, and on fast
-		 * multi-processor machines, it had little or no effect, for reasons
+		 * multiprocessor machines, it had little or no effect, for reasons
 		 * that were unclear.
 		 *
 		 * Further discussion ensued, and the proposal was dropped.
@@ -2153,8 +2171,28 @@ dumpTableData_insert(Archive *fout, const void *dcontext)
 			appendPQExpBufferStr(q, ", ");
 		if (tbinfo->attgenerated[i])
 			appendPQExpBufferStr(q, "NULL");
+		else if (masking_map) /* If we read masking options successfully, we can use masking functions */
+		{
+			char *column_with_fun;
+			column_with_fun=addFunctionToColumn(tbinfo->dobj.namespace->dobj.name, tbinfo->dobj.name,
+												tbinfo->attnames[i], masking_map);
+			if (column_with_fun[0] == ' ')
+			{
+				pg_log_warning("Function\"%s\" was not found", column_with_fun);
+			}
+			if (column_with_fun[0] != '\0')
+			{
+				appendPQExpBufferStr(q, column_with_fun);
+			}
+			else
+			{
+				appendPQExpBufferStr(q, fmtId(tbinfo->attnames[i]));
+			}
+			free(column_with_fun);
+        }
 		else
-			appendPQExpBufferStr(q, fmtId(tbinfo->attnames[i]));
+		  appendPQExpBufferStr(q, fmtId(tbinfo->attnames[i]));
+
 		attgenerated[nfields] = tbinfo->attgenerated[i];
 		nfields++;
 	}
@@ -2292,7 +2330,7 @@ dumpTableData_insert(Archive *fout, const void *dcontext)
 							 * strtod() and friends might accept NaN, so we
 							 * can't use that to test.
 							 *
-							 * In reality we only need to defend against
+							 * In reality, we only need to defend against
 							 * infinity and NaN, so we need not get too crazy
 							 * about pattern matching here.
 							 */
@@ -5978,7 +6016,7 @@ getFuncs(Archive *fout, int *numFuncs)
 	 * otherwise we might not get creation ordering correct.
 	 *
 	 * 3. Otherwise, we normally exclude functions in pg_catalog.  However, if
-	 * they're members of extensions and we are in binary-upgrade mode then
+	 * they're members of extensions, and we are in binary-upgrade mode then
 	 * include them, since we want to dump extension members individually in
 	 * that mode.  Also, if they are used by casts or transforms then we need
 	 * to gather the information about them, though they won't be dumped if
@@ -6707,7 +6745,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 	/*
 	 * We want to perform just one query against pg_index.  However, we
 	 * mustn't try to select every row of the catalog and then sort it out on
-	 * the client side, because some of the server-side functions we need
+	 * the client side, because some server-side functions we need
 	 * would be unsafe to apply to tables we don't have lock on.  Hence, we
 	 * build an array of the OIDs of tables we care about (and now have lock
 	 * on!), and use a WHERE clause to constrain which rows are selected.
@@ -7049,7 +7087,7 @@ getExtendedStatistics(Archive *fout)
  *
  * Get info about constraints on dumpable tables.
  *
- * Currently handles foreign keys only.
+ * Currently, handles foreign keys only.
  * Unique and primary key constraints are handled with indexes,
  * while check constraints are processed in getTableAttrs().
  */
@@ -7074,7 +7112,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables)
 	/*
 	 * We want to perform just one query against pg_constraint.  However, we
 	 * mustn't try to select every row of the catalog and then sort it out on
-	 * the client side, because some of the server-side functions we need
+	 * the client side, because some server-side functions we need
 	 * would be unsafe to apply to tables we don't have lock on.  Hence, we
 	 * build an array of the OIDs of tables we care about (and now have lock
 	 * on!), and use a WHERE clause to constrain which rows are selected.
@@ -7464,7 +7502,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
 	/*
 	 * We want to perform just one query against pg_trigger.  However, we
 	 * mustn't try to select every row of the catalog and then sort it out on
-	 * the client side, because some of the server-side functions we need
+	 * the client side, because some server-side functions we need
 	 * would be unsafe to apply to tables we don't have lock on.  Hence, we
 	 * build an array of the OIDs of tables we care about (and now have lock
 	 * on!), and use a WHERE clause to constrain which rows are selected.
@@ -8084,7 +8122,7 @@ getTransforms(Archive *fout, int *numTransforms)
 /*
  * getTableAttrs -
  *	  for each interesting table, read info about its attributes
- *	  (names, types, default values, CHECK constraints, etc)
+ *	  (names, types, default values, CHECK constraints, etc.)
  *
  *	modifies tblinfo
  */
@@ -9426,7 +9464,7 @@ getAdditionalACLs(Archive *fout)
  * If a matching pg_description entry is found, it is dumped.
  *
  * Note: in some cases, such as comments for triggers and rules, the "type"
- * string really looks like, e.g., "TRIGGER name ON".  This is a bit of a hack
+ * string really looks like, e.g., "TRIGGER name ON".  This is a bit of a hack,
  * but it doesn't seem worth complicating the API for all callers to make
  * it cleaner.
  *
@@ -11138,7 +11176,7 @@ dumpCompositeType(Archive *fout, const TypeInfo *tyinfo)
 		else
 		{
 			/*
-			 * This is a dropped attribute and we're in binary_upgrade mode.
+			 * This is a dropped attribute, and we're in binary_upgrade mode.
 			 * Insert a placeholder for it in the CREATE TYPE command, and set
 			 * length and alignment with direct UPDATE to the catalogs
 			 * afterwards. See similar code in dumpTableSchema().
@@ -11747,6 +11785,7 @@ dumpFunc(Archive *fout, const FuncInfo *finfo)
 							fmtId(finfo->dobj.namespace->dobj.name),
 							funcsig);
 
+
 	if (prokind[0] == PROKIND_PROCEDURE)
 		keyword = "PROCEDURE";
 	else
@@ -14921,7 +14960,7 @@ dumpTable(Archive *fout, const TableInfo *tbinfo)
 			if (fout->remoteVersion >= 90600)
 			{
 				/*
-				 * In principle we should call acldefault('c', relowner) to
+				 * In principle, we should call acldefault('c', relowner) to
 				 * get the default ACL for a column.  However, we don't
 				 * currently store the numeric OID of the relowner in
 				 * TableInfo.  We could convert the owner name using regrole,
@@ -15284,7 +15323,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
 					/*
 					 * Not Null constraint --- suppress if inherited, except
-					 * if partition, or in binary-upgrade case where that
+					 * if partitioned, or in binary-upgrade case where that
 					 * won't work.
 					 */
 					print_notnull = (tbinfo->notnull[j] &&
@@ -15514,7 +15553,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 		 *
 		 * We process foreign and partitioned tables here, even though they
 		 * lack heap storage, because they can participate in inheritance
-		 * relationships and we want this stuff to be consistent across the
+		 * relationships, and we want this stuff to be consistent across the
 		 * inheritance tree.  We can exclude indexes, toast tables, sequences
 		 * and matviews, even though they have storage, because we don't
 		 * support altering or dropping columns in them, nor can they be part
@@ -17517,9 +17556,9 @@ getExtensionMembership(Archive *fout, ExtensionInfo extinfo[],
  *	  Due to the FKs being created at CREATE EXTENSION time and therefore before
  *	  the data is loaded, we have to work out what the best order for reloading
  *	  the data is, to avoid FK violations when the tables are restored.  This is
- *	  not perfect- we can't handle circular dependencies and if any exist they
- *	  will cause an invalid dump to be produced (though at least all of the data
- *	  is included for a user to manually restore).  This is currently documented
+ *	  not perfect - we can't handle circular dependencies and if any exist they
+ *	  will cause an invalid dump to be produced (though at least all the data
+ *	  is included for a user to manually restore).  This is currently documented,
  *	  but perhaps we can provide a better solution in the future.
  */
 void
@@ -18198,3 +18237,83 @@ appendReloptionsArrayAH(PQExpBuffer buffer, const char *reloptions,
 	if (!res)
 		pg_log_warning("could not parse %s array", "reloptions");
 }
+
+/**
+ * getMaskingPatternFromFile
+ *
+ * Parse the specified masking file with description of what we need to mask
+ * If the filename is "-" then filters will be
+ * read from STDIN rather than a file.
+ */
+int
+getMaskingPatternFromFile(const char *filename)
+{
+    FILE *fin;
+    int exit_result;
+    if (filename[0]=='\0')
+    {
+        pg_log_error("--masking filename shouldn't be empty");
+        exit_nicely(1);
+    }
+
+    fin = fopen(filename, "r");
+
+    if (fin == NULL)
+    {
+        exit_nicely(1);
+    }
+
+    masking_map = newMaskingMap();
+
+    exit_result = readMaskingPatternFromFile(fin, masking_map, &masking_func_query_path);
+    fclose(fin);
+    return exit_result;
+}
+
+/**
+ * Read paths to functions from `masking_func_query_path`,
+ * read query inside the files and run them. We checked them
+ * in function masking.c:extractFunctionNameFromQueryFile.
+ */
+int
+createMaskingFunctions(Archive *AH, SimpleStringList *masking_func_query_path)
+{
+    int exit_result;
+    PGconn *conn = GetConnection(AH);
+    char *filename;
+    char *query;
+    bool result;
+
+    exit_result=0;
+    result = false;
+    /* Read all custom masking functions and create them */
+    for (SimpleStringListCell *cell = masking_func_query_path->head; cell; cell = cell->next)
+    {
+        filename=cell->val;
+        query = readQueryForCreatingFunction(filename);
+        if (query[0]=='\0')
+        {
+            pg_log_warning("Query is empty. Check file `%s`", filename);
+            exit_result++;
+        }
+        else
+        {
+            result = executeMaintenanceCommand(conn, query, true);
+        }
+
+        if (!result)
+        {
+            pg_log_warning("Failed execution of query from file \"%s\"", filename);
+            exit_result++;
+        }
+	  	free(query);
+    }
+    /* Read all default functions and create them */
+    result = executeMaintenanceCommand(conn, default_functions(), true);
+    if (!result)
+    {
+        pg_log_warning("Problem during creating default functions from method `masking.c:default_functions`");
+        exit_result++;
+    }
+    return exit_result;
+}
diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl
index a583c8a6d2..5072057ad7 100644
--- a/src/bin/pg_dump/t/001_basic.pl
+++ b/src/bin/pg_dump/t/001_basic.pl
@@ -151,6 +151,11 @@ command_fails_like(
 	qr/\Qpg_dump: error: --rows-per-insert must be in range\E/,
 	'pg_dump: --rows-per-insert must be in range');
 
+command_fails_like(
+	[ 'pg_dump', '--masking', '' ],
+	qr/\Qpg_dump: error: --masking filename shouldn't be empty\E/,
+	'pg_dump: --masking filename shouldn\'t be empty');
+
 command_fails_like(
 	[ 'pg_restore', '--if-exists', '-f -' ],
 	qr/\Qpg_restore: error: option --if-exists requires option -c\/--clean\E/,
diff --git a/src/bin/pg_dump/t/011_dump_masking.pl b/src/bin/pg_dump/t/011_dump_masking.pl
new file mode 100644
index 0000000000..be0a1ce01f
--- /dev/null
+++ b/src/bin/pg_dump/t/011_dump_masking.pl
@@ -0,0 +1,456 @@
+# Copyright (c) 2021, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More tests => 65;
+
+my $tempdir = PostgreSQL::Test::Utils::tempdir;
+my $inputfile;
+
+my $node      = PostgreSQL::Test::Cluster->new('main');
+my $port      = $node->port;
+my $backupdir = $node->backup_dir;
+my $plainfile = "$backupdir/plain.sql";
+my $dumpfile = "$backupdir/options_plain.sql";
+my $dumpdir = "$backupdir/parallel";
+my $dumpjobfile = "$backupdir/parallel/toc.dat'";
+
+$node->init;
+$node->start;
+
+# Generate test objects
+$node->safe_psql('postgres', 'CREATE FOREIGN DATA WRAPPER dummy;');
+$node->safe_psql('postgres', 'CREATE SERVER dummyserver FOREIGN DATA WRAPPER dummy;');
+$node->safe_psql('postgres', "CREATE SCHEMA schema1;");
+$node->safe_psql('postgres', "CREATE SCHEMA schema2;");
+$node->safe_psql('postgres', "CREATE SCHEMA schema3;");
+$node->safe_psql('postgres', "CREATE SCHEMA schema4;");
+$node->safe_psql('postgres', "CREATE SCHEMA schema5;");
+$node->safe_psql('postgres', "CREATE SCHEMA schema6;");
+$node->safe_psql('postgres', "CREATE SCHEMA schema7;");
+
+$node->safe_psql('postgres', "CREATE TABLE schema1.table1(field1 varchar)");
+$node->safe_psql('postgres', "CREATE TABLE schema2.table2(field2 varchar)");
+$node->safe_psql('postgres', "CREATE TABLE schema3.table3(field3 varchar)");
+$node->safe_psql('postgres', "CREATE TABLE schema4.table4(field41 varchar, field42 varchar)");
+$node->safe_psql('postgres', "CREATE TABLE schema5.table51(field511 varchar, email varchar)");
+$node->safe_psql('postgres', "CREATE TABLE schema5.table52(email varchar, field522 varchar)");
+$node->safe_psql('postgres', "CREATE TABLE schema6.table61(email varchar, field612 varchar)");
+$node->safe_psql('postgres', "CREATE TABLE schema6.table62(field621 varchar, email varchar)");
+$node->safe_psql('postgres', "CREATE TABLE schema7.table7(field71 varchar, phone varchar)");
+$node->safe_psql('postgres', "CREATE TABLE schema7.table8(phone varchar, field82 varchar)");
+
+$node->safe_psql('postgres', "INSERT INTO schema1.table1 VALUES('value1')");
+$node->safe_psql('postgres', "INSERT INTO schema2.table2 VALUES('value2')");
+$node->safe_psql('postgres', "INSERT INTO schema3.table3 VALUES('value3')");
+$node->safe_psql('postgres', "INSERT INTO schema4.table4 VALUES('value41', 'value42')");
+$node->safe_psql('postgres', "INSERT INTO schema5.table51 VALUES('value511', 'value512')");
+$node->safe_psql('postgres', "INSERT INTO schema5.table52 VALUES('value521', 'value522')");
+$node->safe_psql('postgres', "INSERT INTO schema6.table61 VALUES('value611', 'value612')");
+$node->safe_psql('postgres', "INSERT INTO schema6.table62 VALUES('value621', 'value622')");
+$node->safe_psql('postgres', "INSERT INTO schema7.table7 VALUES('value71', 'value72')");
+$node->safe_psql('postgres', "INSERT INTO schema7.table8 VALUES('value81', 'value82')");
+
+
+#########################################
+# Use masking with custom function from file
+
+# Create masking pattern file and file with custom function
+
+open $inputfile, '>>', "$tempdir/custom_function_file.txt"
+  or die "unable to open custom_function_file for writing";
+print $inputfile "
+  CREATE FUNCTION schema3.custom_function(in text, out text)
+    AS \$\$ SELECT \$1 || ' custom' \$\$
+    LANGUAGE SQL;";
+close $inputfile;
+
+open $inputfile, '>>', "$tempdir/mask_email.sql"
+  or die "unable to open mask_email.sql for writing";
+print $inputfile "
+  CREATE FUNCTION public.mask_email(in text, out text)
+    AS \$\$ SELECT \$1 || ' email' \$\$
+    LANGUAGE SQL;";
+close $inputfile;
+
+open $inputfile, '>>', "$tempdir/mask_phone.sql"
+  or die "unable to open mask_phone.sql for writing";
+print $inputfile "
+  CREATE FUNCTION public.mask_phone(in text, out text)
+    AS \$\$ SELECT \$1 || ' phone' \$\$
+    LANGUAGE SQL;";
+close $inputfile;
+
+open $inputfile, '>>', "$tempdir/masking_file.txt"
+  or die "unable to open masking file for writing";
+print $inputfile "schema1
+                  {
+                    table1
+                    {
+                        field1: default
+                    }
+                  }
+                  schema2
+                  {
+                    table2 {
+                        not_exist_field: default
+                    }
+                  }
+                  schema3
+                  {
+                    table3
+                    {
+                        field3:  \"$tempdir/custom_function_file.txt\"
+                    }
+                  }
+                  schema4
+                  {
+                    table4
+                    {
+                        field41:  schema3.custom_function,
+                        field42:  \"$tempdir/custom_function_file.txt\"
+                    }
+                  }
+                  default
+                  {
+                    default
+                    {
+                        email: \"$tempdir/mask_email.sql\"
+                    }
+                    table7
+                    {
+                        phone: \"$tempdir/mask_phone.sql\"
+                    }
+                  }
+                  ";
+close $inputfile;
+
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $plainfile,
+		"--masking=$tempdir/masking_file.txt"
+	],
+	"1. Run masking without options");
+
+my $dump = slurp_file($plainfile);
+ok($dump =~ qr/^INSERT INTO schema1\.table1 VALUES \(\'XXXX\'\)/m, "2. [Default function] Field1 was masked");
+ok($dump =~ qr/^CREATE FUNCTION _masking_function\.\"default\"\(text\, OUT text\) RETURNS text/m, "3. [Default function] Default functions were created");
+ok($dump =~ qr/^INSERT INTO schema2\.table2 VALUES \(\'value2\'\)/m, "4. Field2 was not masked");
+ok($dump =~ qr/^CREATE FUNCTION schema3\.custom_function\(text\, OUT text\) RETURNS text/m, "5. [Function from file] Custom function was created");
+ok($dump =~ qr/^INSERT INTO schema3\.table3 VALUES \(\'value3 custom\'\)/m, "6. [Function from file] Field3 was masked");
+ok($dump =~ qr/^INSERT INTO schema4\.table4 VALUES \(\'value41 custom\'\, \'value42 custom\'\)/m, "7. [Function from file] Already created custom function");
+ok($dump =~ qr/^INSERT INTO schema5\.table51 VALUES \(\'value511\'\, \'value512 email\'\)/m, "8. [Default schema and table] Masked only field with name `email`");
+ok($dump =~ qr/^INSERT INTO schema5\.table52 VALUES \(\'value521 email\'\, \'value522\'\)/m, "9. [Default schema and table] Masked only field with name `email`");
+ok($dump =~ qr/^INSERT INTO schema6\.table61 VALUES \(\'value611 email\'\, \'value612\'\)/m, "10. [Default schema and table] Masked only field with name `email`");
+ok($dump =~ qr/^INSERT INTO schema6\.table62 VALUES \(\'value621\'\, \'value622 email\'\)/m, "11. [Default schema and table] Masked only field with name `email`");
+ok($dump =~ qr/^INSERT INTO schema7\.table7 VALUES \(\'value71\'\, \'value72 phone\'\)/m, "12. [Default schema] Masked only field with name `phone` from table7");
+ok($dump =~ qr/^INSERT INTO schema7\.table8 VALUES \(\'value81\'\, \'value82\'\)/m, "13. [Default schema] Masked only field with name `phone` from table7");
+
+#########################################
+# Run masking with other options
+#########################################
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		'--data-only'
+	],
+	"14. Run masking with option --data-only");
+ok(slurp_file($dumpfile) =~ qr/^INSERT INTO schema7\.table7 VALUES \(\'value71\'\, \'value72 phone\'\)/m, "15. Check dump after running masking with option --data-only");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		'--clean'
+	],
+	"16. Run masking with option --clean");
+ok(slurp_file($dumpfile) =~ qr/^INSERT INTO schema7\.table7 VALUES \(\'value71\'\, \'value72 phone\'\)/m, "17. Check dump after running masking with option --clean");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		'--create'
+	],
+	"18. Run masking with option --create");
+ok(slurp_file($dumpfile) =~ qr/^INSERT INTO schema7\.table7 VALUES \(\'value71\'\, \'value72 phone\'\)/m, "19. Check dump after running masking with option --create");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		"--encoding=UTF-8"
+	],
+	"20. Run masking with option --encoding");
+ok(slurp_file($dumpfile) =~ qr/^INSERT INTO schema7\.table7 VALUES \(\'value71\'\, \'value72 phone\'\)/m, "21. Check dump after running masking with option --encoding");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpdir,
+		"--masking=$tempdir/masking_file.txt",
+		"--format=directory",
+		"--jobs=2"
+	],
+	"22. Run masking with option --jobs");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		"--schema=schema6"
+	],
+	"23. Run masking with option --schema");
+ok(slurp_file($dumpfile) !~ qr/^INSERT INTO schema7\.table7 VALUES \(\'value71\'\, \'value72 phone\'\)/m, "24. Check dump after running masking with option --schema");
+ok(slurp_file($dumpfile) =~ qr/^INSERT INTO schema6\.table62 VALUES \(\'value621\'\, \'value622 email\'\)/m, "25. Check dump after running masking with option --schema");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		"--table=schema6.table62"
+	],
+	"26. Run masking with option --table");
+ok(slurp_file($dumpfile) =~ qr/^INSERT INTO schema6\.table62 VALUES \(\'value621\'\, \'value622 email\'\)/m, "27. Check dump after running masking with option --table");
+ok(slurp_file($dumpfile) !~ qr/^INSERT INTO schema6\.table61 VALUES \(\'value611 email\'\, \'value612\'\)/m, "28. Check dump after running masking with option --table");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		"--exclude-table=schema6.table62"
+	],
+	"29. Run masking with option --exclude-table");
+ok(slurp_file($dumpfile) !~ qr/^INSERT INTO schema6\.table62 VALUES \(\'value621\'\, \'value622\'\)/m, "30. Check dump after running masking with option --exclude-table");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		"--verbose"
+	],
+	"31. Run masking with option --verbose");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		"--no-privileges"
+	],
+	"32. Run masking with option --no-privileges");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		"--binary-upgrade"
+	],
+	"33. Run masking with option --binary-upgrade");
+ok(slurp_file($dumpfile) =~ qr/^INSERT INTO schema7\.table7 VALUES \(\'value71\'\, \'value72 phone\'\)/m,
+"34. Check dump after running masking with option --binary-upgrade");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		"--column-inserts"
+	],
+	"35. Run masking with option --column-inserts");
+ok(slurp_file($dumpfile) =~ qr/^INSERT INTO schema7\.table7 \(field71\, mask_phone\) VALUES \(\'value71\'\, \'value72 phone\'\)/m,
+"36. Check dump after running masking with option --column-inserts");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		"--disable-dollar-quoting"
+	],
+	"37. Run masking with option --column-inserts");
+ok(slurp_file($dumpfile) =~ qr/^INSERT INTO schema7\.table7 VALUES \(\'value71\'\, \'value72 phone\'\)/m,
+"38. Check dump after running masking with option --disable-dollar-quoting");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--disable-triggers",
+		"--masking=$tempdir/masking_file.txt"
+	],
+	"39. Run masking with option --disable-triggers");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+ 		"--masking=$tempdir/masking_file.txt",
+		"--if-exists",
+		"--clean",
+	],
+	"40. Run masking with option --if-exists");
+ok(slurp_file($dumpfile) =~ qr/^INSERT INTO schema6\.table62 VALUES \(\'value621\'\, \'value622 email\'\)/m,
+"41. Check dump after running masking with option --if-exists");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+ 		"--masking=$tempdir/masking_file.txt",
+		"--inserts"
+	],
+	"42. Run masking with option --inserts");
+ok(slurp_file($dumpfile) =~ qr/^INSERT INTO schema6\.table62 VALUES \(\'value621\'\, \'value622 email\'\)/m,
+"43. Check dump after running masking with option --inserts");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+ 		"--masking=$tempdir/masking_file.txt",
+		"--load-via-partition-root"
+	],
+	"44. Run masking with option --load-via-partition-root");
+ok(slurp_file($dumpfile) =~ qr/^INSERT INTO schema6\.table62 VALUES \(\'value621\'\, \'value622 email\'\)/m,
+"45. Check dump after running masking with option --load-via-partition-root");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+ 		"--masking=$tempdir/masking_file.txt",
+		"--lock-wait-timeout=10"
+	],
+	"46. Run masking with option --lock-wait-timeout");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+ 		"--masking=$tempdir/masking_file.txt",
+		"--no-comments",
+		"--no-publications",
+		"--no-security-labels",
+		"--no-subscriptions",
+		"--no-sync",
+		"--no-tablespaces",
+		"--no-toast-compression",
+		"--no-unlogged-table-data"
+	],
+	"47. Run masking with skip options");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+ 		"--masking=$tempdir/masking_file.txt",
+		"--quote-all-identifiers"
+	],
+	"48. Run masking with option --quote-all-identifiers");
+ok(slurp_file($dumpfile) =~ qr/^INSERT INTO \"schema7\"\.\"table7\" VALUES \(\'value71\'\, \'value72 phone\'\)/m,
+"49. Check dump after running masking with option --quote-all-identifiers");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+ 		"--masking=$tempdir/masking_file.txt",
+		"--rows-per-insert=10"
+	],
+	"50. Run masking with option --rows-per-insert");
+ok(slurp_file($dumpfile) =~ qr/^	\(\'value71\'\, \'value72 phone\'\)/m,
+"51. Check dump after running masking with option --rows-per-insert");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+ 		"--masking=$tempdir/masking_file.txt",
+		"--section=pre-data"
+	],
+	"52. Run masking with option --section");
+ok(slurp_file($dumpfile) =~ qr/^CREATE FUNCTION public\.mask_phone\(text\, OUT text\) RETURNS text/m,
+"53. Check dump after running masking with option --section");
+ok(slurp_file($dumpfile) !~ qr/^INSERT INTO schema7\.table7 VALUES \(\'value71\'\, \'value72 phone\'\)/m,
+"54. Check dump after running masking with option --section");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+ 		"--masking=$tempdir/masking_file.txt",
+		"--serializable-deferrable"
+	],
+	"55. Run masking with option --serializable-deferrable");
+ok(slurp_file($dumpfile) =~ qr/^INSERT INTO schema7\.table7 VALUES \(\'value71\'\, \'value72 phone\'\)/m,
+"56. Check dump after running masking with option --serializable-deferrable");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		'--strict-names', 'postgres'
+	],
+	"57. Run masking with option --strict names");
+ok(slurp_file($dumpfile) =~ qr/^INSERT INTO schema7\.table7 VALUES \(\'value71\'\, \'value72 phone\'\)/m,
+"58. Check dump after running masking with option --serializable-deferrable");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		'--use-set-session-authorization'
+	],
+	"59. Run masking with option --use-set-session-authorization");
+ok(slurp_file($dumpfile) =~ qr/^INSERT INTO schema7\.table7 VALUES \(\'value71\'\, \'value72 phone\'\)/m,
+"60. Check dump after running masking with option --use-set-session-authorization");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		"--compress=9"
+	],
+	"61. Run masking with option --compress");
+
+open $inputfile, '>>', "$tempdir/drop_table_script.sql"
+  or die "unable to open mask_phone.sql for writing";
+print $inputfile "DROP TABLE schema1.table1;";
+close $inputfile;
+
+open $inputfile, '>>', "$tempdir/masking_file_2.txt"
+  or die "unable to open masking file for writing";
+print $inputfile "schema1
+                  {
+                    table1
+                    {
+                        field1:  \"$tempdir/drop_table_script.sql\"
+                    }
+                  }
+                  ";
+close $inputfile;
+
+
+command_fails_like(
+	[
+		'pg_dump', '-p', $port, '-f', $plainfile,
+		"--masking=$tempdir/masking_file_2.txt"
+	],
+	qr/pg_dump: warning: Keyword 'create' was expected, but found 'drop'. Check query for creating a function/,
+	"62, 63. Run masking with wrong query");
+
+open $inputfile, '>>', "$tempdir/masking_file_2.txt"
+  or die "unable to open masking file for writing";
+print $inputfile "schema1
+                  {
+                    table1
+                    {
+                        field,
+                    }
+                  }
+                  ";
+close $inputfile;
+
+command_fails_like(
+	[
+		'pg_dump', '-p', $port, '-f', $plainfile,
+		"--masking=$tempdir/masking_file_2.txt"
+	],
+	qr/\Qpg_dump: error: Error position (symbol ','): line: 12 pos: 31. Waiting symbol ':'\E/,
+	"64, 65. Run masking with wrong masking file");
+
+done_testing();
+
diff --git a/src/include/fe_utils/simple_list.h b/src/include/fe_utils/simple_list.h
index 757fd6ac5a..848858c817 100644
--- a/src/include/fe_utils/simple_list.h
+++ b/src/include/fe_utils/simple_list.h
@@ -16,6 +16,8 @@
  */
 #ifndef SIMPLE_LIST_H
 #define SIMPLE_LIST_H
+#include "postgres_ext.h"
+#include "c.h"
 
 typedef struct SimpleOidListCell
 {
diff --git a/src/tools/msvc/Mkvcbuild.pm b/src/tools/msvc/Mkvcbuild.pm
index 83a3e40425..e6755b9b15 100644
--- a/src/tools/msvc/Mkvcbuild.pm
+++ b/src/tools/msvc/Mkvcbuild.pm
@@ -457,6 +457,7 @@ sub mkvcbuild
 	$pgrestore->{name} = 'pg_restore';
 	$pgrestore->AddIncludeDir('src/backend');
 	$pgrestore->AddFile('src/bin/pg_dump/pg_restore.c');
+	$pgdumpall->AddFile('src/bin/pg_dump/masking.c');
 	$pgrestore->AddLibrary('ws2_32.lib');
 
 	my $zic = $solution->AddProject('zic', 'exe', 'utils');
-- 
2.25.1

In reply to: Виктория Шепард (#5)
1 attachment(s)
Re: Re[2]: Possible solution for masking chosen columns when using pg_dump

Hi,

Thank you, Oleg Tselebrovskiy, for your valuable review, here are the fixes

Best regards,
Viktoria Shepard

ср, 12 окт. 2022 г. в 12:19, Виктория Шепард <we.viktory@gmail.com>:

Show quoted text

Hi,

Here is an idea of how to read masking options from a file. Please, take a
look.

пн, 10 окт. 2022 г. в 14:54, Олег Целебровский <oleg_tselebrovskiy@mail.ru

:

Hi,

I applied most of suggestions: used separate files for most of added
code, fixed typos/mistakes, got rid of that pesky TODO that was already
implemented, just not deleted.

Added tests (and functionality) for cases when you need to mask columns
in tables with the same name in different schemas. If schema is not
specified, then columns in all tables with specified name are masked
(Example - pg_dump -t ‘t0’ --mask-columns id --mask-function mask_int will
mask all ids in all tables with names ‘t0’ in all existing schemas).

Wrote comments for all ‘magic numbers’

About that

- Also it can be hard to use a lot of different functions for different

fields, maybe it would be better to set up functions in a file.

I agree with that, but I know about at least 2 other patches (both are
WIP, but still) that are interacting with reading command-line options from
file. And if everyone will write their own version of reading command-line
options from file, it will quickly get confusing.

A solution to that problem is another patch that will put all options
from file (one file for any possible options, from existing to future ones)
into **argv in main, so that pg_dump can process them as if they came form
command line.

Пятница, 7 октября 2022, 8:01 +07:00 от Виктория Шепард <
we.viktory@gmail.com>:

Hi,
I took a look, here are several suggestions for improvement:

- Masking is not a main functionality of pg_dump and it is better to
write most of the connected things in a separate file like parallel.c or
dumputils.c. This will help slow down the growth of an already huge pg_dump
file.

- Also it can be hard to use a lot of different functions for different
fields, maybe it would be better to set up functions in a file.

- How will it work for the same field and tables in the different
schemas? Can we set up the exact schema for the field?

- misspelling in a word

/*
* Add all columns and funcions to list of MaskColumnInfo structures,
*/

- Why did you use 256 here?

char* table = (char*) pg_malloc(256 * sizeof(char));

Also for malloc you need malloc on 1 symbol more because you have to
store '\0' symbol.

- Instead of addFuncToDatabase you can run your query using something
already defined from fe_utils/query_utils.c. And It will be better to set
up a connection only once and create all functions. Establishing a
connection is a resource-intensive procedure. There are a lot of magic
numbers, better to leave some comments explaining why there are 64 or 512.

- It seems that you are not using temp_string

char *temp_string = (char*)malloc(256 * sizeof(char));

- Grammar issues

/*
* mask_column_info_list contains info about every to-be-masked column:
* its name, a name its table (if nothing is specified - mask all columns

with this name),

* name of masking function and name of schema containing this function

(public if not specified)

*/

the name of its table

пн, 3 окт. 2022 г. в 20:45, Julien Rouhaud <rjuju123@gmail.com
<//e.mail.ru/compose/?mailto=mailto%3arjuju123@gmail.com>>:

Hi,

On Mon, Oct 03, 2022 at 06:30:17PM +0300, Олег Целебровский wrote:

Hello, here's my take on masking data when using pg_dump

The main idea is using PostgreSQL functions to replace data during a

SELECT.

When table data is dumped SELECT a,b,c,d ... from ... query is

generated, the columns that are marked for masking are replaced with result
of functions on those columns

Example: columns name, count are to be masked, so the query will look

as such: SELECT id, mask_text(name), mask_int(count), date from ...

So about the interface: I added 2 more command-line options:

--mask-columns, which specifies what columns from what tables will be

masked

usage example:
--mask-columns "t1.name, t2.description" - both columns

will be masked with the same corresponding function

or --mask-columns name - ALL columns with name "name" from

all dumped tables will be masked with correspoding function

--mask-function, which specifies what functions will mask data
usage example:
--mask-function mask_int - corresponding columns will be

masked with function named "mask_int" from default schema (public)

or --mask-function my_schema.mask_varchar - same as above

but with specified schema where the function is stored

or --mask-function somedir/filename - the function is

"defined" here - more on the structure below

FTR I wrote an extension POC [1] last weekend that does that but on the
backend
side. The main advantage is that it's working with any existing versions
of
pg_dump (or any client relying on COPY or even plain interactive SQL
statements), and that the DBA can force a dedicated role to only get a
masked
dump, even if they forgot to ask for it.

I only had a quick look at your patch but it seems that you left some
todo in
russian, which isn't helpful at least to me.

[1] https://github.com/rjuju/pg_anonymize

Attachments:

0001-C4a-pg-dump-masking-option.patchtext/x-patch; charset=US-ASCII; name=0001-C4a-pg-dump-masking-option.patchDownload
From a10183edfaba64cdcb814c6c0a22834620e9b49b Mon Sep 17 00:00:00 2001
From: Victoria Shepard <5807469+demonolock@users.noreply.github.com>
Date: Mon, 24 Oct 2022 02:11:35 +0500
Subject: [PATCH] C4a pg dump masking option

---
 doc/src/sgml/ref/pg_dump.sgml         |  56 ++
 src/bin/pg_dump/Makefile              |   5 +-
 src/bin/pg_dump/masking.c             | 854 ++++++++++++++++++++++++++
 src/bin/pg_dump/masking.h             |  70 +++
 src/bin/pg_dump/pg_dump.c             | 122 +++-
 src/bin/pg_dump/t/011_dump_masking.pl | 540 ++++++++++++++++
 src/tools/msvc/Mkvcbuild.pm           |   1 +
 7 files changed, 1630 insertions(+), 18 deletions(-)
 create mode 100644 src/bin/pg_dump/masking.c
 create mode 100644 src/bin/pg_dump/masking.h
 create mode 100644 src/bin/pg_dump/t/011_dump_masking.pl

diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 8b9d9f4cad..d4045d8ca2 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -881,6 +881,62 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--masking=<replaceable class="parameter">filename</replaceable></option></term>
+      <listitem>
+       <para>
+        Specify a filename from which to read masking patterns for using masking functions
+        to fields. You have ability to use user-defined functions from file, functions that
+        already defined in the cluster and default functions.
+
+        Default masking functions - are full masking according to the data types.
+        Returns 'XXXX' for string data types [text, varchar, character].
+        Returns 0 for numeric data types [int, numeric, real, smallint, bigint].
+        Returns '1900-01-01' for date and '1900-01-01 00:00:00' for timestamp.
+
+        Here is an example of masking pattern file:
+
+        masking_file.txt
+        <programlisting>
+        Schema1 {
+            Table1 {
+                  field11 : function_name11
+                , field12 : function_name12
+                , field13 : function_name13
+            }
+
+            Table2 {
+                  field21 : function_name21
+                , field22 : function_name22
+                /* This function will be created by script from the path 'path_to_file_with_function'
+                   and used for 'field23' */
+                , field23 : "path_to_file_with_function"
+            }
+        }
+
+        /* Functions inside this block will be used for all schemes */
+        default {
+            /* Functions inside this block will be used for all tables */
+            default {
+                  /* Function 'for_all_fields' will be used for all fields
+                     did not covered by exact functions */
+                  default: for_all_fields,
+                  field1: value1,
+                  /* Using default masking function for field2 */
+                  field2: default
+            }
+
+            /* Functions inside this block will be used for tables with name 'Table'
+               in the all schemes */
+            Table {
+                   field : function_name
+            }
+        }
+        </programlisting>
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-comments</option></term>
       <listitem>
diff --git a/src/bin/pg_dump/Makefile b/src/bin/pg_dump/Makefile
index 9dc5a784dd..266ec4a417 100644
--- a/src/bin/pg_dump/Makefile
+++ b/src/bin/pg_dump/Makefile
@@ -26,6 +26,7 @@ OBJS = \
 	$(WIN32RES) \
 	compress_io.o \
 	dumputils.o \
+	masking.o \
 	parallel.o \
 	pg_backup_archiver.o \
 	pg_backup_custom.o \
@@ -43,8 +44,8 @@ pg_dump: pg_dump.o common.o pg_dump_sort.o $(OBJS) | submake-libpq submake-libpg
 pg_restore: pg_restore.o $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) pg_restore.o $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
-pg_dumpall: pg_dumpall.o dumputils.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
-	$(CC) $(CFLAGS) pg_dumpall.o dumputils.o $(WIN32RES) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+pg_dumpall: pg_dumpall.o dumputils.o masking.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+	$(CC) $(CFLAGS) pg_dumpall.o dumputils.o masking.o $(WIN32RES) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
 install: all installdirs
 	$(INSTALL_PROGRAM) pg_dump$(X) '$(DESTDIR)$(bindir)'/pg_dump$(X)
diff --git a/src/bin/pg_dump/masking.c b/src/bin/pg_dump/masking.c
new file mode 100644
index 0000000000..8ad084e2b6
--- /dev/null
+++ b/src/bin/pg_dump/masking.c
@@ -0,0 +1,854 @@
+/*-------------------------------------------------------------------------
+ *
+ * masking.c
+ *
+ * Data masking tool for pg_dump
+ *
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/bin/pg_dump/masking.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include <ctype.h>
+#include "masking.h"
+#include "common/logging.h"
+
+#define REL_SIZE 65 /* Length of relation name - 63 bytes (symbols) + addition symbol for correct work with option --quote-all-identifiers*/
+#define DEFAULT_NAME "default"
+#define COL_WITH_FUNC_SIZE 3 * REL_SIZE + 3 /* schema_name.function name + '(' + column_name + ') */
+
+char REL_SEP = '.'; /* Relation separator */
+
+void concatFunctionAndColumn(char *col_with_func, char *schema_name, char *column_name, char *function_name);
+extern void extractFuncNameIfPath(char *func_path, SimpleStringList *masking_func_query_path);
+int extractFunctionNameFromQueryFile(char *filename, char *func_name);
+char *getFullRelName(char *schema_name, char *table_name, char *column_name);
+int getMapIndexByKey(MaskingMap *map, char *key);
+bool isTerminal(char c);
+bool isSpace(char c);
+void printParsingError(struct MaskingDebugDetails *md, char *message, char current_symbol);
+char readName(char *rel_name, char c, struct MaskingDebugDetails *md, FILE *fin, int size);
+char readNextSymbol(struct MaskingDebugDetails *md, FILE *fin);
+void removeQuotes(char *func_name);
+char *readWord(FILE *fin, char *word);
+void setMapValue(MaskingMap *map, char *key, char *value);
+char skipMultiLineComment(char c, struct MaskingDebugDetails *md, FILE *fin);
+char skipOneLineComment(char c, struct MaskingDebugDetails *md, FILE *fin);
+
+/* Initialise masking map */
+MaskingMap *
+newMaskingMap()
+{
+    MaskingMap *map = malloc(sizeof(MaskingMap));
+    map->size = 0;
+    map->capacity = 8;
+    map->data = malloc(sizeof(Pair) * map->capacity);
+    memset(map->data, 0, sizeof(Pair) * map->capacity);
+    return map;
+}
+
+int
+getMapIndexByKey(MaskingMap *map, char *key)
+{
+    int index = 0;
+    while (map->data[index] != NULL)
+    {
+        if (strcmp(map->data[index]->key, key) == 0)
+        {
+            return index;
+        }
+        index++;
+    }
+    return -1;
+}
+
+/*
+ * Add value to map or rewrite, if key already exists
+ */
+void
+setMapValue(MaskingMap *map, char *key, char *value)
+{
+    if (key != NULL)
+    {
+        int index = getMapIndexByKey(map, key);
+        if (index != -1) /* Already have key in map */
+        {
+            free(map->data[index]->value);
+            map->data[index]->value = malloc(strlen(value) + 1);
+            strcpy(map->data[index]->value, value);
+        }
+        else
+        {
+            Pair *pair = malloc(sizeof(Pair));
+            pair->key = malloc(strlen(key) + 1);
+            pair->value = malloc(strlen(value) + 1);
+            memset(pair->key, 0, strlen(key));
+            memset(pair->value, 0, strlen(value));
+            strcpy(pair->key, key);
+            strcpy(pair->value, value);
+
+            map->data[map->size] = malloc(sizeof(Pair));
+            *map->data[map->size] = *pair;
+            map->size++;
+            free(pair);
+        }
+        if (map->size == map->capacity) /* Increase capacity */
+        {
+            map->capacity *= 1.5;
+            map->data = realloc(map->data, sizeof(Pair) * map->capacity);
+        }
+    }
+    free(key);
+}
+
+void
+printParsingError(struct MaskingDebugDetails *md, char *message, char current_symbol)
+{
+    pg_log_error("Error position (symbol '%c'): line: %d pos: %d. %s\n", current_symbol, md->line_num, md->symbol_num,
+           message);
+}
+
+bool
+isTerminal(char c)
+{
+    return c == ':' || c == ',' || c == '{' || c == '}' || c == EOF;
+}
+
+bool
+isSpace(char c)
+{
+    return c == ' ' || c == '\t' || c == '\n' || c == EOF;
+}
+
+/* Read to the end of a comment */
+char
+skipOneLineComment(char c, struct MaskingDebugDetails *md, FILE *fin)
+{
+	while (md->is_comment)
+	{
+		c = readNextSymbol(md, fin);
+		switch (c)
+		{
+			case '\n':
+				md->is_comment = false; /* End of a one line comment */
+				c = readNextSymbol(md, fin);
+				break;
+			case EOF:
+			  	return c; /* Handling `EOF` outside the function */
+			default:
+			  	continue;
+		}
+	}
+	return c;
+}
+
+/* Read to the end of a comment */
+char
+skipMultiLineComment(char c, struct MaskingDebugDetails *md, FILE *fin)
+{
+	while (md->is_comment)
+	{
+		c = readNextSymbol(md, fin);
+		switch (c)
+		{
+			case '*':
+				c = readNextSymbol(md, fin);
+				if (c == '/')
+				{
+					md->is_comment = false; /* End of a multi line comment */
+					c = readNextSymbol(md, fin);
+					break;
+				}
+				continue;
+			case EOF:
+			  return c; /* Handling `EOF` outside the function */
+			default:
+			  continue;
+		}
+	}
+  return c;
+}
+
+/*
+ * Read symbol and change place of cursor in MaskingDebugDetails
+ * md->line_num - increasing when we meet '\n'
+ * md->symbol_num - increasing after reading any symbol and reset
+ * when we meet '\n'
+ */
+char
+readNextSymbol(struct MaskingDebugDetails *md, FILE *fin)
+{
+    char c = fgetc(fin);
+  	/* Count lines and columns */
+    if (c == '\n')
+    {
+        md->line_num++;
+        md->symbol_num = 1;
+    }
+    else
+    {
+        md->symbol_num++;
+    }
+  	/* Skip comment */
+  	if (c == '/' && !md->is_comment) /* First slash */
+	{
+		char next_c = fgetc(fin);
+		fseek(fin, -1, SEEK_CUR); /* Returning on 1 symbol back for correct line counting */
+		if (next_c == '/')
+		{
+			md->is_comment = true;
+			c = skipOneLineComment(c, md, fin);
+		}
+		else if (next_c == '*')
+		{
+			md->is_comment = true;
+			c = skipMultiLineComment(c, md, fin);
+		}
+	}
+    return c;
+}
+
+/* Read relation name */
+char
+readName(char *rel_name, char c, struct MaskingDebugDetails *md, FILE *fin, int size)
+{
+  	bool word_started = false;
+  	bool word_finished = false;
+    memset(rel_name, 0,  size);
+    while (!isTerminal(c))
+    {
+        switch (c)
+        {
+            case ' ':
+            case '\t':
+            case '\n':
+				if (word_started && !word_finished)
+				{
+				  	word_finished = true;
+				}
+				break; /* Skip space symbols */
+            case EOF:
+                return c; /* Handling `EOF` outside the function */
+
+            default:
+				if (word_finished)
+				{
+				  	printParsingError(md, "Syntax error. Relation name can't contain space symbols.", c);
+				  	return c;
+				}
+				word_started = true;
+				strncat(rel_name, &c, 1);
+				break;
+        }
+        c = readNextSymbol(md, fin);
+    }
+    return c;
+}
+
+/* Concat schema name, table name and column name */
+char *
+getFullRelName(char *schema_name, char *table_name, char *column_name)
+{
+    /* Schema.Table.Column */
+    return psprintf("%s%c%s%c%s", schema_name, REL_SEP, table_name, REL_SEP, column_name);
+}
+
+/**
+ * Parsing file with masking pattern
+ * ------------------------------------
+ * Schema1
+ * {
+ *      Table1
+ *      {
+ *            column11 : function_name11
+ *          , column12 : function_name12
+ *          , column13 : function_name13
+ *      }
+ *
+ *      Table2
+ *      {
+ *            column21 : function_name21
+ *          , column22 : function_name22
+ *          , column23 : "path_to_file_with_function/masking.sql"
+ *      // Function 'masking.sql' will be stored in `masking_func_query_path` list, and it will be
+ *      // created by script from the path 'path_to_file_with_function'. See more `pg_dump.c:createMaskingFunctions`
+ *      }
+ *  }
+ *
+ *
+ * default // Functions inside this block will be used for all schemas
+ * {
+ *
+ *      default // Functions inside this block will be used for all tables
+ *      {
+ *          default: for_all_columns, // This function will be used for all columns did not covered by exact functions
+ *          column1: value1,
+ *          column2: value2
+ *      }
+ *
+ *      Table // Functions inside this block will be used for tables with name 'Table' in the all schemas
+ *      {
+ *          column : function_name
+ *      }
+ * }
+ */
+int
+readMaskingPatternFromFile(FILE *fin, MaskingMap *map, SimpleStringList *masking_func_query_path)
+{
+    int exit_status;
+    int brace_counter;
+    int close_brace_counter;
+    char *schema_name;
+    char *table_name;
+    char *column_name;
+    char *func_name;
+    bool skip_reading;
+    char c;
+
+    struct MaskingDebugDetails md;
+    md.line_num = 1;
+    md.symbol_num = 0;
+  	md.is_comment = false;
+    md.parsing_state = SCHEMA_NAME;
+    exit_status = EXIT_SUCCESS;
+
+    schema_name = malloc(REL_SIZE);
+    table_name = malloc(REL_SIZE);
+    column_name = malloc(REL_SIZE);
+    func_name = malloc(PATH_MAX + 1); /* We can get function name or path to file with a creating function query */
+
+    brace_counter = 0;
+    close_brace_counter = 0;
+    skip_reading = false;
+
+    c = ' ';
+    while (c != EOF)
+    {
+        if (skip_reading)
+        {
+            skip_reading = false;
+        }
+        else if (!isTerminal(c))
+        {
+            c = readNextSymbol(&md, fin);
+        }
+        switch (md.parsing_state)
+        {
+            case SCHEMA_NAME:
+                c = readName(schema_name, c, &md, fin, REL_SIZE);
+                md.parsing_state = WAIT_OPEN_BRACE;
+                memset(table_name, 0, REL_SIZE);
+                break;
+
+            case TABLE_NAME:
+                c = readName(table_name, c, &md, fin, REL_SIZE);
+                md.parsing_state = WAIT_OPEN_BRACE;
+                break;
+
+            case COLUMN_NAME:
+                c = readName(column_name, c, &md, fin, REL_SIZE);
+                md.parsing_state = WAIT_COLON;
+                break;
+
+            case FUNCTION_NAME:
+                c = readName(func_name, c, &md, fin, PATH_MAX);
+                extractFuncNameIfPath(func_name, masking_func_query_path);
+                setMapValue(map, getFullRelName(schema_name, table_name, column_name), func_name);
+                md.parsing_state = WAIT_COMMA;
+                break;
+
+            case WAIT_COLON:
+                if (isSpace(c))
+                    break;
+                if (c != ':')
+                {
+                    printParsingError(&md, "Waiting symbol ':'", c);
+                    exit_status = EXIT_FAILURE;
+                    goto clear_resources;
+                }
+                md.parsing_state = FUNCTION_NAME;
+                c = readNextSymbol(&md, fin);
+                skip_reading = true;
+                break;
+
+            case WAIT_OPEN_BRACE:
+                if (isSpace(c))
+                    break;
+                if (c == '}' && brace_counter > 0)
+                {
+                    md.parsing_state = WAIT_CLOSE_BRACE;
+                    break;
+                }
+                if (c != '{')
+                {
+                    printParsingError(&md, "Waiting symbol '{'", c);
+                    exit_status = EXIT_FAILURE;
+                    goto clear_resources;
+                }
+                if (table_name[0] != '\0') /* we have already read table_name */
+                {
+                    md.parsing_state = COLUMN_NAME;
+                }
+                else
+                {
+                    md.parsing_state = TABLE_NAME;
+                }
+                c = readNextSymbol(&md, fin);
+                skip_reading = true;
+                brace_counter++;
+                break;
+
+            case WAIT_CLOSE_BRACE:
+                if (isSpace(c))
+                    break;
+                if (c != '}')
+                {
+                    printParsingError(&md, "Waiting symbol '}'", c);
+                    exit_status = EXIT_FAILURE;
+                    goto clear_resources;
+                }
+                md.parsing_state = TABLE_NAME;
+                c = readNextSymbol(&md, fin);
+                brace_counter--;
+                break;
+
+            case WAIT_COMMA:
+                if (isSpace(c))
+                    break;
+                if (c == '}')
+                {
+                    c = readNextSymbol(&md, fin);
+                    skip_reading = true;
+                    close_brace_counter++;
+                    break;
+                }
+                if (c != ',' && !isTerminal(c)) /* Schema_name or Table_name */
+                {
+                    if (close_brace_counter == 1)
+                    {
+                        md.parsing_state = TABLE_NAME;
+                    }
+                    else if (close_brace_counter == 2)
+                    {
+                        md.parsing_state = SCHEMA_NAME;
+                    }
+                    else
+                    {
+                        printParsingError(&md, "Too many symbols '}'", c);
+                        exit_status = EXIT_FAILURE;
+                        goto clear_resources;
+                    }
+                    skip_reading = true;
+                    close_brace_counter = 0;
+                    break;
+                }
+                else if (c != ',')
+                {
+                    printParsingError(&md, "Waiting symbol ','", c);
+                    exit_status = EXIT_FAILURE;
+                    goto clear_resources;
+                }
+                md.parsing_state = COLUMN_NAME;
+                c = readNextSymbol(&md, fin);
+                skip_reading = true;
+                break;
+        }
+    }
+    clear_resources:
+    free(schema_name);
+    free(table_name);
+    free(column_name);
+    free(func_name);
+    return exit_status;
+}
+
+/* Creating string in format `schema_name.function name(column_name)` */
+void
+concatFunctionAndColumn(char *col_with_func, char *schema_name, char *column_name, char *function_name)
+{
+    /* Default function */
+    if (strcmp(function_name, DEFAULT_NAME)==0)
+	{
+	  strcpy(col_with_func, psprintf("_masking_function.%s", function_name));
+	}
+	/* Function name already contains schema name. If not, then add the same schema */
+	else if (strrchr(function_name, '.') != NULL)
+    {
+        strcpy(col_with_func, function_name);
+    }
+    else
+    {
+	  	strcpy(col_with_func, psprintf("%s.%s", schema_name, function_name));
+    }
+    strcpy(col_with_func, psprintf("%s(%s)", col_with_func, column_name));
+}
+
+/*
+ * Wrapping columns with functions
+ * schema_name.function_name(schema_name.table_name.column_name)
+ */
+char *
+addFunctionToColumn(char *schema_name, char *table_name, char *column_name, MaskingMap *map)
+{
+    char *col_with_func;
+    /* Try to find for exact schema, table and column */
+    int index = getMapIndexByKey(map, getFullRelName(schema_name, table_name, column_name));
+    if (index == -1) /* If didn't find, try to find function, that used for all schemas */
+    {
+        /* Try to find for exact table and column [default.table.column] */
+        index = getMapIndexByKey(map, getFullRelName(DEFAULT_NAME, table_name, column_name));
+        if (index == -1) /* If didn't find, try to find function, that used for all schemas and all tables */
+        {
+            /* Try to find for exact column [default.default.column] */
+            index = getMapIndexByKey(map, getFullRelName(DEFAULT_NAME, DEFAULT_NAME, column_name));
+            if (index == -1) /* If didn't find, try to find function, that used for all schemas and all tables and all columns */
+            {
+                /* Try to find function that used for all columns in all schemas and tables [default.default.default] */
+                index = getMapIndexByKey(map, getFullRelName(DEFAULT_NAME, DEFAULT_NAME, DEFAULT_NAME));
+            }
+        }
+    }
+    col_with_func = malloc(COL_WITH_FUNC_SIZE);
+    memset(col_with_func, 0, COL_WITH_FUNC_SIZE);
+    if (index != -1)
+    {
+        char *function_name = map->data[index]->value;
+        concatFunctionAndColumn(col_with_func, schema_name, column_name, function_name);
+    }
+    return col_with_func;
+}
+
+/* Remove the first and the last symbol in func_name */
+void
+removeQuotes(char *func_name)
+{
+    char *new_func_name = malloc(PATH_MAX + 1);
+    strncpy(new_func_name, func_name + 1, strlen(func_name) - 2);
+    memset(func_name, 0, PATH_MAX);
+    strcpy(func_name, new_func_name);
+    free(new_func_name);
+}
+
+/* Read a word from a query */
+char *
+readWord(FILE *fin, char *word)
+{
+    char c;
+    memset(word, 0, strlen(word));
+    do
+    {
+        c = tolower(getc(fin));
+        if (isSpace(c) || c == '(') /* Space or open brace before function arguments */
+        {
+            if (word[0] == '\0') /* Spaces before the word */
+                continue;
+            else
+                break; /* Spaces after the word */
+        }
+        else
+        {
+            strncat(word, &c, 1);
+        }
+    } while (c != EOF);
+    return word;
+}
+
+/**
+ * Extract function name from query. During extracting we also check
+ * the query, but only the start of it. We expecting the pattern:
+ * `create [or replace] function {func_name}`
+ * If something is wrong we will not use function and leave
+ * the column without transforming.
+ *
+ * We don't check the full script because we are guessing that this script will be
+ * run by users who has access to run them and will not harm theirs own data
+ */
+int
+extractFunctionNameFromQueryFile(char *filename, char *func_name)
+{
+    FILE *fin;
+    char *word;
+
+    memset(func_name, 0, REL_SIZE);
+    fin = NULL;
+    if (filename[0] != '\0')
+    {
+        fin = fopen(filename, "r");
+    }
+    if (fin == NULL)
+    {
+        pg_log_warning("Problem with file \'%s\"", filename);
+    }
+    else
+    {
+        word = malloc(REL_SIZE);
+        memset(word, 0, REL_SIZE);
+        if (strcmp(readWord(fin, word), "create") == 0) /* reading 'create' */
+        {
+            if (strcmp(readWord(fin, word), "or") == 0) /* reading 'or' | 'function' */
+            {
+                if (strcmp(readWord(fin, word), "replace") != 0) /* reading 'replace' */
+                {
+                    pg_log_warning("Keyword 'replace' was expected, but found '%s'. Check query for creating a function '%s'.\n",
+                           word, filename);
+                    goto free_resources;
+                }
+                else
+                {
+                    readWord(fin, word); /* reading 'function' */
+                }
+            }
+        }
+        else
+        {
+            pg_log_warning("Keyword 'create' was expected, but found '%s'. Check query for creating a function '%s'.\n", word,
+                   filename);
+            goto free_resources;
+        }
+        if (strcmp(word, "function") == 0)
+        {
+            strcpy(func_name, readWord(fin, word));
+        }
+        else
+        {
+            pg_log_warning("Keyword 'function' was expected, but found '%s'. Check query for creating a function '%s'.\n", word,
+                   filename);
+            goto free_resources;
+        }
+        free_resources:
+        free(word);
+        fclose(fin);
+    }
+    return func_name[0] != '\0'; /* If we got a function name, then - return 0, else - return 1 */
+}
+
+/**
+ * If there is a path (the first symbol is a quote '"'), then store this path in masking_func_query_path
+ * and write to the first argument (func_path) name of the function from the query in the file
+ * If there is not a path - do nothing
+*/
+void
+extractFuncNameIfPath(char *func_path, SimpleStringList *masking_func_query_path)
+{
+    char *func_name;
+    if (func_path[0] == '"')
+    {
+        func_name = malloc(REL_SIZE);
+        removeQuotes(func_path);
+        if (extractFunctionNameFromQueryFile(func_path, func_name) != 0) /* Read function name from query and store in func_name */
+        {
+            if (!simple_string_list_member(masking_func_query_path, func_path))
+            {
+                simple_string_list_append(masking_func_query_path, func_path);
+            }
+            strcpy(func_path, func_name); /* Store func_name in func_path to throw it to upper function */
+        }
+        free(func_name);
+    }
+}
+
+/* Read whole script from the file `filename` */
+char *
+readQueryForCreatingFunction(char *filename)
+{
+    FILE *fin;
+    char *query;
+    long fsize;
+    query = malloc(sizeof(char));
+    memset(query, 0, sizeof(char));
+    fin = fopen(filename, "r");
+    if (fin != NULL)
+    {
+        fseek(fin, 0L, SEEK_END);
+        fsize = ftell(fin);
+        fseek(fin, 0L, SEEK_SET);
+
+        query = (char *) calloc(fsize + 1, sizeof(char));
+
+        fsize = (int) fread(query, sizeof(char), fsize, fin);
+        if (fsize==0)
+        {
+            pg_log_error("File is empty `%s`", filename);
+        }
+        fclose(fin);
+    }
+    return query;
+}
+
+void
+maskingColumns(char *schema_name, char *table_name, char* column_list, MaskingMap *masking_map, PQExpBuffer *q)
+{
+    char *current_column_name = strtok(column_list, " ,()");
+    char *masked_query = malloc(COL_WITH_FUNC_SIZE);
+    char *func_with_column = malloc(COL_WITH_FUNC_SIZE);
+
+    while (current_column_name != NULL)
+    {
+        func_with_column = addFunctionToColumn(schema_name, table_name, current_column_name, masking_map);
+        if (func_with_column[0] != '\0')
+        {
+            strcpy(masked_query, func_with_column);
+        }
+        else
+        {
+            strcpy(masked_query, current_column_name);
+        }
+        current_column_name = strtok(NULL, " ,()");
+        if (current_column_name != NULL)
+            strcat(masked_query, ",");
+        appendPQExpBufferStr(*q, masked_query);
+    }
+    free(masked_query);
+    free(func_with_column);
+}
+
+/**
+ * getMaskingPatternFromFile
+ *
+ * Parse the specified masking file with description of what we need to mask into masking_map
+ */
+int
+getMaskingPatternFromFile(const char *filename, MaskingMap *masking_map, SimpleStringList *masking_func_query_path)
+{
+  FILE *fin;
+  int exit_result;
+  if (filename[0]=='\0')
+  {
+	pg_log_error("--masking filename shouldn't be empty");
+	return 1;
+  }
+
+  fin = fopen(filename, "r");
+
+  if (fin == NULL)
+  {
+	pg_log_error("--masking couldn't open file `%s`", filename);
+	return 1;
+  }
+
+  exit_result = readMaskingPatternFromFile(fin, masking_map, masking_func_query_path);
+  fclose(fin);
+  return exit_result;
+}
+
+/**
+ * Default masking function
+ * Full masking according to the data types. Returns 'XXXX' for string data types [text, varchar, character].
+ * Returns 0 for numeric data types [int, numeric, real, smallint, bigint]
+ * Returns '1900-01-01' for date and '1900-01-01 00:00:00' for timestamp
+ */
+char *
+default_functions()
+{
+  return "CREATE SCHEMA IF NOT EXISTS _masking_function;\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in text, out text)\n"
+		 "    AS $$ SELECT 'XXXX' $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in real, out real)\n"
+		 "    AS $$ SELECT 0 $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in date, out date)\n"
+		 "    AS $$ SELECT DATE '1900-01-01' $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in timestamp, out timestamp)\n"
+		 "    AS $$ SELECT TIMESTAMP '1900-01-01 00:00:00' $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "   \n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in timestamptz, out timestamptz)\n"
+		 "    AS $$ SELECT TIMESTAMPTZ '1900-01-01 00:00:00-00' $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "   \n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in time, out time)\n"
+		 "    AS $$ SELECT TIME '00:00:00' $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "   \n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in timetz, out timetz)\n"
+		 "    AS $$ SELECT TIMETZ '00:00:00-00' $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in interval, out interval)\n"
+		 "    AS $$ SELECT INTERVAL '1 year 2 months 3 days' $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in box, out box)\n"
+		 "    AS $$ SELECT box(circle '((0,0),2.0)') $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in circle, out circle)\n"
+		 "    AS $$ SELECT circle(point '(0,0)', 0) $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in path, out path)\n"
+		 "    AS $$ SELECT '[ ( 0 , 1 ) , ( 1 , 2 ) ]'::path $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in point, out point)\n"
+		 "    AS $$ SELECT '(0, 0)'::point $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in polygon , out polygon)\n"
+		 "    AS $$ SELECT '( ( 0 , 0 ) , ( 0 , 0 ) )'::polygon $$\n"
+		 "    LANGUAGE SQL;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in bytea, out bytea)\n"
+		 "    AS $$ SELECT '\\000'::bytea $$\n"
+		 "    LANGUAGE sql;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in inet, out inet)\n"
+		 "    AS $$ SELECT '0.0.0.0'::inet $$\n"
+		 "    LANGUAGE sql;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in cidr, out cidr)\n"
+		 "    AS $$ SELECT '0.0.0.0'::cidr $$\n"
+		 "    LANGUAGE sql;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in macaddr, out macaddr)\n"
+		 "    AS $$ SELECT macaddr '0:0:0:0:0:ab' $$\n"
+		 "    LANGUAGE sql;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in json, out json)\n"
+		 "    AS $$ SELECT '{\"a\":\"foo\", \"b\":\"bar\"}'::json $$\n"
+		 "    LANGUAGE sql;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in jsonb, out jsonb)\n"
+		 "    AS $$ SELECT '{\"a\":1, \"b\":2}'::jsonb $$\n"
+		 "    LANGUAGE sql;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in line, out line)\n"
+		 "    AS $$ SELECT '{1,2,3}'::line $$\n"
+		 "    LANGUAGE sql;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in lseg, out lseg)\n"
+		 "    AS $$ SELECT '((0,0),(0,0))'::lseg $$\n"
+		 "    LANGUAGE sql;\n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in bit, out bit)\n"
+		 "    AS $$ SELECT '0'::bit $$\n"
+		 "    LANGUAGE sql; \n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in boolean, out boolean)\n"
+		 "    AS $$ SELECT true $$\n"
+		 "    LANGUAGE sql;   \n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in money, out money)\n"
+		 "    AS $$ SELECT 0 $$\n"
+		 "    LANGUAGE sql;   \n"
+		 "\n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in pg_lsn, out pg_lsn)\n"
+		 "    AS $$ SELECT '0/0'::pg_lsn $$\n"
+		 "    LANGUAGE sql;  \n"
+		 "   \n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in uuid, out uuid)\n"
+		 "    AS $$ SELECT '00000000-0000-0000-0000-000000000000'::uuid $$\n"
+		 "    LANGUAGE sql;  \n"
+		 "   \n"
+		 "CREATE OR REPLACE FUNCTION _masking_function.default(in tsvector, out tsvector)\n"
+		 "    AS $$ SELECT 'a:1'::tsvector $$\n"
+		 "    LANGUAGE sql;     ";
+}
diff --git a/src/bin/pg_dump/masking.h b/src/bin/pg_dump/masking.h
new file mode 100644
index 0000000000..b879b4d41e
--- /dev/null
+++ b/src/bin/pg_dump/masking.h
@@ -0,0 +1,70 @@
+/*-------------------------------------------------------------------------
+ *
+ * masking.h
+ *
+ *	Data masking tool for pg_dump
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_dump/masking.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef MASKING_H
+#define MASKING_H
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+#include <limits.h>
+#include "fe_utils/option_utils.h"
+#include "fe_utils/simple_list.h"
+#include "dumputils.h"
+
+typedef struct _pair
+{
+  char *key;
+  char *value;
+} Pair;
+
+typedef struct MaskingMap
+{
+  Pair **data;
+  int size;
+  int capacity;
+} MaskingMap;
+
+enum
+ParsingState
+{
+  SCHEMA_NAME,
+  TABLE_NAME,
+  COLUMN_NAME,
+  FUNCTION_NAME,
+  WAIT_COLON,
+  WAIT_OPEN_BRACE,
+  WAIT_CLOSE_BRACE,
+  WAIT_COMMA
+};
+
+struct
+MaskingDebugDetails
+{
+  int line_num;
+  int symbol_num;
+  bool is_comment;
+  enum ParsingState parsing_state;
+};
+
+char *addFunctionToColumn(char *schema_name, char *table_name, char *column_name, MaskingMap *map);
+char *default_functions(void);
+int getMaskingPatternFromFile(const char *filename, MaskingMap *masking_map, SimpleStringList *masking_func_query_path);
+void maskingColumns(char *schema_name, char *table_name, char* column_list, MaskingMap *masking_map, PQExpBuffer *q);
+MaskingMap *newMaskingMap(void);
+extern int readMaskingPatternFromFile(FILE *fin, MaskingMap *map, SimpleStringList *masking_func_query_path);
+char *readQueryForCreatingFunction(char *filename);
+
+#endif                            /* MASKING_H */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index bd9b066e4e..a857f91cf9 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -59,8 +59,10 @@
 #include "dumputils.h"
 #include "fe_utils/option_utils.h"
 #include "fe_utils/string_utils.h"
+#include "fe_utils/query_utils.h"
 #include "getopt_long.h"
 #include "libpq/libpq-fs.h"
+#include "masking.h"
 #include "parallel.h"
 #include "pg_backup_db.h"
 #include "pg_backup_utils.h"
@@ -127,6 +129,9 @@ static SimpleOidList foreign_servers_include_oids = {NULL, NULL};
 
 static SimpleStringList extension_include_patterns = {NULL, NULL};
 static SimpleOidList extension_include_oids = {NULL, NULL};
+/* List of path to query with masking functions, that must be created before starting dump */
+static SimpleStringList masking_func_query_path = {NULL, NULL};
+static MaskingMap *masking_map; /* Map of columns and functions for data masking */
 
 static const CatalogId nilCatalogId = {0, 0};
 
@@ -318,7 +323,7 @@ static void appendReloptionsArrayAH(PQExpBuffer buffer, const char *reloptions,
 static char *get_synchronized_snapshot(Archive *fout);
 static void setupDumpWorker(Archive *AH);
 static TableInfo *getRootTableInfo(const TableInfo *tbinfo);
-
+static int createMaskingFunctions(Archive *AH, SimpleStringList *masking_func_query_path);
 
 int
 main(int argc, char **argv)
@@ -397,7 +402,8 @@ main(int argc, char **argv)
 		{"no-tablespaces", no_argument, &dopt.outputNoTablespaces, 1},
 		{"quote-all-identifiers", no_argument, &quote_all_identifiers, 1},
 		{"load-via-partition-root", no_argument, &dopt.load_via_partition_root, 1},
-		{"role", required_argument, NULL, 3},
+        {"masking", required_argument, NULL, 13},
+        {"role", required_argument, NULL, 3},
 		{"section", required_argument, NULL, 5},
 		{"serializable-deferrable", no_argument, &dopt.serializable_deferrable, 1},
 		{"snapshot", required_argument, NULL, 6},
@@ -414,7 +420,7 @@ main(int argc, char **argv)
 		{"rows-per-insert", required_argument, NULL, 10},
 		{"include-foreign-data", required_argument, NULL, 11},
 
-		{NULL, 0, NULL, 0}
+        {NULL, 0, NULL, 0}
 	};
 
 	pg_logging_init(argv[0]);
@@ -623,6 +629,13 @@ main(int argc, char **argv)
 										  optarg);
 				break;
 
+            case 13:			/* masking */
+				masking_map = newMaskingMap();
+			    /* If reading of masking patterns was unsuccessful, then exit */
+				if (getMaskingPatternFromFile(optarg, masking_map, &masking_func_query_path) != 0)
+					exit_nicely(1);
+				break;
+
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -743,6 +756,10 @@ main(int argc, char **argv)
 	 * death.
 	 */
 	ConnectDatabase(fout, &dopt.cparams, false);
+    if (masking_map) /* If run with --masking option */
+    {
+        createMaskingFunctions(fout, &masking_func_query_path);
+    }
 	setup_connection(fout, dumpencoding, dumpsnapshot, use_role);
 
 	/*
@@ -1035,6 +1052,7 @@ help(const char *progname)
 	printf(_("  --inserts                    dump data as INSERT commands, rather than COPY\n"));
 	printf(_("  --load-via-partition-root    load partitions via the root table\n"));
 	printf(_("  --no-comments                do not dump comments\n"));
+    printf(_("  --masking    				 data masking, helps with hiding sensitive data\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
@@ -1991,17 +2009,25 @@ dumpTableData_copy(Archive *fout, const void *dcontext)
 
 	/*
 	 * Use COPY (SELECT ...) TO when dumping a foreign table's data, and when
-	 * a filter condition was specified.  For other cases a simple COPY
-	 * suffices.
+	 * a filter (tdinfo->filtercond) or masking (masking_map) condition was specified.
+	 * For other cases a simple COPY suffices.
 	 */
-	if (tdinfo->filtercond || tbinfo->relkind == RELKIND_FOREIGN_TABLE)
+	if (tdinfo->filtercond || tbinfo->relkind == RELKIND_FOREIGN_TABLE || masking_map)
 	{
 		appendPQExpBufferStr(q, "COPY (SELECT ");
 		/* klugery to get rid of parens in column list */
 		if (strlen(column_list) > 2)
 		{
-			appendPQExpBufferStr(q, column_list + 1);
-			q->data[q->len - 1] = ' ';
+			if (masking_map) /* If run with --masking option */
+			{
+                maskingColumns(tbinfo->dobj.namespace->dobj.name, tbinfo->dobj.name, pg_strdup(column_list), masking_map, &q);
+                appendPQExpBufferStr(q, " ");
+			}
+			else
+			{
+				appendPQExpBufferStr(q, column_list + 1);
+				q->data[q->len - 1] = ' ';
+			}
 		}
 		else
 			appendPQExpBufferStr(q, "* ");
@@ -2010,13 +2036,13 @@ dumpTableData_copy(Archive *fout, const void *dcontext)
 						  fmtQualifiedDumpable(tbinfo),
 						  tdinfo->filtercond ? tdinfo->filtercond : "");
 	}
-	else
-	{
-		appendPQExpBuffer(q, "COPY %s %s TO stdout;",
-						  fmtQualifiedDumpable(tbinfo),
-						  column_list);
-	}
-	res = ExecuteSqlQuery(fout, q->data, PGRES_COPY_OUT);
+    else
+    {
+        appendPQExpBuffer(q, "COPY %s %s TO stdout;",
+                          fmtQualifiedDumpable(tbinfo),
+                          column_list);
+    }
+    res = ExecuteSqlQuery(fout, q->data, PGRES_COPY_OUT);
 	PQclear(res);
 	destroyPQExpBuffer(clistBuf);
 
@@ -2153,8 +2179,25 @@ dumpTableData_insert(Archive *fout, const void *dcontext)
 			appendPQExpBufferStr(q, ", ");
 		if (tbinfo->attgenerated[i])
 			appendPQExpBufferStr(q, "NULL");
+		else if (masking_map) /* If run with --masking option */
+		{
+			char *column_with_fun;
+			column_with_fun=addFunctionToColumn(tbinfo->dobj.namespace->dobj.name, tbinfo->dobj.name,
+												tbinfo->attnames[i], masking_map);
+
+			if (column_with_fun[0] != '\0')
+			{
+				appendPQExpBufferStr(q, column_with_fun);
+			}
+			else
+			{
+				appendPQExpBufferStr(q, fmtId(tbinfo->attnames[i]));
+			}
+			free(column_with_fun);
+        }
 		else
-			appendPQExpBufferStr(q, fmtId(tbinfo->attnames[i]));
+		  appendPQExpBufferStr(q, fmtId(tbinfo->attnames[i]));
+
 		attgenerated[nfields] = tbinfo->attgenerated[i];
 		nfields++;
 	}
@@ -18198,3 +18241,50 @@ appendReloptionsArrayAH(PQExpBuffer buffer, const char *reloptions,
 	if (!res)
 		pg_log_warning("could not parse %s array", "reloptions");
 }
+
+/**
+ * Read paths to functions from `masking_func_query_path`,
+ * read query inside the files and run them. We checked them
+ * in function masking.c:extractFunctionNameFromQueryFile.
+ */
+int
+createMaskingFunctions(Archive *AH, SimpleStringList *masking_func_query_path)
+{
+    int exit_result;
+    PGconn *conn = GetConnection(AH);
+    char *filename;
+    char *query;
+    bool result;
+
+    exit_result=0;
+    result = false;
+    /* Read all custom masking functions and create them */
+    for (SimpleStringListCell *cell = masking_func_query_path->head; cell; cell = cell->next)
+    {
+        filename=cell->val;
+        query = readQueryForCreatingFunction(filename);
+        if (query[0]=='\0')
+        {
+            pg_log_warning("Query is empty. Check file `%s`", filename);
+            exit_result++;
+        }
+        else
+        {
+            result = executeMaintenanceCommand(conn, query, true);
+        }
+
+        if (!result)
+        {
+            pg_log_warning("Failed execution of query from file \"%s\"", filename);
+        }
+	  	free(query);
+    }
+    /* Read all default functions and create them */
+    result = executeMaintenanceCommand(conn, default_functions(), true);
+    if (!result)
+    {
+        pg_log_warning("Problem during creating default functions from method `masking.c:default_functions`");
+        exit_result++;
+    }
+    return exit_result;
+}
diff --git a/src/bin/pg_dump/t/011_dump_masking.pl b/src/bin/pg_dump/t/011_dump_masking.pl
new file mode 100644
index 0000000000..6416f438c3
--- /dev/null
+++ b/src/bin/pg_dump/t/011_dump_masking.pl
@@ -0,0 +1,540 @@
+# Copyright (c) 2021, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More tests => 84;
+
+my $tempdir = PostgreSQL::Test::Utils::tempdir;
+my $inputfile;
+
+my $node      = PostgreSQL::Test::Cluster->new('main');
+my $port      = $node->port;
+my $backupdir = $node->backup_dir;
+my $plainfile = "$backupdir/plain_copy.sql";
+my $plainfile_insert = "$backupdir/plain_insert.sql";
+my $testdumo = "$backupdir/testdumo.sql";
+my $dumpfile = "$backupdir/options_plain.sql";
+my $dumpdir = "$backupdir/parallel";
+my $dumpjobfile = "$backupdir/parallel/toc.dat'";
+
+$node->init;
+$node->start;
+
+# Generate test objects
+$node->safe_psql('postgres', 'CREATE FOREIGN DATA WRAPPER dummy;');
+$node->safe_psql('postgres', 'CREATE SERVER dummyserver FOREIGN DATA WRAPPER dummy;');
+
+$node->safe_psql('postgres', "CREATE SCHEMA schema1;");
+$node->safe_psql('postgres', "CREATE SCHEMA schema2;");
+$node->safe_psql('postgres', "CREATE SCHEMA schema3;");
+$node->safe_psql('postgres', "CREATE SCHEMA schema4;");
+$node->safe_psql('postgres', "CREATE SCHEMA schema5;");
+$node->safe_psql('postgres', "CREATE SCHEMA schema6;");
+$node->safe_psql('postgres', "CREATE SCHEMA schema7;");
+$node->safe_psql('postgres', "CREATE SCHEMA large_schema_name1234567890123456789012345678901234567890123456;");
+
+$node->safe_psql('postgres', "CREATE TABLE schema1.table1(field1 varchar);");
+$node->safe_psql('postgres', "CREATE TABLE schema2.table2(field2 varchar);");
+$node->safe_psql('postgres', "CREATE TABLE schema3.table3(field3 varchar);");
+$node->safe_psql('postgres', "CREATE TABLE schema4.table4(field41 varchar, field42 varchar);");
+$node->safe_psql('postgres', "CREATE TABLE schema5.table51(field511 varchar, email varchar);");
+$node->safe_psql('postgres', "CREATE TABLE schema5.table52(email varchar, field522 varchar);");
+$node->safe_psql('postgres', "CREATE TABLE schema6.table61(email varchar, field612 varchar);");
+$node->safe_psql('postgres', "CREATE TABLE schema6.table62(field621 varchar, email varchar);");
+$node->safe_psql('postgres', "CREATE TABLE schema7.table7(field71 varchar, phone varchar);");
+$node->safe_psql('postgres', "CREATE TABLE schema7.table8(phone varchar, field82 varchar);");
+$node->safe_psql('postgres', "CREATE TABLE large_schema_name1234567890123456789012345678901234567890123456.large_table_name12345678901234567890123456789012345678901234567(large_field_1_1234567890123456789012345678901234567890123456789 varchar, large_field_2_1234567890123456789012345678901234567890123456789 varchar);");
+
+$node->safe_psql('postgres', "INSERT INTO schema1.table1 VALUES('value1');");
+$node->safe_psql('postgres', "INSERT INTO schema2.table2 VALUES('value2');");
+$node->safe_psql('postgres', "INSERT INTO schema3.table3 VALUES('value3');");
+$node->safe_psql('postgres', "INSERT INTO schema4.table4 VALUES('value41', 'value42');");
+$node->safe_psql('postgres', "INSERT INTO schema5.table51 VALUES('value511', 'value512');");
+$node->safe_psql('postgres', "INSERT INTO schema5.table52 VALUES('value521', 'value522');");
+$node->safe_psql('postgres', "INSERT INTO schema6.table61 VALUES('value611', 'value612');");
+$node->safe_psql('postgres', "INSERT INTO schema6.table62 VALUES('value621', 'value622');");
+$node->safe_psql('postgres', "INSERT INTO schema7.table7 VALUES('value71', 'value72');");
+$node->safe_psql('postgres', "INSERT INTO schema7.table8 VALUES('value81', 'value82');");
+$node->safe_psql('postgres', "INSERT INTO large_schema_name1234567890123456789012345678901234567890123456.large_table_name12345678901234567890123456789012345678901234567 VALUES('large_value_1_1234567890123456789012345678901234567890123456789', 'large_value_2');");
+
+
+#########################################
+# Use masking with custom function from file
+
+# Create files with custom function
+open $inputfile, '>>', "$tempdir/custom_function_file.txt"
+  or die "unable to open custom_function_file for writing";
+print $inputfile "
+  CREATE FUNCTION schema3.custom_function(in text, out text)
+    AS \$\$ SELECT \$1 || ' custom' \$\$
+    LANGUAGE SQL;";
+close $inputfile;
+
+open $inputfile, '>>', "$tempdir/mask_email.sql"
+  or die "unable to open mask_email.sql for writing";
+print $inputfile "
+  CREATE FUNCTION public.mask_email(in text, out text)
+    AS \$\$ SELECT \$1 || ' email' \$\$
+    LANGUAGE SQL;";
+close $inputfile;
+
+open $inputfile, '>>', "$tempdir/mask_phone.sql"
+  or die "unable to open mask_phone.sql for writing";
+print $inputfile "
+  CREATE FUNCTION public.mask_phone(in text, out text)
+    AS \$\$ SELECT \$1 || ' phone' \$\$
+    LANGUAGE SQL;";
+close $inputfile;
+
+# Create masking pattern file
+open $inputfile, '>>', "$tempdir/masking_file.txt"
+  or die "unable to open masking file for writing";
+print $inputfile "// First comment
+                  schema1
+                  {
+                    table1  // Second comment
+                    {
+                        field1: default
+                    }
+                  }
+
+                  /*
+                  Third comment
+                  */
+                  schema2
+                  {
+                    table2 {
+                        not_exist_field: default
+                    }
+                  }
+                  /**
+                  * Fourth multi line comment
+                  */
+                  schema3 /* Fifth multi line comment */
+                  {
+                    table3
+                    {
+                        field3:  \"$tempdir/custom_function_file.txt\"//Sixth comment
+                    }
+                  }
+                  schema4
+                  {
+                    table4
+                    {
+                        field41:  schema3.custom_function,
+                        field42:  \"$tempdir/custom_function_file.txt\"
+                    }
+                  }
+                  default
+                  {
+                    default
+                    {
+                        email: \"$tempdir/mask_email.sql\"
+                    }
+                    table7
+                    {
+                        phone: \"$tempdir/mask_phone.sql\"
+                    }
+                  }
+                  large_schema_name1234567890123456789012345678901234567890123456
+                  {
+                    large_table_name12345678901234567890123456789012345678901234567
+                    {
+                        large_field_1_1234567890123456789012345678901234567890123456789: default,
+                        large_field_2_1234567890123456789012345678901234567890123456789: schema3.custom_function
+                    }
+                  }
+                  ";
+close $inputfile;
+
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $plainfile,
+		"--masking=$tempdir/masking_file.txt"
+	],
+	"1. Run masking without options");
+
+my $dump = slurp_file($plainfile);
+ok($dump =~ qr/^COPY schema1\.table1 \(field1\) FROM stdin\;\nXXXX/m, "2. [Default function] Field1 was masked");
+ok($dump =~ qr/^COPY schema2\.table2 \(field2\) FROM stdin\;\nvalue2/m, "3. Field2 was not masked");
+ok($dump =~ qr/^COPY schema3\.table3 \(field3\) FROM stdin\;\nvalue3 custom/m, "4. [Function from file] Field3 was masked");
+ok($dump =~ qr/^COPY schema4\.table4 \(field41\, field42\) FROM stdin\;\nvalue41 custom	value42 custom/m, "5. [Function from file] Already created custom function can be used second time");
+ok($dump =~ qr/^COPY schema5\.table51 \(field511\, email\) FROM stdin\;\nvalue511	value512 email/m, "6. [Default schema and table] Masked only field with name `email`");
+ok($dump =~ qr/^COPY schema5\.table52 \(email\, field522\) FROM stdin\;\nvalue521 email	value522/m, "7. [Default schema and table] Masked only field with name `email`");
+ok($dump =~ qr/^COPY schema6\.table61 \(email\, field612\) FROM stdin\;\nvalue611 email	value612/m, "8. [Default schema and table] Masked only field with name `email`");
+ok($dump =~ qr/^COPY schema6\.table62 \(field621\, email\) FROM stdin\;\nvalue621	value622 email/m, "9. [Default schema and table] Masked only field with name `email`");
+ok($dump =~ qr/^COPY schema7\.table7 \(field71\, phone\) FROM stdin\;\nvalue71	value72 phone/m, "10. [Default schema] Masked only field with name `phone` from table7");
+ok($dump =~ qr/^COPY schema7\.table8 \(phone\, field82\) FROM stdin\;\nvalue81	value82/m, "11. [Default schema] Masked only field with name `phone` from table7");
+ok($dump =~ qr/^COPY large_schema_name1234567890123456789012345678901234567890123456\.large_table_name12345678901234567890123456789012345678901234567 \(large_field_1_1234567890123456789012345678901234567890123456789\, large_field_2_1234567890123456789012345678901234567890123456789\) FROM stdin\;\nXXXX	large_value_2 custom/m,
+"12. [Large values] Limit of relation name size is 63 symbols");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $plainfile_insert,
+		"--masking=$tempdir/masking_file.txt",
+		"--inserts"
+	],
+	"13. Run masking with option --inserts");
+
+my $dump = slurp_file($plainfile_insert);
+ok($dump =~ qr/^INSERT INTO schema1\.table1 VALUES \(\'XXXX\'\)/m, "14. [Default function] Field1 was masked");
+ok($dump =~ qr/^CREATE FUNCTION _masking_function\.\"default\"\(text\, OUT text\) RETURNS text/m, "15. [Default function] Default functions were created");
+ok($dump =~ qr/^INSERT INTO schema2\.table2 VALUES \(\'value2\'\)/m, "16. Field2 was not masked");
+ok($dump =~ qr/^CREATE FUNCTION schema3\.custom_function\(text\, OUT text\) RETURNS text/m, "17. [Function from file] Custom function was created");
+ok($dump =~ qr/^INSERT INTO schema3\.table3 VALUES \(\'value3 custom\'\)/m, "18. [Function from file] Field3 was masked");
+ok($dump =~ qr/^INSERT INTO schema4\.table4 VALUES \(\'value41 custom\'\, \'value42 custom\'\)/m, "19. [Function from file] Already created custom function can be used second time");
+ok($dump =~ qr/^INSERT INTO schema5\.table51 VALUES \(\'value511\'\, \'value512 email\'\)/m, "20. [Default schema and table] Masked only field with name `email`");
+ok($dump =~ qr/^INSERT INTO schema5\.table52 VALUES \(\'value521 email\'\, \'value522\'\)/m, "21. [Default schema and table] Masked only field with name `email`");
+ok($dump =~ qr/^INSERT INTO schema6\.table61 VALUES \(\'value611 email\'\, \'value612\'\)/m, "22. [Default schema and table] Masked only field with name `email`");
+ok($dump =~ qr/^INSERT INTO schema6\.table62 VALUES \(\'value621\'\, \'value622 email\'\)/m, "23. [Default schema and table] Masked only field with name `email`");
+ok($dump =~ qr/^INSERT INTO schema7\.table7 VALUES \(\'value71\'\, \'value72 phone\'\)/m, "24. [Default schema] Masked only field with name `phone` from table7");
+ok($dump =~ qr/^INSERT INTO schema7\.table8 VALUES \(\'value81\'\, \'value82\'\)/m, "25. [Default schema] Masked only field with name `phone` from table7");
+ok($dump =~ qr/^INSERT INTO large_schema_name1234567890123456789012345678901234567890123456\.large_table_name12345678901234567890123456789012345678901234567 VALUES \(\'XXXX\'\, \'large_value_2 custom\'\)/m,
+"26. [Large values] Limit of relation name size is 63 symbols");
+
+#########################################
+# Run masking with other options
+#########################################
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		'--data-only'
+	],
+	"27. Run masking with option --data-only");
+ok(slurp_file($dumpfile) =~ qr/^COPY schema7\.table7 \(field71\, phone\) FROM stdin\;\nvalue71	value72 phone/m,
+"28. Check dump after running masking with option --data-only");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		'--clean'
+	],
+	"29. Run masking with option --clean");
+ok(slurp_file($dumpfile) =~ qr/^COPY schema7\.table7 \(field71\, phone\) FROM stdin\;\nvalue71	value72 phone/m,
+"30. Check dump after running masking with option --clean");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		'--create'
+	],
+	"31. Run masking with option --create");
+ok(slurp_file($dumpfile) =~ qr/^COPY schema7\.table7 \(field71\, phone\) FROM stdin\;\nvalue71	value72 phone/m,
+"32. Check dump after running masking with option --create");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		"--encoding=UTF-8"
+	],
+	"33. Run masking with option --encoding");
+ok(slurp_file($dumpfile) =~ qr/^COPY schema7\.table7 \(field71\, phone\) FROM stdin\;\nvalue71	value72 phone/m,
+"34. Check dump after running masking with option --encoding");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		"--schema=schema6"
+	],
+	"35. Run masking with option --schema");
+ok(slurp_file($dumpfile) !~ qr/^COPY schema7\.table7 \(field71\, phone\) FROM stdin\;\nvalue71	value72 phone/m,
+"36. Check dump after running masking with option --schema");
+ok(slurp_file($dumpfile) =~ qr/^COPY schema6\.table62 \(field621\, email\) FROM stdin\;\nvalue621	value622 email/m,
+"37. Check dump after running masking with option --schema");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		"--table=schema6.table62"
+	],
+	"38. Run masking with option --table");
+ok(slurp_file($dumpfile) =~ qr/^COPY schema6\.table62 \(field621\, email\) FROM stdin\;\nvalue621	value622 email/m,
+"39. Check dump after running masking with option --table");
+ok(slurp_file($dumpfile) !~ qr/^COPY schema6\.table61 \(email\, field612\) FROM stdin\;\nvalue611 email	value612/m,
+"40. Check dump after running masking with option --table");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		"--exclude-table=schema6.table62"
+	],
+	"41. Run masking with option --exclude-table");
+ok(slurp_file($dumpfile) !~ qr/^COPY schema6\.table62 \(field621\, email\) FROM stdin\;\nvalue621	value622 email/m,
+"42. Check dump after running masking with option --exclude-table");
+ok(slurp_file($dumpfile) =~ qr/^COPY schema6\.table61 \(email\, field612\) FROM stdin\;\nvalue611 email	value612/m,
+"43. Check dump after running masking with option --exclude-table");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+ 		"--masking=$tempdir/masking_file.txt",
+		"--load-via-partition-root"
+	],
+	"44. Run masking with option --load-via-partition-root");
+ok(slurp_file($dumpfile) =~ qr/^COPY schema6\.table62 \(field621\, email\) FROM stdin\;\nvalue621	value622 email/m,
+"45. Check dump after running masking with option --load-via-partition-root");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+ 		"--masking=$tempdir/masking_file.txt",
+		"--lock-wait-timeout=10"
+	],
+	"46. Run masking with option --lock-wait-timeout");
+ok(slurp_file($dumpfile) =~ qr/^COPY schema6\.table62 \(field621\, email\) FROM stdin\;\nvalue621	value622 email/m,
+"47. Check dump after running masking with option --lock-wait-timeout");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+ 		"--masking=$tempdir/masking_file.txt",
+		"--no-comments",
+		"--no-publications",
+		"--no-security-labels",
+		"--no-subscriptions",
+		"--no-sync",
+		"--no-tablespaces",
+		"--no-toast-compression",
+		"--no-unlogged-table-data"
+	],
+	"48. Run masking with skip options");
+ok(slurp_file($dumpfile) =~ qr/^COPY schema6\.table62 \(field621\, email\) FROM stdin\;\nvalue621	value622 email/m,
+"49. Check dump after running masking with skip options");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+ 		"--masking=$tempdir/masking_file.txt",
+		"--quote-all-identifiers"
+	],
+	"50. Run masking with option --quote-all-identifiers");
+ok(slurp_file($dumpfile) =~ qr/^COPY \"schema4\"\.\"table4\" \(\"field41\"\, \"field42\"\) FROM stdin\;\nvalue41	value42/m,
+"51. Check dump after running masking with option --quote-all-identifiers");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+ 		"--masking=$tempdir/masking_file.txt",
+		"--rows-per-insert=10"
+	],
+	"52. Run masking with option --rows-per-insert");
+ok(slurp_file($dumpfile) =~ qr/^INSERT INTO schema6\.table62 VALUES\n	\(\'value621\'\, \'value622 email\'\)\;/m,
+"53. Check dump after running masking with option --rows-per-insert");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+ 		"--masking=$tempdir/masking_file.txt",
+		"--section=pre-data"
+	],
+	"54. Run masking with option --section");
+ok(slurp_file($dumpfile) =~ qr/^CREATE FUNCTION public\.mask_phone\(text\, OUT text\) RETURNS text/m,
+"55. Check dump after running masking with option --section");
+ok(slurp_file($dumpfile) !~ qr/^INSERT INTO schema7\.table7 VALUES \(\'value71\'\, \'value72 phone\'\)/m,
+"56. Check dump after running masking with option --section");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+ 		"--masking=$tempdir/masking_file.txt",
+		"--serializable-deferrable"
+	],
+	"57. Run masking with option --serializable-deferrable");
+ok(slurp_file($dumpfile) =~ qr/^COPY schema6\.table62 \(field621\, email\) FROM stdin\;\nvalue621	value622 email/m,
+"58. Check dump after running masking with option --serializable-deferrable");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		'--strict-names', 'postgres'
+	],
+	"59. Run masking with option --strict names");
+ok(slurp_file($dumpfile) =~ qr/^COPY schema6\.table62 \(field621\, email\) FROM stdin\;\nvalue621	value622 email/m,
+"60. Check dump after running masking with option --serializable-deferrable");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		'--use-set-session-authorization'
+	],
+	"61. Run masking with option --use-set-session-authorization");
+ok(slurp_file($dumpfile) =~ qr/^COPY schema6\.table62 \(field621\, email\) FROM stdin\;\nvalue621	value622 email/m,
+"62. Check dump after running masking with option --use-set-session-authorization");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		"--compress=9"
+	],
+	"63. Run masking with option --compress");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpdir,
+		"--masking=$tempdir/masking_file.txt",
+		"--format=directory",
+		"--jobs=2"
+	],
+	"64. Run masking with option --jobs");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		"--no-privileges"
+	],
+	"65. Run masking with option --no-privileges");
+ok(slurp_file($dumpfile) =~ qr/^COPY schema6\.table62 \(field621\, email\) FROM stdin\;\nvalue621	value622 email/m,
+"66. Check dump after running masking with option --no-privileges");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		"--binary-upgrade"
+	],
+	"67. Run masking with option --binary-upgrade");
+ok(slurp_file($dumpfile) =~ qr/^COPY schema6\.table62 \(field621\, email\) FROM stdin\;\nvalue621	value622 email/m,
+"68. Check dump after running masking with option --binary-upgrade");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		"--column-inserts"
+	],
+	"69. Run masking with option --column-inserts");
+ok(slurp_file($dumpfile) =~ qr/^INSERT INTO schema7\.table7 \(field71\, mask_phone\) VALUES \(\'value71\'\, \'value72 phone\'\)/m,
+"70. Check dump after running masking with option --column-inserts");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		"--disable-dollar-quoting"
+	],
+	"71. Run masking with option --disable-dollar-quoting");
+ok(slurp_file($dumpfile) =~ qr/^COPY schema6\.table62 \(field621\, email\) FROM stdin\;\nvalue621	value622 email/m,
+"72. Check dump after running masking with option --disable-dollar-quoting");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--disable-triggers",
+		"--masking=$tempdir/masking_file.txt"
+	],
+	"73. Run masking with option --disable-triggers");
+ok(slurp_file($dumpfile) =~ qr/^COPY schema6\.table62 \(field621\, email\) FROM stdin\;\nvalue621	value622 email/m,
+"74. Check dump after running masking with option --disable-triggers");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+ 		"--masking=$tempdir/masking_file.txt",
+		"--if-exists",
+		"--clean",
+	],
+	"75. Run masking with option --if-exists");
+ok(slurp_file($dumpfile) =~ qr/^COPY schema6\.table62 \(field621\, email\) FROM stdin\;\nvalue621	value622 email/m,
+"76. Check dump after running masking with option --if-exists");
+
+command_ok(
+	[
+		'pg_dump', '-p', $port, '-f', $dumpfile,
+		"--masking=$tempdir/masking_file.txt",
+		"--verbose"
+	],
+	"77. Run masking with option --verbose");
+
+#########################################
+# Negative cases
+#########################################
+open $inputfile, '>>', "$tempdir/drop_table_script.sql"
+  or die "unable to open mask_phone.sql for writing";
+print $inputfile "DROP TABLE schema1.table1;";
+close $inputfile;
+
+open $inputfile, '>>', "$tempdir/masking_file_2.txt"
+  or die "unable to open masking file for writing";
+print $inputfile "schema1
+                  {
+                    table1
+                    {
+                        field1:  \"$tempdir/drop_table_script.sql\"
+                    }
+                  }
+                  ";
+close $inputfile;
+
+
+command_fails_like(
+	[
+		'pg_dump', '-p', $port, '-f', $plainfile,
+		"--masking=$tempdir/masking_file_2.txt"
+	],
+	qr/pg_dump: warning: Keyword 'create' was expected, but found 'drop'. Check query for creating a function/,
+	"78, 79. Run masking with wrong query");
+
+open $inputfile, '>>', "$tempdir/masking_file_2.txt"
+  or die "unable to open masking file for writing";
+print $inputfile "schema1
+                  {
+                    table1
+                    {
+                        field,
+                    }
+                  }
+                  ";
+close $inputfile;
+
+command_fails_like(
+	[
+		'pg_dump', '-p', $port, '-f', $plainfile,
+		"--masking=$tempdir/masking_file_2.txt"
+	],
+	qr/\Qpg_dump: error: Error position (symbol ','): line: 12 pos: 31. Waiting symbol ':'\E/,
+	"80, 81. Run masking with wrong masking file. Unexpected terminal symbol.");
+
+open $inputfile, '>>', "$tempdir/masking_file_3.txt"
+  or die "unable to open masking file for writing";
+print $inputfile
+"schema1
+{
+table1
+    {
+        field1: function     ,
+        field2: wrong function
+    }
+}";
+
+command_fails_like(
+	[
+		'pg_dump', '-p', $port, '-f', $plainfile,
+		"--masking=$tempdir/masking_file_3.txt"
+	],
+	qr/\Qpg_dump: error: Error position (symbol 'f'): line: 6 pos: 24. Syntax error. Relation name can't contain space symbols.\E/,
+	"82, 83. Run masking with wrong masking file. Function name with space.");
+close $inputfile;
+
+command_fails_like(
+	[ 'pg_dump', '--masking', '' ],
+	qr/\Qpg_dump: error: --masking filename shouldn't be empty\E/,
+	'84. File parameter shouldn\'t be empty');
+
+done_testing();
+
diff --git a/src/tools/msvc/Mkvcbuild.pm b/src/tools/msvc/Mkvcbuild.pm
index 83a3e40425..d606d77037 100644
--- a/src/tools/msvc/Mkvcbuild.pm
+++ b/src/tools/msvc/Mkvcbuild.pm
@@ -436,6 +436,7 @@ sub mkvcbuild
 	$pgdump->AddFile('src/bin/pg_dump/pg_dump.c');
 	$pgdump->AddFile('src/bin/pg_dump/common.c');
 	$pgdump->AddFile('src/bin/pg_dump/pg_dump_sort.c');
+	$pgdump->AddFile('src/bin/pg_dump/masking.c');
 	$pgdump->AddLibrary('ws2_32.lib');
 
 	my $pgdumpall = AddSimpleFrontend('pg_dump', 1);
-- 
2.25.1