#!/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 --units [y/m/w/d] | --report | --start ] --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 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