From f28837adf64c38d0ed99e79d49daa25dc8fc4667 Mon Sep 17 00:00:00 2001 From: Gerhard Gonter <ggonter@gmail.com> Date: Sun, 6 Apr 2014 10:24:56 +0200 Subject: [PATCH] updated POD section; update of translation cache --- lib/Redmine/DB/CTX.pm | 163 ++++++++++++++++++++++++++++++++++------ lib/Redmine/DB/MySQL.pm | 83 +++++++++++++++++--- t_sync.pl | 99 +++++++++++++++++------- 3 files changed, 287 insertions(+), 58 deletions(-) diff --git a/lib/Redmine/DB/CTX.pm b/lib/Redmine/DB/CTX.pm index ef9ae6a..7babcfe 100644 --- a/lib/Redmine/DB/CTX.pm +++ b/lib/Redmine/DB/CTX.pm @@ -1,9 +1,31 @@ +=head1 NAME + + Redmine::DB::CTX; + +=head1 DESCRIPTION + +This implements what I call a "Redmine synchronisation context". +It has a synchronisation context id (sync_conext_id), a source (src) +and a destination (dst) which resemble database connections. + +=head1 SYNOPSIS + + my $ctx= new Redmine::DB::CTX ('ctx_id' => $setup->{'sync_context_id'}, 'src' => $src, 'dst' => $dst); + +=cut + package Redmine::DB::CTX; use strict; use parent 'Redmine::DB'; +=head2 $context->sync_project ($source_project_id, $destination_project_id) + +sync one project + +=cut + sub sync_project { my $ctx= shift; @@ -13,21 +35,54 @@ sub sync_project $ctx->sync_project_users ($sp_id, $dp_id); } -=head2 $context->translate ($table_name, $src_id) +=head1 TRANSLATION + +Possibly the most important aspect of a synchronisation job is the +translation of record IDs. This is done using a translation table called +`syncs` which is stored in the destinatios (dst) database. + +The "CREATE TABLE" statements can be found in the source (here) or retrieved via + + my ($ddl_sync_contexts, $ddl_syncs)= Redmine::DB::CTX::get_DDL(); =cut -sub translate + my $TABLE_sync_contexts= <<'EOX'; +CREATE TABLE `sync_contexts` +( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) DEFAULT NULL, + `description` longtext DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; +EOX + + my $TABLE_syncs= <<'EOX'; +CREATE TABLE `syncs` +( + `id` int(11) NOT NULL AUTO_INCREMENT, + `sync_context_id` int(11) DEFAULT NULL, + `table_name` varchar(255) DEFAULT NULL, + `src_id` int(11) DEFAULT NULL, + `dst_id` int(11) DEFAULT NULL, + `sync_date` datetime DEFAULT NULL, + `status` int(4) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; +EOX + +sub get_DDL { return ($TABLE_sync_contexts, $TABLE_syncs); } + +=head2 $context->init_translation() + +Read the known translation table for the give synchronisation context +and keep it around. + +=cut + +sub init_translation { my $ctx= shift; - my $table_name= shift; - my $src_id= shift; - - unless (defined ($src_id)) - { - print "TRANSLATE: table_name=[$table_name] src_id=[undef] tlt=[undef]\n"; - return undef; - } # fetch all known translations, they are stored in the destionation's database my $t; @@ -45,6 +100,29 @@ sub translate } } + return $t; +} + +=head2 $context->translate ($table_name, $src_id) + +translate a table's id. Takes the source's id and returns the +destination's id, if known or undef otherwise. + +=cut + +sub translate +{ + my $ctx= shift; + my $table_name= shift; + my $src_id= shift; + + unless (defined ($src_id)) + { + print "TRANSLATE: table_name=[$table_name] src_id=[undef] tlt=[undef]\n"; + return undef; + } + + my $t= $ctx->init_translation(); if (exists ($t->{$table_name}->{$src_id})) { my $x= $t->{$table_name}->{$src_id}; @@ -55,25 +133,56 @@ sub translate return undef; } -sub store_translate +=head2 $ctx->store_translation ($table_name, $src_id, $dst_id) + +Update translation table in the database and in the cache. + +=cut + +sub store_translation { my $ctx= shift; my $table_name= shift; my $src_id= shift; my $dst_id= shift; + my $t= $ctx->init_translation(); + my $dbh= $ctx->{'dst'}->connect(); return undef unless (defined ($dbh)); - my $ssi= "INSERT INTO syncs (sync_context_id, table_name, src_id, dst_id, sync_date, status) VALUES (?,?,?,?,now(),2)"; - print "ssi=[$ssi]\n"; - my $sth= $dbh->prepare($ssi); - my @vals= ($ctx->{'ctx_id'}, $table_name, $src_id, $dst_id); - print "vals: ", join (',', @vals), "\n"; - $sth->execute(@vals); - $sth->finish(); + # TODO: maybe we need to check if (sync_context_id, table_name, src_id) + # are already present in the database. Then we should update the + # record! + + my $ssi= "INSERT INTO syncs (sync_context_id, table_name, + src_id, dst_id, sync_date, status) VALUES (?,?,?,?,now(),2)"; + print "ssi=[$ssi]\n"; my $sth= $dbh->prepare($ssi); my @vals= + ($ctx->{'ctx_id'}, $table_name, $src_id, $dst_id); print "vals: ", + join (',', @vals), "\n"; $sth->execute(@vals); $sth->finish(); + + $t->{$table_name}->{$src_id}= [ $dst_id, undef, 2 ]; } +=head1 USERS + +User-related tables are: + + (handled) + users + members + member_roles + + (not yet handled) + watchers (TODO: this also points to contents (tickets, wiki_pages, etc.), so this must wait) + user_preferences + +=head2 $context->sync_project_users ($source_project_id, $destination_project_id) + +Synchronize the users and related tables. + +=cut + sub sync_project_users { my $ctx= shift; @@ -118,7 +227,7 @@ sub sync_project_users }; $d_member_id= $dst->insert ('members', $d_member); - $ctx->store_translate('members', $s_member_id, $d_member_id); + $ctx->store_translation('members', $s_member_id, $d_member_id); } } @@ -200,7 +309,7 @@ inherited_from points back to member_roles.id and that record might not be synce print "new member_role record: ", main::Dumper (\%d_mr); $d_mr_id= $ctx->{'dst'}->insert ('member_roles', \%d_mr); - $ctx->store_translate('member_roles', $s_mr_id, $d_mr_id); + $ctx->store_translation('member_roles', $s_mr_id, $d_mr_id); } } @@ -219,7 +328,7 @@ sub sync_role delete ($d_role{'id'}); my $d_role_id= $ctx->{'dst'}->insert ('roles', \%d_role); - $ctx->store_translate('roles', $s_role_id, $d_role_id); + $ctx->store_translation('roles', $s_role_id, $d_role_id); $d_role_id; } @@ -249,7 +358,7 @@ sub sync_user print "cloned_user: ", main::Dumper ($d_user); $d_user_id= $ctx->{'dst'}->insert ('users', $d_user); - $ctx->store_translate('users', $s_user_id, $d_user_id); + $ctx->store_translation('users', $s_user_id, $d_user_id); } $d_user_id; @@ -284,3 +393,13 @@ sync wiki 1; +__END__ + +=head1 TODOs + +=head2 statistics + + We need counters about unchanged, new and updated records. Deleted records may also be necessary. + +=end + diff --git a/lib/Redmine/DB/MySQL.pm b/lib/Redmine/DB/MySQL.pm index 7154158..ab41c10 100644 --- a/lib/Redmine/DB/MySQL.pm +++ b/lib/Redmine/DB/MySQL.pm @@ -9,6 +9,10 @@ use parent 'Redmine::DB'; my $show_query= 0; my $show_fetched= 0; +sub show_fetched { shift; $show_fetched= shift; } +sub show_query { shift; $show_query= shift; } +sub verbose { shift; $show_fetched= $show_query= shift; } + sub connect { my $self= shift; @@ -46,12 +50,12 @@ sub get_all_x # my $project= new Redmine::DB::Project (%par); # print "project: ", main::Dumper ($project); - my $ss = qq/SELECT * FROM $table/; + my $ss= "SELECT * FROM $table"; my @v= (); if (defined ($where)) { - print "where: ", main::Dumper ($where) if ($show_query); + # print "where: ", main::Dumper ($where) if ($show_query); $ss .= ' WHERE ' . shift (@$where); @v= @$where; } @@ -78,6 +82,53 @@ sub get_all_x $t; } +sub insert +{ + my $self= shift; + my $table= shift; + my $record= shift; + + my $dbh= $self->connect(); + return undef unless (defined ($dbh)); + + my (@vars, @vals); + foreach my $an (keys %$record) + { + push (@vars, $an); + push (@vals, $record->{$an}); + } + + my $ssi= "INSERT INTO `$table` (". join (',', @vars) .") VALUES (" . join(',', map { '?' } @vars) . ")"; + print "ssi=[$ssi]\n"; + print "vals: ", join (',', @vals), "\n"; + my $sth= $dbh->prepare($ssi); + $sth->execute(@vals); + print "ERROR: ", $dbh->errstr() if ($dbh->err); + $sth->finish(); + + return $record->{'id'} if (defined ($record->{'id'})); # id attribute was set already + + my $ssq= "SELECT LAST_INSERT_ID()"; + print "ssq=[$ssq]\n"; + $sth= $dbh->prepare($ssq); + $sth->execute(); + print "ERROR: ", $dbh->errstr() if ($dbh->err); + my ($id)= $sth->fetchrow_array(); + print "INSERT: id=[$id]\n"; + + $id; +} + +sub mysql +{ + my $self= shift; + print "self: ", main::Dumper ($self); + + my @cmd= ('mysql', '-h', $self->{'host'}, '-u', $self->{'username'}, $self->{'database'}, '--password='.$self->{'password'}); + print ">> cmd=[", join (' ', @cmd), "]\n"; + system (@cmd); +} + =head1 REDMINE STUFF before that, this might be usable for other Rails applications @@ -89,6 +140,26 @@ TODO: factor out... sub get_all_projects { shift->get_all_x ('projects'); } sub get_all_users { shift->get_all_x ('users'); } +sub get_user +{ + my $self= shift; + my $an= shift; + my $av= shift; + + my $res= $self->get_all_x ('users', [ $an.'=?', $av ]); +} + +sub get_users +{ + my $self= shift; + my $an= shift; + + # print "missing users: [", join (' ', @missing_users), "]\n"; + my $in= $an . ' IN ('. join(',', map { '?' } @_) . ')'; + $show_query= $show_fetched= 1; + $self->get_all_x ('users', [ $in, @_ ]), +} + sub get_project_members { my $self= shift; @@ -131,13 +202,7 @@ sub pcx_members # last if (@missing_users > 3); } - if (@missing_users) - { - # print "missing users: [", join (' ', @missing_users), "]\n"; - my $in= 'id IN ('. join(',', map { '?' } @missing_users) . ')'; - # $show_fetched= 1; - $self->get_all_x ('users', [ $in, @missing_users ]), - } + $res->{'users'}= $self->get_users ('id', @missing_users) if (@missing_users); $res; } diff --git a/t_sync.pl b/t_sync.pl index 1dc42f1..e20708b 100755 --- a/t_sync.pl +++ b/t_sync.pl @@ -28,6 +28,7 @@ use Data::Dumper; $Data::Dumper::Indent= 1; use Redmine::DB::MySQL; +use Redmine::DB::CTX; # use Redmine::DB::Project; nothing here yet ... # yeah, this should be in a config file, but @@ -46,18 +47,24 @@ my $setup= 'config' => '/home/gg/etc/dst/database.yml', 'db' => 'production', }, + 'sync_context_id' => 1, + 'syncs' => + [ + # { 'table' => 'projects', 'src_id' => 170, 'dst_id' => 1 } + { 'table' => 'auth_sources', 'src_id' => 1, 'dst_id' => 1, } + ], 'sync_projects' => [ { 'src_proj' => 170, - 'dst_proj' => 1, + 'dst_proj' => 2, # we could try to retrieve a translation here } ] }; my @parameters= (); my $op_mode= 'usage'; -my %op_modes= map { $_ => 1 } qw(sync sdp prep); +my %op_modes= map { $_ => 1 } qw(sync sdp prep auth mysql user syncuser); while (my $arg= shift (@ARGV)) { @@ -102,22 +109,69 @@ elsif ($op_mode eq 'sdp') # sdp: show destination intance's projects my $dst_proj= $dst->get_all_projects(); print "dst_proj: ", Dumper ($dst_proj); } +elsif ($op_mode eq 'mysql') +{ + my $target= shift (@parameters); + usage() unless (defined ($target)); + my $env= shift (@parameters) || 'production'; + my $cfg= read_configs($setup, $target); + $cfg->mysql(); +} +elsif ($op_mode eq 'auth') +{ + my $src= read_configs($setup, 'src'); + my $dst= read_configs($setup, 'dst'); + + $src->show_fetched (1); + $src->get_all_x ('auth_sources'); + $dst->get_all_x ('auth_sources'); +} +elsif ($op_mode eq 'user') +{ + my $target= shift (@parameters); + usage() unless (defined ($target)); + my $an= shift (@parameters); + usage() unless (defined ($an)); + my $cfg= read_configs($setup, $target); + + foreach my $av (@parameters) + { + my $user= $cfg->get_user ($an, $av); + print "user: ", Dumper ($user); + } + +} +elsif ($op_mode eq 'syncuser') +{ + usage() unless (@parameters); + + my $src= read_configs($setup, 'src'); + my $dst= read_configs($setup, 'dst'); + # my $x_u= $src->get_all_users(); + + my $ctx= new Redmine::DB::CTX ('ctx_id' => $setup->{'sync_context_id'}, 'src' => $src, 'dst' => $dst); + my $res= $src->get_users ('login', @parameters); + print "res: ", Dumper ($res); + foreach my $s_user_id (keys %$res) + { + $ctx->sync_user ($s_user_id, $res->{$s_user_id}); + } +} elsif ($op_mode eq 'sync') { my $src= read_configs($setup, 'src'); my $dst= read_configs($setup, 'dst'); # my $x_u= $src->get_all_users(); + my $ctx= new Redmine::DB::CTX ('ctx_id' => $setup->{'sync_context_id'}, 'src' => $src, 'dst' => $dst); + # print "setup: ", Dumper ($setup); foreach my $sp (@{$setup->{'sync_projects'}}) { - my $proj_id= $sp->{'src_proj'}; print "sp: ", Dumper ($sp); - $src->pcx_members ($proj_id); - my $pcx= $src->pcx_wiki ($proj_id); - print "pcx: ", Dumper ($pcx); - # print "src: ", Dumper ($src); + + $ctx->sync_project ($sp->{'src_proj'}, $sp->{'dst_proj'}); } } elsif ($op_mode eq 'diag') @@ -151,14 +205,14 @@ sub read_configs my $stp= shift; my $s= shift; - my $ss= $stp->{$s}; + my $ss= $stp->{$s}; - my ($yml, $db)= map { $ss->{$_} } qw(config db); + my ($yml, $db)= map { $ss->{$_} } qw(config db); - print "s=[$s] yml=[$yml] db=[$db]\n"; - my $x= YAML::Syck::LoadFile ($yml); - # $ss->{'_cfg'}= - my $c= $x->{$db}; + print "s=[$s] yml=[$yml] db=[$db]\n"; + my $x= YAML::Syck::LoadFile ($yml); + # $ss->{'_cfg'}= + my $c= $x->{$db}; $c->{'adapter'}= 'mysql' if ($c->{'adapter'} eq 'mysql2'); my $m= new Redmine::DB::MySQL (%$c); @@ -176,20 +230,11 @@ sub prepare_sync_table my $dbh= $con->connect(); print "dbh=[$dbh]\n"; - my $ss= <<'EOX'; -CREATE TABLE `syncs` -( - `id` int(11) NOT NULL AUTO_INCREMENT, - `context_id` int(11) DEFAULT NULL, - `table` varchar(255) DEFAULT NULL, - `src_id` int(11) DEFAULT NULL, - `dst_id` int(11) DEFAULT NULL, - `sync_date` datetime DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; -EOX - + my ($ddl_sync_contexts, $ddl_syncs)= Redmine::DB::CTX::get_DDL(); # NOTE: for some reason, this can't be sent to the database, maybe/probably I'm missing something... - print "perform this on the database\n", "--- 8< ---\n", $ss, "--- >8 ---\n"; + print "perform this on the database\n", "--- 8< ---\n", + $ddl_sync_contexts, "\n", + $ddl_syncs, + "--- >8 ---\n"; } -- GitLab