#!/usr/bin/perl -w
# Copyright (c) 2007, Perforce Software, Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL PERFORCE SOFTWARE, INC. BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# See PerlDoc in footer of script
use POSIX qw( strftime );
use Time::Local;
package ckp_increment_dates;
use P4::Journal;
my $delta_seconds;
my $report_mode; # Set if in report mode
my $new_start_date; # Set if doing a squash
my $old_start_date;
my $date_factor;
my %date_map = (); # Record previously mapped dates
# When in report mode
my %table_min_dates = (); # Min/max dates per table
my %table_max_dates = (); # Min/max dates per table
my %table_total_dates = ();
my %table_count = ();
our @ISA = qw( P4::Journal );
sub new( $$ ) {
my $class = shift;
my $output = shift;
my $i;
my $self = new P4::Journal;
bless( $self, $class );
open OUTPUT, ">$output" or
die "Could not write to \'" . $output . "\':\n" . $!;
return $self;
}
# A list of all date fields indexed by table name.
my %FIELDMAP = (
'db.change' => [ 'date','access','update' ],
'db.changex' => [ 'date','access','update' ],
'db.configh' => [ 'date' ],
'db.domain' => [ 'updateDate','accessDate' ],
'db.fix' => [ 'date' ],
'db.fixrev' => [ 'date' ],
'db.graphindex' => [ 'date' ],
'db.have' => [ 'time' ],
'db.have.pt' => [ 'time' ],
'db.have.rp' => [ 'time' ],
'db.jnlack' => [ 'lastUpdate' ],
'db.job' => [ 'xdate' ],
'db.monitor' => [ 'startDate' ],
'db.property' => [ 'date' ],
'db.protect' => [ 'update' ],
'db.pubkey' => [ 'update' ],
'db.refhist' => [ 'date' ],
'db.remote' => [ 'update', 'access' ],
'db.repo' => [ 'created', 'pushed' ],
'db.rev' => [ 'date', 'modTime' ],
'db.revbx' => [ 'date', 'modTime' ],
'db.revdx' => [ 'date', 'modTime' ],
'db.revhx' => [ 'date', 'modTime' ],
'db.revpx' => [ 'date', 'modTime' ],
'db.revsh' => [ 'date', 'modTime' ],
'db.revstg' => [ 'date', 'modTime' ],
'db.revsx' => [ 'date', 'modTime' ],
'db.revtx' => [ 'date', 'modTime' ],
'db.revux' => [ 'date', 'modTime' ],
'db.sendq' => [ 'date', 'modTime' ],
'db.sendq.pt' => [ 'modtime', 'date' ],
'db.storage' => [ 'date' ],
'db.storageg' => [ 'date' ],
'db.storagesh' => [ 'date' ],
'db.stream' => [ 'preview' ],
'db.ticket' => [ 'updateDate' ],
'db.ticket.rp' => [ 'updateDate' ],
'db.upgrades' => [ 'startdate', 'enddate' ],
'db.upgrades.rp' => [ 'startdate', 'enddate' ],
'db.user' => [ 'updateDate', 'accessDate', 'endDate', 'passDate', 'passExpire', 'attempts' ],
'db.user.rp' => [ 'updateDate', 'accessDate', 'endDate', 'passDate', 'passExpire', 'attempts' ],
'db.working' => [ 'modTime' ],
'db.workingg' => [ 'modTime' ],
'db.workingx' => [ 'modTime' ],
'pdb.lbr' => [ 'date', 'modTime' ],
'rdb.lbr' => [ 'date', 'modTime' ],
);
# Rev tables with times that need to be updated together - so if same time occurs in any of these then
# need to ensure the new value is the same
my %REV_TABLES = ('db.change' => 1, 'db.changex' => 1, 'db.rev' => 1, 'db.revbx' => 1, 'db.revdx' => 1, 'db.revhx' => 1,
'db.revpx' => 1, 'db.revsh' => 1, 'db.revstg' => 1, 'db.revsx' => 1, 'db.revtx' => 1, 'db.revux' => 1);
# db.bodtext is a spec field which is user defined. The first entry contains the spec definition.
# Ideally we would parse this, but for now we just hard code the field IDs which specify dates
# For example with a spec definition like this:
# @pv@ 1 @db.bodtext@ @job@ 0 0 @Job;code:101;opt:required;rq;len:32;;Status;code:102;type:select;opt:required;rq;len:10;pre:open;val:Open/In_Progress/Closed_Fixed;;Created_by;code:103;opt:once;ro;len:32;pre:$user;;Date_created;code:104;type:date;opt:once;ro;len:20;pre:$now;;Description;code:105;type:text;opt:required;rq;pre:$blank;;Date_modified;code:130;type:date;opt:always;ro;len:20;pre:$now;;Assigned;code:120;opt:required;rq;len:32;pre:$user;;@
# We can see that the fields of type 'date' are 104 (Date_created) and 130 (Date_modified)
my %BODTEXT_DATE_FIELDS = (104 => 1, 130 => 1);
# Map from old date range to new date range, returning cached val if appropriate - to avoid minor
# discrepancies.
sub map_date( $$ ) {
my $tableName = shift;
my $val = shift;
my $result = int((($val - $old_start_date) * $date_factor + $new_start_date));
if (exists($REV_TABLES{$tableName})) {
if (exists($date_map{$val})) {
$result = $date_map{$val};
} else {
$date_map{$val} = $result;
}
}
return $result;
}
sub ParseRecord( $ ) {
my $self = shift;
my $rec = shift;
my $op;
my $jver;
my $dbName;
my $remainder;
if ( $rec->Raw() ) {
($op, $jver, $dbName, $remainder) = split " ", $rec->Raw(), 4;
SWITCH: {
if( !defined $dbName ) { last SWITCH; }
if( !defined $rec->Raw() ) { last SWITCH; }
# Special processing for job fields as user defined spec - see comment where BODTEXT_DATE_FIELDS is declared
if( $dbName eq "\@db.bodtext@" ) {
my $attr = $rec->FetchField( 'attr' );
if( exists($BODTEXT_DATE_FIELDS{$attr}) ) {
my $table = 'db.bodtext';
my $val = $rec->FetchField( 'text' );
if ( $report_mode ) {
$table_count{$table}++;
$table_total_dates{$table} += $val;
$table_min_dates{$table} = $val if (!exists($table_min_dates{$table}) || $table_min_dates{$table} gt $val);
$table_max_dates{$table} = $val if (!exists($table_max_dates{$table}) || $table_max_dates{$table} lt $val);
} elsif ($new_start_date ne 0) {
# If we don't add a space the P4::Journal treats as an integer and doesn't quote it!
my $new_val = sprintf( " %012d", map_date($table, $val) );
$rec->SetField( 'text', $new_val );
} else {
my $new_val = sprintf( " %012d", $val + $delta_seconds );
$rec->SetField( 'text', $new_val );
}
}
last SWITCH;
}
foreach my $table ( keys %FIELDMAP ) {
if( $dbName eq "\@$table@" ) {
foreach my $fName ( @{$FIELDMAP{ $table }} ) {
my $val = $rec->FetchField( $fName );
if( defined($val) && $val ne 0 ) {
if ( $report_mode ) {
$table_count{$table}++;
$table_total_dates{$table} += $val;
$table_min_dates{$table} = $val if (!exists($table_min_dates{$table}) || $table_min_dates{$table} gt $val);
$table_max_dates{$table} = $val if (!exists($table_max_dates{$table}) || $table_max_dates{$table} lt $val);
} elsif ($new_start_date ne 0) {
$rec->SetField( $fName, map_date($table, $val) );
} else {
$rec->SetField( $fName, $val + $delta_seconds );
}
}
}
last SWITCH;
}
}
}
}
printf OUTPUT "%s\n", $rec->Raw();
}
sub DESTROY
{
close OUTPUT;
}
package main;
use Getopt::Long 'HelpMessage';
$report_mode = 0;
GetOptions(
'delta=i' => \(my $delta=1),
'units=s' => \(my $units='y'),
'old_start_date=s' => \(my $old_startstr=""),
'new_start_date=s' => \(my $new_startstr=""),
'report' => \($report_mode),
'help' => sub { HelpMessage(0) },
) or HelpMessage(1);
my $output = shift;
if (!defined $output) { die "You must supply an output file name.\n"; }
if ($units !~ /y|m|w|d/) {
die "Units must be one of: y,m,w,d";
}
if ((($new_startstr ne "") && ($old_startstr eq "")) || (($new_startstr eq "") && ($old_startstr ne ""))) {
die "Need to specify both --min_date and --new_start"
}
$new_start_date = 0;
$old_start_date = 0;
if ($new_startstr ne "") {
if ($new_startstr =~ /(\d{4})\/(\d{1,2})\/(\d{1,2})/) {
my $year = $1;
my $month = $2;
my $day = $3;
$new_start_date = timelocal(0, 0, 0, $day, $month - 1, $year);
} else {
die "Can't parse $new_startstr as YYYY/mm/dd";
}
if ($old_startstr =~ /(\d{4})\/(\d{1,2})\/(\d{1,2})/) {
my $year = $1;
my $month = $2;
my $day = $3;
$old_start_date = timelocal(0, 0, 0, $day, $month - 1, $year);
} else {
die "Can't parse $old_startstr as YYYY/mm/dd";
}
my $time_now = timelocal(gmtime());
$date_factor = ($time_now - $new_start_date) / ($time_now - $old_start_date);
}
if ($units eq "y") {
$delta_seconds = $delta * 365 * 24 * 60 * 60;
} elsif ($units eq "m") {
$delta_seconds = $delta * 30 * 24 * 60 * 60;
} elsif ($units eq "w") {
$delta_seconds = $delta * 7 * 24 * 60 * 60;
} else {
$delta_seconds = $delta * 24 * 60 * 60;
}
my $ckp = new ckp_increment_dates( $output );
$ckp->Parse;
if ($report_mode) {
for (keys %table_count) {
printf "Count Table date %s: %d\n", $_, $table_count{$_};
}
for (keys %table_count) {
my $avg = $table_total_dates{$_} / $table_count{$_};
printf "Average Table date %s: %d %s\n", $_, $avg,
POSIX::strftime("%Y-%m-%d %H:%M:%S", gmtime($avg));
}
for (keys %table_min_dates) {
printf "Min (non-zero) Table date %s: %d %s\n", $_, $table_min_dates{$_},
POSIX::strftime("%Y-%m-%d %H:%M:%S", gmtime($table_min_dates{$_}));
}
for (keys %table_max_dates) {
printf "Max (non-zero) Table date %s: %d %s\n", $_, $table_max_dates{$_},
POSIX::strftime("%Y-%m-%d %H:%M:%S", gmtime($table_max_dates{$_}));;
}
}
=head1 NAME
ckp_increment_dates - increment dates in a checkpoint
=head1 SYNOPSIS
ckp_increment_dates.pl [ --delta <no units> --units [y/m/w/d] | --report | --start <yyyy/mm/dd> ] <output-file>
--delta,-d Specify delta as an integer value (default=1)
--units,-u Specify units: y=year, m=month, w=week, d=day, (default=y)
--report,-r Report on dates per table (mutually exclus)
--old_start_date Specifies a YYYY/MM/DD date - as old as any date in the file
--new_start_date Specifies new YYYY/MM/DD to use as earliest date for increment and squash all dates
into the time period between then and now
--help,-h Print this help
<output-file> Specify output file (or '-' for stdout)
Increment all dates in a checkpoint by a fixed delta, or squash into a new range.
Only relevant date fields on certain records are updated.
The P4::Journal module must be installed as this script heavily uses that
module. Be careful about running this script on large checkpoints - may take a while!
Examples:
Increment an uncompressed checkpoint by a delta of 1 year and
save the results to an uncompressed checkpoint:
cat customer.ckp | ckp_increment_dates.pl --delta 1 --units y incremented.ckp
Increment a compressed checkpoint, squashing the dates into a new time period, and saving them to a compressed checkpoint:
cat customer.ckp.gz | gunzip | ckp_increment_dates.pl --start 2020/01/01 - | gzip > incremented.ckp.gz
=cut