1/*-------------------------------------------------------------------------
2 *
3 * pg_rewind.c
4 * Synchronizes a PostgreSQL data directory to a new timeline
5 *
6 * Portions Copyright (c) 1996-2019, PostgreSQL Global Development Group
7 *
8 *-------------------------------------------------------------------------
9 */
10#include "postgres_fe.h"
11
12#include <sys/stat.h>
13#include <fcntl.h>
14#include <time.h>
15#include <unistd.h>
16
17#include "pg_rewind.h"
18#include "fetch.h"
19#include "file_ops.h"
20#include "filemap.h"
21
22#include "access/timeline.h"
23#include "access/xlog_internal.h"
24#include "catalog/catversion.h"
25#include "catalog/pg_control.h"
26#include "common/controldata_utils.h"
27#include "common/file_perm.h"
28#include "common/file_utils.h"
29#include "common/restricted_token.h"
30#include "getopt_long.h"
31#include "storage/bufpage.h"
32
33static void usage(const char *progname);
34
35static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
36 XLogRecPtr checkpointloc);
37
38static void digestControlFile(ControlFileData *ControlFile, char *source,
39 size_t size);
40static void syncTargetDirectory(void);
41static void sanityChecks(void);
42static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
43
44static ControlFileData ControlFile_target;
45static ControlFileData ControlFile_source;
46
47const char *progname;
48int WalSegSz;
49
50/* Configuration options */
51char *datadir_target = NULL;
52char *datadir_source = NULL;
53char *connstr_source = NULL;
54
55static bool debug = false;
56bool showprogress = false;
57bool dry_run = false;
58bool do_sync = true;
59
60/* Target history */
61TimeLineHistoryEntry *targetHistory;
62int targetNentries;
63
64/* Progress counters */
65uint64 fetch_size;
66uint64 fetch_done;
67
68
69static void
70usage(const char *progname)
71{
72 printf(_("%s resynchronizes a PostgreSQL cluster with another copy of the cluster.\n\n"), progname);
73 printf(_("Usage:\n %s [OPTION]...\n\n"), progname);
74 printf(_("Options:\n"));
75 printf(_(" -D, --target-pgdata=DIRECTORY existing data directory to modify\n"));
76 printf(_(" --source-pgdata=DIRECTORY source data directory to synchronize with\n"));
77 printf(_(" --source-server=CONNSTR source server to synchronize with\n"));
78 printf(_(" -n, --dry-run stop before modifying anything\n"));
79 printf(_(" -N, --no-sync do not wait for changes to be written\n"
80 " safely to disk\n"));
81 printf(_(" -P, --progress write progress messages\n"));
82 printf(_(" --debug write a lot of debug messages\n"));
83 printf(_(" -V, --version output version information, then exit\n"));
84 printf(_(" -?, --help show this help, then exit\n"));
85 printf(_("\nReport bugs to <pgsql-bugs@lists.postgresql.org>.\n"));
86}
87
88
89int
90main(int argc, char **argv)
91{
92 static struct option long_options[] = {
93 {"help", no_argument, NULL, '?'},
94 {"target-pgdata", required_argument, NULL, 'D'},
95 {"source-pgdata", required_argument, NULL, 1},
96 {"source-server", required_argument, NULL, 2},
97 {"version", no_argument, NULL, 'V'},
98 {"dry-run", no_argument, NULL, 'n'},
99 {"no-sync", no_argument, NULL, 'N'},
100 {"progress", no_argument, NULL, 'P'},
101 {"debug", no_argument, NULL, 3},
102 {NULL, 0, NULL, 0}
103 };
104 int option_index;
105 int c;
106 XLogRecPtr divergerec;
107 int lastcommontliIndex;
108 XLogRecPtr chkptrec;
109 TimeLineID chkpttli;
110 XLogRecPtr chkptredo;
111 size_t size;
112 char *buffer;
113 bool rewind_needed;
114 XLogRecPtr endrec;
115 TimeLineID endtli;
116 ControlFileData ControlFile_new;
117
118 pg_logging_init(argv[0]);
119 set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_rewind"));
120 progname = get_progname(argv[0]);
121
122 /* Process command-line arguments */
123 if (argc > 1)
124 {
125 if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
126 {
127 usage(progname);
128 exit(0);
129 }
130 if (strcmp(argv[1], "--version") == 0 || strcmp(argv[1], "-V") == 0)
131 {
132 puts("pg_rewind (PostgreSQL) " PG_VERSION);
133 exit(0);
134 }
135 }
136
137 while ((c = getopt_long(argc, argv, "D:nNP", long_options, &option_index)) != -1)
138 {
139 switch (c)
140 {
141 case '?':
142 fprintf(stderr, _("Try \"%s --help\" for more information.\n"), progname);
143 exit(1);
144
145 case 'P':
146 showprogress = true;
147 break;
148
149 case 'n':
150 dry_run = true;
151 break;
152
153 case 'N':
154 do_sync = false;
155 break;
156
157 case 3:
158 debug = true;
159 pg_logging_set_level(PG_LOG_DEBUG);
160 break;
161
162 case 'D': /* -D or --target-pgdata */
163 datadir_target = pg_strdup(optarg);
164 break;
165
166 case 1: /* --source-pgdata */
167 datadir_source = pg_strdup(optarg);
168 break;
169 case 2: /* --source-server */
170 connstr_source = pg_strdup(optarg);
171 break;
172 }
173 }
174
175 if (datadir_source == NULL && connstr_source == NULL)
176 {
177 pg_log_error("no source specified (--source-pgdata or --source-server)");
178 fprintf(stderr, _("Try \"%s --help\" for more information.\n"), progname);
179 exit(1);
180 }
181
182 if (datadir_source != NULL && connstr_source != NULL)
183 {
184 pg_log_error("only one of --source-pgdata or --source-server can be specified");
185 fprintf(stderr, _("Try \"%s --help\" for more information.\n"), progname);
186 exit(1);
187 }
188
189 if (datadir_target == NULL)
190 {
191 pg_log_error("no target data directory specified (--target-pgdata)");
192 fprintf(stderr, _("Try \"%s --help\" for more information.\n"), progname);
193 exit(1);
194 }
195
196 if (optind < argc)
197 {
198 pg_log_error("too many command-line arguments (first is \"%s\")",
199 argv[optind]);
200 fprintf(stderr, _("Try \"%s --help\" for more information.\n"), progname);
201 exit(1);
202 }
203
204 /*
205 * Don't allow pg_rewind to be run as root, to avoid overwriting the
206 * ownership of files in the data directory. We need only check for root
207 * -- any other user won't have sufficient permissions to modify files in
208 * the data directory.
209 */
210#ifndef WIN32
211 if (geteuid() == 0)
212 {
213 pg_log_error("cannot be executed by \"root\"");
214 fprintf(stderr, _("You must run %s as the PostgreSQL superuser.\n"),
215 progname);
216 exit(1);
217 }
218#endif
219
220 get_restricted_token();
221
222 /* Set mask based on PGDATA permissions */
223 if (!GetDataDirectoryCreatePerm(datadir_target))
224 {
225 pg_log_error("could not read permissions of directory \"%s\": %m",
226 datadir_target);
227 exit(1);
228 }
229
230 umask(pg_mode_mask);
231
232 /* Connect to remote server */
233 if (connstr_source)
234 libpqConnect(connstr_source);
235
236 /*
237 * Ok, we have all the options and we're ready to start. Read in all the
238 * information we need from both clusters.
239 */
240 buffer = slurpFile(datadir_target, "global/pg_control", &size);
241 digestControlFile(&ControlFile_target, buffer, size);
242 pg_free(buffer);
243
244 buffer = fetchFile("global/pg_control", &size);
245 digestControlFile(&ControlFile_source, buffer, size);
246 pg_free(buffer);
247
248 sanityChecks();
249
250 /*
251 * If both clusters are already on the same timeline, there's nothing to
252 * do.
253 */
254 if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
255 {
256 pg_log_info("source and target cluster are on the same timeline");
257 rewind_needed = false;
258 }
259 else
260 {
261 findCommonAncestorTimeline(&divergerec, &lastcommontliIndex);
262 pg_log_info("servers diverged at WAL location %X/%X on timeline %u",
263 (uint32) (divergerec >> 32), (uint32) divergerec,
264 targetHistory[lastcommontliIndex].tli);
265
266 /*
267 * Check for the possibility that the target is in fact a direct
268 * ancestor of the source. In that case, there is no divergent history
269 * in the target that needs rewinding.
270 */
271 if (ControlFile_target.checkPoint >= divergerec)
272 {
273 rewind_needed = true;
274 }
275 else
276 {
277 XLogRecPtr chkptendrec;
278
279 /* Read the checkpoint record on the target to see where it ends. */
280 chkptendrec = readOneRecord(datadir_target,
281 ControlFile_target.checkPoint,
282 targetNentries - 1);
283
284 /*
285 * If the histories diverged exactly at the end of the shutdown
286 * checkpoint record on the target, there are no WAL records in
287 * the target that don't belong in the source's history, and no
288 * rewind is needed.
289 */
290 if (chkptendrec == divergerec)
291 rewind_needed = false;
292 else
293 rewind_needed = true;
294 }
295 }
296
297 if (!rewind_needed)
298 {
299 pg_log_info("no rewind required");
300 exit(0);
301 }
302
303 findLastCheckpoint(datadir_target, divergerec,
304 lastcommontliIndex,
305 &chkptrec, &chkpttli, &chkptredo);
306 pg_log_info("rewinding from last common checkpoint at %X/%X on timeline %u",
307 (uint32) (chkptrec >> 32), (uint32) chkptrec,
308 chkpttli);
309
310 /*
311 * Build the filemap, by comparing the source and target data directories.
312 */
313 filemap_create();
314 if (showprogress)
315 pg_log_info("reading source file list");
316 fetchSourceFileList();
317 if (showprogress)
318 pg_log_info("reading target file list");
319 traverse_datadir(datadir_target, &process_target_file);
320
321 /*
322 * Read the target WAL from last checkpoint before the point of fork, to
323 * extract all the pages that were modified on the target cluster after
324 * the fork. We can stop reading after reaching the final shutdown record.
325 * XXX: If we supported rewinding a server that was not shut down cleanly,
326 * we would need to replay until the end of WAL here.
327 */
328 if (showprogress)
329 pg_log_info("reading WAL in target");
330 extractPageMap(datadir_target, chkptrec, lastcommontliIndex,
331 ControlFile_target.checkPoint);
332 filemap_finalize();
333
334 if (showprogress)
335 calculate_totals();
336
337 /* this is too verbose even for verbose mode */
338 if (debug)
339 print_filemap();
340
341 /*
342 * Ok, we're ready to start copying things over.
343 */
344 if (showprogress)
345 {
346 pg_log_info("need to copy %lu MB (total source directory size is %lu MB)",
347 (unsigned long) (filemap->fetch_size / (1024 * 1024)),
348 (unsigned long) (filemap->total_size / (1024 * 1024)));
349
350 fetch_size = filemap->fetch_size;
351 fetch_done = 0;
352 }
353
354 /*
355 * This is the point of no return. Once we start copying things, we have
356 * modified the target directory and there is no turning back!
357 */
358
359 executeFileMap();
360
361 progress_report(true);
362 printf("\n");
363
364 if (showprogress)
365 pg_log_info("creating backup label and updating control file");
366 createBackupLabel(chkptredo, chkpttli, chkptrec);
367
368 /*
369 * Update control file of target. Make it ready to perform archive
370 * recovery when restarting.
371 *
372 * minRecoveryPoint is set to the current WAL insert location in the
373 * source server. Like in an online backup, it's important that we recover
374 * all the WAL that was generated while we copied the files over.
375 */
376 memcpy(&ControlFile_new, &ControlFile_source, sizeof(ControlFileData));
377
378 if (connstr_source)
379 {
380 endrec = libpqGetCurrentXlogInsertLocation();
381 endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
382 }
383 else
384 {
385 endrec = ControlFile_source.checkPoint;
386 endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
387 }
388 ControlFile_new.minRecoveryPoint = endrec;
389 ControlFile_new.minRecoveryPointTLI = endtli;
390 ControlFile_new.state = DB_IN_ARCHIVE_RECOVERY;
391 update_controlfile(datadir_target, &ControlFile_new, do_sync);
392
393 if (showprogress)
394 pg_log_info("syncing target data directory");
395 syncTargetDirectory();
396
397 pg_log_info("Done!");
398
399 return 0;
400}
401
402static void
403sanityChecks(void)
404{
405 /* TODO Check that there's no backup_label in either cluster */
406
407 /* Check system_id match */
408 if (ControlFile_target.system_identifier != ControlFile_source.system_identifier)
409 pg_fatal("source and target clusters are from different systems");
410
411 /* check version */
412 if (ControlFile_target.pg_control_version != PG_CONTROL_VERSION ||
413 ControlFile_source.pg_control_version != PG_CONTROL_VERSION ||
414 ControlFile_target.catalog_version_no != CATALOG_VERSION_NO ||
415 ControlFile_source.catalog_version_no != CATALOG_VERSION_NO)
416 {
417 pg_fatal("clusters are not compatible with this version of pg_rewind");
418 }
419
420 /*
421 * Target cluster need to use checksums or hint bit wal-logging, this to
422 * prevent from data corruption that could occur because of hint bits.
423 */
424 if (ControlFile_target.data_checksum_version != PG_DATA_CHECKSUM_VERSION &&
425 !ControlFile_target.wal_log_hints)
426 {
427 pg_fatal("target server needs to use either data checksums or \"wal_log_hints = on\"");
428 }
429
430 /*
431 * Target cluster better not be running. This doesn't guard against
432 * someone starting the cluster concurrently. Also, this is probably more
433 * strict than necessary; it's OK if the target node was not shut down
434 * cleanly, as long as it isn't running at the moment.
435 */
436 if (ControlFile_target.state != DB_SHUTDOWNED &&
437 ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
438 pg_fatal("target server must be shut down cleanly");
439
440 /*
441 * When the source is a data directory, also require that the source
442 * server is shut down. There isn't any very strong reason for this
443 * limitation, but better safe than sorry.
444 */
445 if (datadir_source &&
446 ControlFile_source.state != DB_SHUTDOWNED &&
447 ControlFile_source.state != DB_SHUTDOWNED_IN_RECOVERY)
448 pg_fatal("source data directory must be shut down cleanly");
449}
450
451/*
452 * Print a progress report based on the fetch_size and fetch_done variables.
453 *
454 * Progress report is written at maximum once per second, unless the
455 * force parameter is set to true.
456 */
457void
458progress_report(bool force)
459{
460 static pg_time_t last_progress_report = 0;
461 int percent;
462 char fetch_done_str[32];
463 char fetch_size_str[32];
464 pg_time_t now;
465
466 if (!showprogress)
467 return;
468
469 now = time(NULL);
470 if (now == last_progress_report && !force)
471 return; /* Max once per second */
472
473 last_progress_report = now;
474 percent = fetch_size ? (int) ((fetch_done) * 100 / fetch_size) : 0;
475
476 /*
477 * Avoid overflowing past 100% or the full size. This may make the total
478 * size number change as we approach the end of the backup (the estimate
479 * will always be wrong if WAL is included), but that's better than having
480 * the done column be bigger than the total.
481 */
482 if (percent > 100)
483 percent = 100;
484 if (fetch_done > fetch_size)
485 fetch_size = fetch_done;
486
487 /*
488 * Separate step to keep platform-dependent format code out of
489 * translatable strings. And we only test for INT64_FORMAT availability
490 * in snprintf, not fprintf.
491 */
492 snprintf(fetch_done_str, sizeof(fetch_done_str), INT64_FORMAT,
493 fetch_done / 1024);
494 snprintf(fetch_size_str, sizeof(fetch_size_str), INT64_FORMAT,
495 fetch_size / 1024);
496
497 fprintf(stderr, _("%*s/%s kB (%d%%) copied"),
498 (int) strlen(fetch_size_str), fetch_done_str, fetch_size_str,
499 percent);
500 if (isatty(fileno(stderr)))
501 fprintf(stderr, "\r");
502 else
503 fprintf(stderr, "\n");
504}
505
506/*
507 * Find minimum from two WAL locations assuming InvalidXLogRecPtr means
508 * infinity as src/include/access/timeline.h states. This routine should
509 * be used only when comparing WAL locations related to history files.
510 */
511static XLogRecPtr
512MinXLogRecPtr(XLogRecPtr a, XLogRecPtr b)
513{
514 if (XLogRecPtrIsInvalid(a))
515 return b;
516 else if (XLogRecPtrIsInvalid(b))
517 return a;
518 else
519 return Min(a, b);
520}
521
522/*
523 * Retrieve timeline history for given control file which should behold
524 * either source or target.
525 */
526static TimeLineHistoryEntry *
527getTimelineHistory(ControlFileData *controlFile, int *nentries)
528{
529 TimeLineHistoryEntry *history;
530 TimeLineID tli;
531
532 tli = controlFile->checkPointCopy.ThisTimeLineID;
533
534 /*
535 * Timeline 1 does not have a history file, so there is no need to check
536 * and fake an entry with infinite start and end positions.
537 */
538 if (tli == 1)
539 {
540 history = (TimeLineHistoryEntry *) pg_malloc(sizeof(TimeLineHistoryEntry));
541 history->tli = tli;
542 history->begin = history->end = InvalidXLogRecPtr;
543 *nentries = 1;
544 }
545 else
546 {
547 char path[MAXPGPATH];
548 char *histfile;
549
550 TLHistoryFilePath(path, tli);
551
552 /* Get history file from appropriate source */
553 if (controlFile == &ControlFile_source)
554 histfile = fetchFile(path, NULL);
555 else if (controlFile == &ControlFile_target)
556 histfile = slurpFile(datadir_target, path, NULL);
557 else
558 pg_fatal("invalid control file");
559
560 history = rewind_parseTimeLineHistory(histfile, tli, nentries);
561 pg_free(histfile);
562 }
563
564 if (debug)
565 {
566 int i;
567
568 if (controlFile == &ControlFile_source)
569 pg_log_debug("Source timeline history:");
570 else if (controlFile == &ControlFile_target)
571 pg_log_debug("Target timeline history:");
572 else
573 Assert(false);
574
575 /*
576 * Print the target timeline history.
577 */
578 for (i = 0; i < targetNentries; i++)
579 {
580 TimeLineHistoryEntry *entry;
581
582 entry = &history[i];
583 pg_log_debug("%d: %X/%X - %X/%X", entry->tli,
584 (uint32) (entry->begin >> 32), (uint32) (entry->begin),
585 (uint32) (entry->end >> 32), (uint32) (entry->end));
586 }
587 }
588
589 return history;
590}
591
592/*
593 * Determine the TLI of the last common timeline in the timeline history of the
594 * two clusters. targetHistory is filled with target timeline history and
595 * targetNentries is number of items in targetHistory. *tliIndex is set to the
596 * index of last common timeline in targetHistory array, and *recptr is set to
597 * the position where the timeline history diverged (ie. the first WAL record
598 * that's not the same in both clusters).
599 *
600 * Control files of both clusters must be read into ControlFile_target/source
601 * before calling this routine.
602 */
603static void
604findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex)
605{
606 TimeLineHistoryEntry *sourceHistory;
607 int sourceNentries;
608 int i,
609 n;
610
611 /* Retrieve timelines for both source and target */
612 sourceHistory = getTimelineHistory(&ControlFile_source, &sourceNentries);
613 targetHistory = getTimelineHistory(&ControlFile_target, &targetNentries);
614
615 /*
616 * Trace the history forward, until we hit the timeline diverge. It may
617 * still be possible that the source and target nodes used the same
618 * timeline number in their history but with different start position
619 * depending on the history files that each node has fetched in previous
620 * recovery processes. Hence check the start position of the new timeline
621 * as well and move down by one extra timeline entry if they do not match.
622 */
623 n = Min(sourceNentries, targetNentries);
624 for (i = 0; i < n; i++)
625 {
626 if (sourceHistory[i].tli != targetHistory[i].tli ||
627 sourceHistory[i].begin != targetHistory[i].begin)
628 break;
629 }
630
631 if (i > 0)
632 {
633 i--;
634 *recptr = MinXLogRecPtr(sourceHistory[i].end, targetHistory[i].end);
635 *tliIndex = i;
636
637 pg_free(sourceHistory);
638 return;
639 }
640 else
641 {
642 pg_fatal("could not find common ancestor of the source and target cluster's timelines");
643 }
644}
645
646
647/*
648 * Create a backup_label file that forces recovery to begin at the last common
649 * checkpoint.
650 */
651static void
652createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli, XLogRecPtr checkpointloc)
653{
654 XLogSegNo startsegno;
655 time_t stamp_time;
656 char strfbuf[128];
657 char xlogfilename[MAXFNAMELEN];
658 struct tm *tmp;
659 char buf[1000];
660 int len;
661
662 XLByteToSeg(startpoint, startsegno, WalSegSz);
663 XLogFileName(xlogfilename, starttli, startsegno, WalSegSz);
664
665 /*
666 * Construct backup label file
667 */
668 stamp_time = time(NULL);
669 tmp = localtime(&stamp_time);
670 strftime(strfbuf, sizeof(strfbuf), "%Y-%m-%d %H:%M:%S %Z", tmp);
671
672 len = snprintf(buf, sizeof(buf),
673 "START WAL LOCATION: %X/%X (file %s)\n"
674 "CHECKPOINT LOCATION: %X/%X\n"
675 "BACKUP METHOD: pg_rewind\n"
676 "BACKUP FROM: standby\n"
677 "START TIME: %s\n",
678 /* omit LABEL: line */
679 (uint32) (startpoint >> 32), (uint32) startpoint, xlogfilename,
680 (uint32) (checkpointloc >> 32), (uint32) checkpointloc,
681 strfbuf);
682 if (len >= sizeof(buf))
683 pg_fatal("backup label buffer too small"); /* shouldn't happen */
684
685 /* TODO: move old file out of the way, if any. */
686 open_target_file("backup_label", true); /* BACKUP_LABEL_FILE */
687 write_target_range(buf, 0, len);
688 close_target_file();
689}
690
691/*
692 * Check CRC of control file
693 */
694static void
695checkControlFile(ControlFileData *ControlFile)
696{
697 pg_crc32c crc;
698
699 /* Calculate CRC */
700 INIT_CRC32C(crc);
701 COMP_CRC32C(crc, (char *) ControlFile, offsetof(ControlFileData, crc));
702 FIN_CRC32C(crc);
703
704 /* And simply compare it */
705 if (!EQ_CRC32C(crc, ControlFile->crc))
706 pg_fatal("unexpected control file CRC");
707}
708
709/*
710 * Verify control file contents in the buffer src, and copy it to *ControlFile.
711 */
712static void
713digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
714{
715 if (size != PG_CONTROL_FILE_SIZE)
716 pg_fatal("unexpected control file size %d, expected %d",
717 (int) size, PG_CONTROL_FILE_SIZE);
718
719 memcpy(ControlFile, src, sizeof(ControlFileData));
720
721 /* set and validate WalSegSz */
722 WalSegSz = ControlFile->xlog_seg_size;
723
724 if (!IsValidWalSegSize(WalSegSz))
725 pg_fatal(ngettext("WAL segment size must be a power of two between 1 MB and 1 GB, but the control file specifies %d byte",
726 "WAL segment size must be a power of two between 1 MB and 1 GB, but the control file specifies %d bytes",
727 WalSegSz),
728 WalSegSz);
729
730 /* Additional checks on control file */
731 checkControlFile(ControlFile);
732}
733
734/*
735 * Sync target data directory to ensure that modifications are safely on disk.
736 *
737 * We do this once, for the whole data directory, for performance reasons. At
738 * the end of pg_rewind's run, the kernel is likely to already have flushed
739 * most dirty buffers to disk. Additionally fsync_pgdata uses a two-pass
740 * approach (only initiating writeback in the first pass), which often reduces
741 * the overall amount of IO noticeably.
742 */
743static void
744syncTargetDirectory(void)
745{
746 if (!do_sync || dry_run)
747 return;
748
749 fsync_pgdata(datadir_target, PG_VERSION_NUM);
750}
751