use English qw( -no_match_vars );
use Getopt::Long;
use List::MoreUtils qw(none);
+use List::UtilsBy qw(partition_by);
use Pod::Usage;
+use Rose::DB::Object 0.809;
use Term::ANSIColor;
use SL::Auth;
my %config;
-our %foreign_key_name_map = (
- oe => { payment => 'payment_terms', },
- ar => { payment => 'payment_terms', },
- ap => { payment => 'payment_terms', },
-
- orderitems => { parts => 'part', trans => 'order', },
- delivery_order_items => { parts => 'part' },
- invoice => { parts => 'part' },
- follow_ups => { 'employee_obj' => 'created_for' },
-
- periodic_invoices_configs => { oe => 'order' },
+# Maps column names in tables to foreign key relationship names. For
+# example:
+#
+# »follow_up_access« contains a column named »who«. Rose normally
+# names the resulting relationship after the class the target table
+# uses. In this case the target table is »employee« and the
+# corresponding class SL::DB::Employee. The resulting relationship
+# would be named »employee«.
+#
+# In order to rename this relationship we have to map »who« to
+# e.g. »granted_by«:
+# follow_up_access => { who => 'granted_by' },
+
+our %foreign_key_name_map = (
+ KIVITENDO => {
+ oe => { payment_id => 'payment_terms', },
+ ar => { payment_id => 'payment_terms', },
+ ap => { payment_id => 'payment_terms', },
+
+ orderitems => { parts_id => 'part', trans_id => 'order', },
+ delivery_order_items => { parts_id => 'part' },
+ invoice => { parts_id => 'part' },
+ follow_ups => { created_for_user => 'created_for', created_by => 'created_by', },
+ follow_up_access => { who => 'with_access', what => 'to_follow_ups_by', },
+
+ periodic_invoices_configs => { oe_id => 'order' },
+ },
);
sub setup {
}
}
+sub fix_relationship_names {
+ my ($domain, $table, $fkey_text) = @_;
+
+ if ($fkey_text !~ m/key_columns \s+ => \s+ \{ \s+ ['"]? ( [^'"\s]+ ) /x) {
+ die "fix_relationship_names: could not extract the key column for domain/table $domain/$table; foreign key definition text:\n${fkey_text}\n";
+ }
+
+ my $column_name = $1;
+ my %changes = map { %{$_} } grep { $_ } ($foreign_key_name_map{$domain}->{ALL}, $foreign_key_name_map{$domain}->{$table});
+
+ if (my $desired_name = $changes{$column_name}) {
+ $fkey_text =~ s/^ \s\s [^\s]+ \b/ ${desired_name}/msx;
+ }
+
+ return $fkey_text;
+}
+
sub process_table {
- my @spec = @_;
- my $table = $spec[0];
+ my ($domain, $table, $package) = @_;
my $schema = '';
($schema, $table) = split(m/\./, $table) if $table =~ m/\./;
- my $package = ucfirst($spec[1] || $spec[0]);
+ $package = ucfirst($package || $table);
$package =~ s/_+(.)/uc($1)/ge;
my $meta_file = "${meta_path}/${package}.pm";
my $mngr_file = "${manager_path}/${package}.pm";
$foreign_key_definition =~ s/::AUTO::/::/g;
if ($foreign_key_definition && ($definition =~ /\Q$foreign_key_definition\E/)) {
+ # These positions refer to the whole setup call, not just the
+ # parameters/actual relationship definitions.
my ($start, $end) = ($-[0], $+[0]);
- while (my ($auto_generated_name, $desired_name) = each %{ $foreign_key_name_map{$table} || {} }) {
- $foreign_key_definition =~ s/^ \s \s ${auto_generated_name} \b/ ${desired_name}/msx;
- }
+ # Match the function parameters = the actual relationship
+ # definitions
+ next unless $foreign_key_definition =~ m/\(\n(.+)\n\)/s;
- # Sort foreign key definitions alphabetically
- if ($foreign_key_definition =~ m/\(\n(.+)\n\)/s) {
- my ($list_start, $list_end) = ($-[0], $+[0]);
- my @foreign_keys = split m/\n\n/m, $1;
- my $sorted_foreign_keys = "(\n" . join("\n\n", sort @foreign_keys) . "\n)";
+ my ($list_start, $list_end) = ($-[0], $+[0]);
- substr $foreign_key_definition, $list_start, $list_end - $list_start, $sorted_foreign_keys;;
- }
+ # Split the whole chunk on double new lines. The resulting
+ # elements are one relationship each. Then fix the relationship
+ # names and sort them by their new names.
+ my @new_foreign_keys = sort map { fix_relationship_names($domain, $table, $_) } split m/\n\n/m, $1;
- substr($definition, $start, $end - $start) = $foreign_key_definition;
+ # Replace the function parameters = the actual relationship
+ # definitions with the new ones.
+ my $sorted_foreign_keys = "(\n" . join("\n\n", @new_foreign_keys) . "\n)";
+ substr $foreign_key_definition, $list_start, $list_end - $list_start, $sorted_foreign_keys;
+
+ # Replace the whole setup call in the auto-generated output with
+ # our new version.
+ substr $definition, $start, $end - $start, $foreign_key_definition;
}
$definition =~ s/(meta->table.*)\n/$1\n$schema_str/m if $schema;
my $old_md5 = md5_hex($orig_file);
my $new_size = length $full_definition;
my $new_md5 = md5_hex($full_definition);
- if ($old_size == $new_size && $old_md5 == $new_md5) {
+ if ($old_size == $new_size && $old_md5 eq $new_md5) {
notice("No changes in $meta_file, skipping.") unless $config{quiet};
return;
}
GetOptions(
'client=s' => \ my $client,
all => \ my $all,
+ 'db=s' => \ my $db,
'no-commit|dry-run' => \ my $nocommit,
help => sub { pod2usage(verbose => 99, sections => 'NAME|SYNOPSIS|OPTIONS') },
quiet => \ my $quiet,
$options->{client} = $client;
$options->{all} = $all;
+ $options->{db} = $db;
$options->{nocommit} = $nocommit;
$options->{quiet} = $quiet;
$options->{color} = -t STDOUT ? 1 : 0;
}
sub make_tables {
- my @tables;
+ my %tables_by_domain;
if ($config{all}) {
- my $db = SL::DB::create(undef, 'KIVITENDO');
- @tables = grep { my $table = $_; none { $_ eq $table } @{ $blacklist{KIVITENDO} } } $db->list_tables;
+ my @domains = $config{db} ? (uc $config{db}) : sort keys %package_names;
+
+ foreach my $domain (@domains) {
+ my $db = SL::DB::create(undef, $domain);
+ $tables_by_domain{$domain} = [ grep { my $table = $_; none { $_ eq $table } @{ $blacklist{$domain} } } $db->list_tables ];
+ $db->disconnect;
+ }
} elsif (@ARGV) {
- @tables = @ARGV;
+ %tables_by_domain = partition_by {
+ my ($domain, $table) = split m{:};
+ $table ? uc($domain) : 'KIVITENDO';
+ } @ARGV;
+
+ foreach my $tables (values %tables_by_domain) {
+ s{.*:}{} for @{ $tables };
+ }
+
} else {
error("You specified neither --all nor any specific tables.");
usage();
}
- @tables;
+ return %tables_by_domain;
}
sub error {
print @_, $/;
}
+sub check_errors_in_package_names {
+ foreach my $domain (sort keys %package_names) {
+ my @both = grep { $package_names{$domain}->{$_} } @{ $blacklist{$domain} || [] };
+ next unless @both;
+
+ print "Error: domain '$domain': The following table names are present in both the black list and the package name hash: ", join(' ', sort @both), "\n";
+ exit 1;
+ }
+}
+
parse_args(\%config);
setup();
-my @tables = make_tables();
+check_errors_in_package_names();
-my @unknown_tables = grep { !$package_names{KIVITENDO}->{$_} } @tables;
-if (@unknown_tables) {
- error("The following tables do not have entries in \%SL::DB::Helper::Mappings::kivitendo_package_names: " . join(' ', sort @unknown_tables));
- exit 1;
-}
+my %tables_by_domain = make_tables();
-process_table($_, $package_names{KIVITENDO}->{$_}) for @tables;
+foreach my $domain (keys %tables_by_domain) {
+ my @tables = @{ $tables_by_domain{$domain} };
+ my @unknown_tables = grep { !$package_names{$domain}->{$_} } @tables;
+ if (@unknown_tables) {
+ error("The following tables do not have entries in \%SL::DB::Helper::Mappings::${domain}_package_names: " . join(' ', sort @unknown_tables));
+ exit 1;
+ }
+
+ process_table($domain, $_, $package_names{$domain}->{$_}) for @tables;
+}
1;
=head1 SYNOPSIS
- scripts/rose_create_model.pl --client name-or-id table1 [table2 ...]
- scripts/rose_create_model.pl --client name-or-id [--all|-a]
+ scripts/rose_auto_create_model.pl --client name-or-id [db1:]table1 [[db2:]table2 ...]
+ scripts/rose_auto_create_model.pl --client name-or-id [--all|-a]
# updates all models
- scripts/rose_create_model.pl --client name-or-id --all
+ scripts/rose_auto_create_model.pl --client name-or-id --all [--db db]
# updates only customer table, login taken from config
- scripts/rose_create_model.pl customer
+ scripts/rose_auto_create_model.pl customer
# updates only parts table, package will be Part
- scripts/rose_create_model.pl parts=Part
+ scripts/rose_auto_create_model.pl parts=Part
# try to update parts, but don't do it. tell what would happen in detail
- scripts/rose_create_model.pl --no-commit parts
+ scripts/rose_auto_create_model.pl --no-commit parts
=head1 DESCRIPTION
file. The first one will be updated if the schema changes, the second
and third ones will only be created if it they do not exist.
+=head1 DATABASE NAMES AND TABLES
+
+If you want to generate the data for specific tables only then you
+have to list them on the command line. The format is
+C<db-name:table-name>. The part C<db-name:> is optional and defaults
+to C<KIVITENDO:> – which means the tables in the default kivitendo
+database.
+
+Valid database names are keys in the hash returned by
+L<SL::DB::Helper::Mappings/get_package_names>.
+
=head1 OPTIONS
=over 4
Process all tables from the database. Only those that are blacklistes in
L<SL::DB::Helper::Mappings> are excluded.
+=item C<--db db>
+
+In combination with C<--all> causes all tables in the specific
+database to be processed, not in all databases.
+
=item C<--no-commit, -n>
=item C<--dry-run>