#!/usr/bin/perl
#----------------------------------------------------------------------
#
# renumber_oids.pl
#    Perl script that shifts a range of OIDs in the Postgres catalog data
#    to a different range, skipping any OIDs that are already in use.
#
#    Note: This does not reformat the .dat files, so you may want
#    to run reformat_dat_file.pl afterwards.
#
# Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
# Portions Copyright (c) 1994, Regents of the University of California
#
# src/include/catalog/renumber_oids.pl
#
#----------------------------------------------------------------------

use strict;
use warnings;

use FindBin;
use Getopt::Long;

# Must run in src/include/catalog
chdir $FindBin::RealBin or die "could not cd to $FindBin::RealBin: $!\n";

use lib "$FindBin::RealBin/../../backend/catalog/";
use Catalog;

# We'll need this number.
my $FirstGenbkiObjectId =
  Catalog::FindDefinedSymbol('access/transam.h', '..', 'FirstGenbkiObjectId');

# Process command line switches.
my $output_path      = '';
my $first_mapped_oid = 0;
my $last_mapped_oid  = $FirstGenbkiObjectId - 1;
my $target_oid       = 0;

GetOptions(
	'output=s'           => \$output_path,
	'first-mapped-oid=i' => \$first_mapped_oid,
	'last-mapped-oid=i'  => \$last_mapped_oid,
	'target-oid=i'       => \$target_oid) || usage();

# Sanity check arguments.
die "Unexpected non-switch arguments.\n" if @ARGV;
die "--first-mapped-oid must be specified.\n"
  if $first_mapped_oid <= 0;
die "Empty mapped OID range.\n"
  if $last_mapped_oid < $first_mapped_oid;
die "--target-oid must be specified.\n"
  if $target_oid <= 0;
die "--target-oid must not be within mapped OID range.\n"
  if $target_oid >= $first_mapped_oid && $target_oid <= $last_mapped_oid;

# Make sure output_path ends in a slash.
if ($output_path ne '' && substr($output_path, -1) ne '/')
{
	$output_path .= '/';
}

# Collect all the existing assigned OIDs (including those to be remapped).
my @header_files = (glob("pg_*.h"), qw(indexing.h toasting.h));
my $oids = Catalog::FindAllOidsFromHeaders(@header_files);

# Hash-ify the existing OIDs for convenient lookup.
my %oidhash;
@oidhash{@$oids} = undef;

# Select new OIDs for existing OIDs in the mapped range.
# We do this first so that we preserve the ordering of the mapped OIDs
# (for reproducibility's sake), and so that if we fail due to running out
# of OID room, that happens before we've overwritten any files.
my %maphash;
my $next_oid = $target_oid;

for (
	my $mapped_oid = $first_mapped_oid;
	$mapped_oid <= $last_mapped_oid;
	$mapped_oid++)
{
	next if !exists $oidhash{$mapped_oid};
	$next_oid++
	  while (
		exists $oidhash{$next_oid}
		|| (   $next_oid >= $first_mapped_oid
			&& $next_oid <= $last_mapped_oid));
	die "Reached FirstGenbkiObjectId before assigning all OIDs.\n"
	  if $next_oid >= $FirstGenbkiObjectId;
	$maphash{$mapped_oid} = $next_oid;
	$next_oid++;
}

die "There are no OIDs in the mapped range.\n" if $next_oid == $target_oid;

# Read each .h file and write out modified data.
foreach my $input_file (@header_files)
{
	$input_file =~ /(\w+)\.h$/
	  or die "Input file $input_file needs to be a .h file.\n";
	my $catname = $1;

	# Ignore generated *_d.h files.
	next if $catname =~ /_d$/;

	open(my $ifd, '<', $input_file) || die "$input_file: $!";

	# Write output files to specified directory.
	# Use a .tmp suffix, then rename into place, in case we're overwriting.
	my $output_file     = "$output_path$catname.h";
	my $tmp_output_file = "$output_file.tmp";
	open my $ofd, '>', $tmp_output_file
	  or die "can't open $tmp_output_file: $!";
	my $changed = 0;

	# Scan the input file.
	while (<$ifd>)
	{
		my $line = $_;

		# Check for OID-defining macros that Catalog::ParseHeader knows about,
		# and update OIDs as needed.
		if ($line =~ m/^(DECLARE_TOAST\(\s*\w+,\s*)(\d+)(,\s*)(\d+)\)/)
		{
			my $oid2 = $2;
			my $oid4 = $4;
			if (exists $maphash{$oid2})
			{
				$oid2 = $maphash{$oid2};
				my $repl = $1 . $oid2 . $3 . $oid4 . ")";
				$line =~ s/^DECLARE_TOAST\(\s*\w+,\s*\d+,\s*\d+\)/$repl/;
				$changed = 1;
			}
			if (exists $maphash{$oid4})
			{
				$oid4 = $maphash{$oid4};
				my $repl = $1 . $oid2 . $3 . $oid4 . ")";
				$line =~ s/^DECLARE_TOAST\(\s*\w+,\s*\d+,\s*\d+\)/$repl/;
				$changed = 1;
			}
		}
		elsif (
			$line =~ m/^(DECLARE_(UNIQUE_)?INDEX\(\s*\w+,\s*)(\d+)(,\s*.+)\)/)
		{
			if (exists $maphash{$3})
			{
				my $repl = $1 . $maphash{$3} . $4 . ")";
				$line =~
				  s/^DECLARE_(UNIQUE_)?INDEX\(\s*\w+,\s*\d+,\s*.+\)/$repl/;
				$changed = 1;
			}
		}
		elsif ($line =~ m/^CATALOG\((\w+),(\d+),(\w+)\)/)
		{
			if (exists $maphash{$2})
			{
				my $repl =
				  "CATALOG(" . $1 . "," . $maphash{$2} . "," . $3 . ")";
				$line =~ s/^CATALOG\(\w+,\d+,\w+\)/$repl/;
				$changed = 1;
			}

			if ($line =~ m/BKI_ROWTYPE_OID\((\d+),(\w+)\)/)
			{
				if (exists $maphash{$1})
				{
					my $repl =
					  "BKI_ROWTYPE_OID(" . $maphash{$1} . "," . $2 . ")";
					$line =~ s/BKI_ROWTYPE_OID\(\d+,\w+\)/$repl/;
					$changed = 1;
				}
			}
		}

		# In indexing.h and toasting.h only, check for #define SYM nnnn,
		# and replace if within mapped range.
		elsif ($line =~ m/^(\s*#\s*define\s+\w+\s+)(\d+)\b/)
		{
			if (($catname eq 'indexing' || $catname eq 'toasting')
				&& exists $maphash{$2})
			{
				my $repl = $1 . $maphash{$2};
				$line =~ s/^\s*#\s*define\s+\w+\s+\d+\b/$repl/;
				$changed = 1;
			}
		}

		print $ofd $line;
	}

	close $ifd;
	close $ofd;

	# Avoid updating files if we didn't change them.
	if ($changed || $output_path ne '')
	{
		rename $tmp_output_file, $output_file
		  or die "can't rename $tmp_output_file to $output_file: $!";
	}
	else
	{
		unlink $tmp_output_file
		  or die "can't unlink $tmp_output_file: $!";
	}
}

# Likewise, read each .dat file and write out modified data.
foreach my $input_file (glob("pg_*.dat"))
{
	$input_file =~ /(\w+)\.dat$/
	  or die "Input file $input_file needs to be a .dat file.\n";
	my $catname = $1;

	open(my $ifd, '<', $input_file) || die "$input_file: $!";

	# Write output files to specified directory.
	# Use a .tmp suffix, then rename into place, in case we're overwriting.
	my $output_file     = "$output_path$catname.dat";
	my $tmp_output_file = "$output_file.tmp";
	open my $ofd, '>', $tmp_output_file
	  or die "can't open $tmp_output_file: $!";
	my $changed = 0;

	# Scan the input file.
	while (<$ifd>)
	{
		my $line = $_;

		# Check for oid => 'nnnn', and replace if within mapped range.
		if ($line =~ m/\b(oid\s*=>\s*)'(\d+)'/)
		{
			if (exists $maphash{$2})
			{
				my $repl = $1 . "'" . $maphash{$2} . "'";
				$line =~ s/\boid\s*=>\s*'\d+'/$repl/;
				$changed = 1;
			}
		}

		# Likewise for array_type_oid.
		if ($line =~ m/\b(array_type_oid\s*=>\s*)'(\d+)'/)
		{
			if (exists $maphash{$2})
			{
				my $repl = $1 . "'" . $maphash{$2} . "'";
				$line =~ s/\barray_type_oid\s*=>\s*'\d+'/$repl/;
				$changed = 1;
			}
		}

		print $ofd $line;
	}

	close $ifd;
	close $ofd;

	# Avoid updating files if we didn't change them.
	if ($changed || $output_path ne '')
	{
		rename $tmp_output_file, $output_file
		  or die "can't rename $tmp_output_file to $output_file: $!";
	}
	else
	{
		unlink $tmp_output_file
		  or die "can't unlink $tmp_output_file: $!";
	}
}

sub usage
{
	my $last = $FirstGenbkiObjectId - 1;
	die <<EOM;
Usage: renumber_oids.pl [--output PATH] --first-mapped-oid X [--last-mapped-oid Y] --target-oid Z

Options:
    --output PATH           output directory (default '.')
    --first-mapped-oid X    first OID to be moved
    --last-mapped-oid Y     last OID to be moved (default $last)
    --target-oid Z          first OID to move to

Catalog *.h and *.dat files are updated and written to the
output directory; by default, this overwrites the input files.

Caution: the output PATH will be interpreted relative to
src/include/catalog, even if you start the script
in some other directory.

EOM
}
