File Coverage

lib/UR/Namespace/Command/Update/ClassesFromDb.pm
Criterion Covered Total %
statement 30 607 4.9
branch 0 270 0.0
condition 0 73 0.0
subroutine 10 25 40.0
pod 1 4 25.0
total 41 979 4.1


line stmt bran cond sub pod time code
1              
2             package UR::Namespace::Command::Update::ClassesFromDb;
3              
4 2     2   27252 use strict;
  2         3  
  2         50  
5 2     2   9 use warnings;
  2         4  
  2         40  
6 2     2   8 use UR;
  2         2  
  2         13  
7             our $VERSION = "0.46"; # UR $VERSION;
8 2     2   833 use Text::Diff;
  2         11799  
  2         3851  
9              
10             UR::Object::Type->define(
11             class_name => __PACKAGE__,
12             is => 'UR::Namespace::Command::RunsOnModulesInTree',
13             has => [
14             data_source => { is => 'List', is_optional => 1, doc => 'Limit updates to these data sources' },
15             force_check_all_tables => { is => 'Boolean', is_optional => 1, doc => 'By default we only look at tables with a new DDL time for changed database schema information. This explicitly (slowly) checks each table against our cache.' },
16             force_rewrite_all_classes => { is => 'Boolean', is_optional => 1, doc => 'By default we only rewrite classes where there are database changes. Set this flag to rewrite all classes even where there are no schema changes.' },
17             table_name => { is => 'List', is_optional => 1, doc => 'Update the specified table.' },
18             class_name => { is => 'List', is_optional => 1, doc => 'Update only the specified classes.' },
19             ],
20             );
21              
22 0     0 0   sub sub_command_sort_position { 2 };
23              
24             sub help_brief {
25 0     0 0   "Update class definitions (and data dictionary cache) to reflect changes in the database schema."
26             }
27              
28             sub help_detail {
29 0     0 0   return <
30              
31             Reads from the data sources in the current working directory's namespace,
32             and updates the local class tree.
33              
34             This hits the data dictionary for the remote database, and gets changes there
35             first. Those changes are then used to mutate the class tree.
36              
37             If specific data sources are specified on the command-line, it will limit
38             its database examination to just data in those data sources. This command
39             will, however, always load ALL classes in the namespace when doing this update,
40             to find classes which currently reference the updated table, or are connected
41             to its class indirectly.
42              
43             EOS
44             }
45              
46              
47              
48             sub create {
49 0     0 1   my($class,%params) = @_;
50              
51 0           for my $param_name (qw/data_source class_name table_name/) {
52 0 0 0       if (exists $params{$param_name} && ! ref($params{$param_name})) {
53             # Make sure the data_source parameter is always a listref, even if there's only one item
54 0           $params{$param_name} = [ $params{$param_name} ];
55             }
56             }
57              
58             # This is used by the test case to turn on no-commit for the metadata DB,
59             # but still have _sync_filesystem write out the modules
60 0           my $override = delete $params{'_override_no_commit_for_filesystem_items'};
61              
62 0           my $obj = $class->SUPER::create(%params);
63 0 0         return unless $obj;
64              
65 0 0         $obj->{'_override_no_commit_for_filesystem_items'} = $override if $override;
66              
67 0           return $obj;
68             }
69              
70              
71             our @dd_classes = (
72             'UR::DataSource::RDBMS::Table',
73             'UR::DataSource::RDBMS::TableColumn',
74             'UR::DataSource::RDBMS::FkConstraint',
75             'UR::DataSource::RDBMS::Table::Ghost',
76             'UR::DataSource::RDBMS::TableColumn::Ghost',
77             'UR::DataSource::RDBMS::FkConstraint::Ghost',
78             );
79              
80             sub execute {
81 0     0     my $self = shift;
82              
83             #
84             # Command parameter checking
85             #
86            
87 0           my $force_check_all_tables = $self->force_check_all_tables;
88 0           my $force_rewrite_all_classes = $self->force_rewrite_all_classes;
89            
90 0           my $namespace = $self->namespace_name;
91 0           $self->status_message("Updating namespace: $namespace\n");
92              
93 0           my @namespace_data_sources = $namespace->get_data_sources;
94              
95 0           my $specified_table_name_arrayref = $self->table_name;
96 0           my $specified_data_source_arrayref = $self->data_source;
97 0           my $specified_class_name_arrayref = $self->class_name;
98            
99            
100 0           my @data_dictionary_objects;
101            
102 0 0 0       if ($specified_class_name_arrayref or $specified_table_name_arrayref) {
103 0           my $ds_table_list;
104 0 0         if ($specified_class_name_arrayref) {
105             $ds_table_list = [
106 0           map { [$_->data_source, $_->table_name] }
107 0           grep { $_->data_source }
108 0           map { $_->__meta__ }
  0            
109             @$specified_class_name_arrayref
110             ];
111             }
112             else {
113             $ds_table_list = [
114 0           map { [$_->data_source, $_->table_name] }
  0            
115             UR::DataSource::RDBMS::Table->get(table_name => $specified_table_name_arrayref)
116             ];
117 0           for my $item (@$ds_table_list) {
118 0           UR::Object::Type->get(data_source => $item->[0], table_name => $item->[1]);
119             }
120             }
121              
122 0           for my $item (@$ds_table_list) {
123 0           my ($data_source, $table_name) = @$item;
124 0           $self->_update_database_metadata_objects_for_schema_changes(
125             data_source => $data_source,
126             force_check_all_tables => $force_check_all_tables,
127             table_name => $table_name,
128             );
129 0           for my $dd_class (qw/UR::DataSource::RDBMS::Table UR::DataSource::RDBMS::FkConstraint UR::DataSource::RDBMS::TableColumn/) {
130 0           push @data_dictionary_objects,
131             $dd_class->get(data_source_obj => $data_source, table_name => $table_name);
132             }
133             }
134             }
135             else {
136             # Do the update by data source, all or whatever is specified.
137            
138             #
139             # Determine which data sources to update from.
140             # By default, we do all datasources owned by the namespace.
141             #
142            
143 0           my @target_data_sources;
144 0 0         if ($specified_data_source_arrayref) {
145 0           @target_data_sources = ();
146 0           my %data_source_is_specified = map { $_ => 1 } @$specified_data_source_arrayref;
  0            
147 0           for my $ds (@namespace_data_sources) {
148 0 0         if ($data_source_is_specified{$ds->id}) {
149 0           push @target_data_sources, $ds;
150 0           delete $data_source_is_specified{$ds->id};
151             }
152             }
153             #delete @data_source_is_specified{@namespace_data_sources};
154 0 0         if (my @unknown = keys %data_source_is_specified) {
155             $self->error_message(
156             "Unknown data source(s) for namespace $namespace: @unknown!\n"
157             . "Select from:\n"
158 0           . join("\n",map { $_->id } @namespace_data_sources)
  0            
159             . "\n"
160             );
161 0           return;
162             }
163             } else {
164             # Don't update the Meta datasource, unless they specificly asked for it
165 0           @target_data_sources = grep { $_->id !~ /::Meta$/ } @namespace_data_sources;
  0            
166             }
167              
168             # Some data sources can't handle the magic required for automatic class updating...
169 0           @target_data_sources = grep { $_->can('get_table_names') } @target_data_sources;
  0            
170            
171             $self->status_message("Found data sources: "
172             . join(", " ,
173 0 0         map { /${namespace}::DataSource::(.*)$/; $1 || $_ }
  0            
174 0           map { $_->id }
  0            
175             @target_data_sources
176             )
177             );
178            
179             #
180             # A copy of the database metadata is in the ::Meta sqlite datasource.
181             # Get updates to it first.
182             #
183            
184             ##$DB::single = 1;
185            
186 0           for my $data_source (@target_data_sources) {
187             # ensure the class has been lazy-loaded until UNIVERSAL::can is smarter...
188 0           $data_source->class;
189 0           $self->status_message("Checking " . $data_source->id . " for schema changes ...");
190 0           my $success =
191             $self->_update_database_metadata_objects_for_schema_changes(
192             data_source => $data_source,
193             force_check_all_tables => $force_check_all_tables,
194             );
195 0 0         unless ($success) {
196 0           return;
197             }
198             }
199              
200             #
201             # Summarize the database changes by table. We'll create/update/delete the class which goes with that table.
202             #
203              
204             ##$DB::single = 1;
205              
206 0           my $cx = UR::Context->current;
207 0           for my $dd_class (qw/UR::DataSource::RDBMS::Table UR::DataSource::RDBMS::FkConstraint UR::DataSource::RDBMS::TableColumn/) {
208             push @data_dictionary_objects,
209 0 0         grep { $force_rewrite_all_classes ? 1 : $_->__changes__ or exists($_->{'db_saved_uncommitted'}) }
  0 0          
210             $cx->all_objects_loaded($dd_class);
211              
212 0           my $ghost_class = $dd_class . "::Ghost";
213 0           push @data_dictionary_objects, $cx->all_objects_loaded($ghost_class);
214             }
215             }
216            
217             # The @data_dictionary_objects array has all dd meta which should be used to rewrite classes.
218            
219 0           my %changed_tables;
220 0           for my $obj (
221             @data_dictionary_objects
222             ) {
223 0           my $table;
224 0 0 0       if ($obj->can("get_table")) {
    0          
225 0           $table = $obj->get_table;
226 0 0         unless ($table) {
227 0           Carp::confess("No table object for $obj" . $obj->id);
228             }
229             }
230             elsif ($obj->isa("UR::DataSource::RDBMS::Table") or $obj->isa("UR::DataSource::RDBMS::Table::Ghost")) {
231 0           $table = $obj
232             }
233             # we may find no table if it was dropped, and this is one of its old cols/constraints
234 0 0         next unless $table;
235              
236 0           $changed_tables{$table->id} = 1;
237             }
238              
239              
240             # Some ill-behaved modules might set no_commit to true at compile time.
241             # Reset it back to whatever it is now after going through the namespace's modules
242             # Note that when we have class info in the metadata DB, this probably won't be
243             # necessary anymore since we won't have to actually load up the .pm files to
244             # discover classes in the namespace
245            
246 0           my $remembered_no_commit_setting = UR::DBI->no_commit();
247 0           my $remembered_dummy_ids_setting = UR::DataSource->use_dummy_autogenerated_ids();
248              
249              
250             #
251             # Update the classes based-on changes to the database schemas
252             #
253              
254             ##$DB::single = 1;
255              
256 0 0         if (@data_dictionary_objects) {
257 0 0         $self->status_message("Found " . keys(%changed_tables) . " tables with changes.") unless $force_rewrite_all_classes;
258 0           $self->status_message("Resolving corresponding class changes...");
259 0           my $success =
260             $self->_update_class_metadata_objects_to_match_database_metadata_changes(
261             data_dictionary_objects => \@data_dictionary_objects
262             );
263 0 0         unless ($success) {
264 0           return;
265             }
266             }
267             else {
268 0           $self->status_message("No data schema changes.");
269             }
270              
271 0           UR::DBI->no_commit($remembered_no_commit_setting);
272 0           UR::DataSource->use_dummy_autogenerated_ids($remembered_dummy_ids_setting);
273              
274              
275             #
276             # The namespace module may have special rules for creating classes from regular (non-schema) data.
277             # At this point we allow the namespace to adjust the class tree as it chooses.
278             #
279              
280 0           $namespace->class;
281 0 0 0       if (
      0        
      0        
282             $namespace->can("_update_classes_from_data_sources")
283             and not $specified_table_name_arrayref
284             and not $specified_class_name_arrayref
285             and not $specified_data_source_arrayref
286             ) {
287 0           $self->status_message("Checking for custom changes for the $namespace namespace...");
288 0           $namespace->_update_classes_from_data_sources();
289             }
290              
291 0           $self->status_message("Saving metadata changes...");
292 0           my $sync_success = UR::Context->_sync_databases();
293 0 0         unless ($sync_success) {
294             ##$DB::single = 1;
295 0           $self->error_message("Metadata sync_database failed");
296 0           UR::Context->_rollback_databases();
297 0           return;
298             }
299              
300             #
301             # Re-write the class headers for changed classes.
302             # Output a summary report of what has been changed.
303             # This block of logic shold be part of saving class data.
304             # Right now, it's done with a _load() override, no data_source, and this block of code. :(
305             #
306              
307             ##$DB::single = 1;
308              
309 0           my $cx = UR::Context->current;
310 0           my @changed_class_meta_objects;
311             my %changed_classes;
312 0           my $module_update_success = eval {
313 0           for my $meta_class (qw/
314             UR::Object::Type
315             UR::Object::Property
316             /) {
317 0           push @changed_class_meta_objects, grep { $_->__changes__ } $cx->all_objects_loaded($meta_class);
  0            
318              
319 0           my $ghost_class = $meta_class . "::Ghost";
320 0           push @changed_class_meta_objects, $cx->all_objects_loaded($ghost_class);
321             }
322              
323 0           for my $obj (
324             @changed_class_meta_objects
325             ) {
326 0           my $class_name = $obj->class_name;
327 0 0         next unless $class_name; #if $obj is a ghost, class_name might return undef?
328 0           $changed_classes{$class_name} = 1;
329             }
330 0 0         unless (@changed_class_meta_objects) {
331 0           $self->status_message("No class changes.");
332             }
333              
334 0           my $changed_class_count = scalar(keys %changed_classes);
335 0 0         my $subj = $changed_class_count == 1 ? "class" : "classes";
336 0           $self->status_message("Resolved changes for $changed_class_count $subj");
337              
338 0           $self->status_message("Updating the filesystem...");
339 0           my $success = $self->_sync_filesystem(
340             changed_class_names => [sort keys %changed_classes],
341             );
342 0           return $success;
343             };
344              
345 0 0         if ($@) {
    0          
346 0           $self->error_message("Error updating the filesystem: $@");
347 0           return;
348             }
349             elsif (!$module_update_success) {
350 0           $self->status_message("Error updating filesystem!");
351 0           return;
352             }
353            
354 0           $self->status_message("Filesystem update complete.");
355            
356              
357             #
358             # This commit actually records the data dictionary changes in the ::Meta datasource sqlite database.
359             #
360              
361 0           $self->status_message("Committing changes to data sources...");
362              
363 0 0         unless (UR::Context->_commit_databases()) {
364             ##$DB::single = 1;
365 0           $self->error_message("Metadata commit failed");
366 0           return;
367             }
368              
369              
370             #
371             # The logic below is only necessary if this process is run as part of some larger process.
372             # Right now that includes the automated test for this module.
373             # After classes have been updated they won't function properly.
374             # Ungenerate and re-generate each of the classes we touched, so that it functions according to its new spec.
375             #
376              
377 0           $self->status_message("Cleaning up.");
378              
379 0           my $success = 1;
380 0           for my $class_name (sort keys %changed_classes) {
381 0           my $class_obj = UR::Object::Type->get($class_name);
382 0 0         next unless $class_obj;
383 0           $class_obj->ungenerate;
384 0 0         Carp::confess("class $class_name didn't ungenerate properly") if $class_obj->generated;
385 0 0         unless (eval { $class_obj->generate } ) {
  0            
386 0           $self->warning_message("Class $class_name didn't re-generate properly: $@");
387 0           $success = 0;
388             }
389             }
390              
391 0 0         unless ($success) {
392 0           $self->status_message("Errors occurred re-generating some classes after update.");
393 0           return;
394             }
395              
396             #
397             # Done
398             #
399              
400 0           $self->status_message("Update complete.");
401 0           return 1;
402             }
403              
404             #
405             # The execute() method above is broken into three parts:
406             # ->_update_database_metadata_objects_for_schema_changes()
407             # ->_update_class_metadata_objects_to_match_database_metadata_changes()
408             # ->_sync_filesystem()
409             #
410              
411             sub _update_database_metadata_objects_for_schema_changes {
412 0     0     my ($self, %params) = @_;
413 0           my $data_source = delete $params{data_source};
414 0           my $force_check_all_tables = delete $params{force_check_all_tables};
415 0           my $table_name = delete $params{table_name};
416 0 0         die "unknown params " . Dumper(\%params) if keys %params;
417              
418             #$data_source = $data_source->class;
419              
420 0           my @changed;
421              
422 0           my $last_ddl_time_for_table_name = {};
423 0 0 0       if ($data_source->can("get_table_last_ddl_times_by_table_name") and !$force_check_all_tables) {
424             # the driver implements a way to get the last DDL time
425 0           $last_ddl_time_for_table_name = $data_source->get_table_last_ddl_times_by_table_name;
426             }
427              
428             # from the cache of known tables
429 0           my @previous_table_names = $data_source->get_table_names;
430 0           my %previous_table_names = map { $_ => 1 } @previous_table_names;
  0            
431              
432             # from the database now
433 0           my @current_table_names = $data_source->_get_table_names_from_data_dictionary();
434 0           my %current_table_names = map { s/"|'//g; $_ => $_ } @current_table_names;
  0            
  0            
435              
436 0 0         my %all_table_names = $table_name
437             ? ( $table_name => 1 )
438             : ( %current_table_names, %previous_table_names);
439              
440 0           my $new_object_revision = $UR::Context::current->now();
441              
442             # handle tables which are new/updated by updating the class
443 0           my (@create,@delete,@update);
444 0           my $pattern = '%-42s';
445 0           my ($dsn) = ($data_source->id =~ /^.*::DataSource::(.*?)$/);
446 0           for my $table_name (keys %all_table_names) {
447 0           my $last_actual_ddl_time = $last_ddl_time_for_table_name->{$table_name};
448              
449 0           my $table_object;
450             my $last_recorded_ddl_time;
451 0           my $last_object_revision;
452              
453 0           my $db_table_name = $current_table_names{$table_name};
454              
455 0           eval {
456             #($table_object) = $data_source->get_tables(table_name => $table_name);
457              
458             # Using the above doesn't account for a table switching databases, which happens.
459             # Once the data source is _part_ of the id we'll just have a delete/add, but for now it's an update.
460 0           $table_object = UR::DataSource::RDBMS::Table->get(data_source => $data_source->id,
461             table_name => $table_name);
462             };
463              
464 0 0 0       if ($current_table_names{$table_name} and not $table_object) {
    0 0        
    0 0        
465             # new table
466 0           push @create, $table_name;
467 0 0         $self->status_message(
468             sprintf(
469             "A $pattern Schema changes " . ($last_actual_ddl_time ? "on $last_actual_ddl_time" : ""),
470             $dsn . " " . $table_name
471             )
472             );
473 0           my $table_object = $data_source->refresh_database_metadata_for_table_name($db_table_name);
474 0 0         next unless $table_object;
475              
476 0           $table_object->last_ddl_time($last_ddl_time_for_table_name->{$table_name});
477             }
478             elsif ($current_table_names{$table_name} and $table_object) {
479             # retained table
480             # either we know it changed, or we can't know, so update it anyway
481 0 0 0       if (! exists $last_ddl_time_for_table_name->{$table_name} or
      0        
482             ! defined $table_object->last_ddl_time or
483             $last_ddl_time_for_table_name->{$table_name} gt $table_object->last_ddl_time
484             ) {
485 0   0       my $last_update = $table_object->last_ddl_time || $table_object->last_object_revision;
486 0   0       my $this_update = $last_ddl_time_for_table_name->{$table_name} || "";
487 0           my $table_object = $data_source->refresh_database_metadata_for_table_name($db_table_name);
488 0 0         unless ($table_object) {
489             ##$DB::single = 1;
490 0           print;
491             }
492 0           my @changes =
493             # grep { not ($_->properties == 1 and ($_->properties)[0] eq "last_object_revision") }
494             $table_object->__changes__;
495 0 0         if (@changes) {
496 0           $self->status_message(
497             sprintf("U $pattern Last updated on $last_update. Newer schema changes on $this_update."
498             , $dsn . " " . $table_name
499             )
500             );
501 0           push @update, $table_name;
502             }
503 0           $table_object->last_ddl_time($last_ddl_time_for_table_name->{$table_name});
504             }
505             }
506             elsif ($table_object and not $current_table_names{$table_name}) {
507             # deleted table
508 0           push @delete, $table_name;
509 0   0       $self->status_message(
510             sprintf(
511             "D $pattern Last updated on %s. Table dropped.",
512             $dsn . " " . $table_name,
513             $last_object_revision || ""
514             )
515             );
516 0           my $table_object = UR::DataSource::RDBMS::Table->get(
517             data_source => $data_source->id,
518             table_name => $table_name,
519             );
520 0           $table_object->delete;
521             }
522             else {
523 0           Carp::confess("Unable to categorize table $table_name as new/old/deleted?!");
524             }
525             }
526              
527 0           return 1;
528             }
529              
530              
531              
532             # Keep a cache of class meta objects so we don't have to keep asking the
533             # object system to do it for us. This should be a speed optimization because
534             # the asking eventually filters down to calling get_material_classes() on the
535             # namespace which can be extremely slow. If it's not in the cache, defer to
536             # asking the data source
537             sub _get_class_meta_for_table_name {
538 0     0     my($self,%param) = @_;
539              
540 0           my $data_source = $param{'data_source'};
541 0           my $data_source_name = $data_source->get_name();
542 0           my $table_name = $param{'table_name'};
543              
544             my ($obj) =
545 0           grep { not $_->isa("UR::Object::Ghost") }
  0            
546             UR::Object::Type->is_loaded(
547             data_source_id => $data_source,
548             table_name => $table_name
549             );
550 0 0         return $obj if $obj;
551              
552              
553 0 0         unless ($self->{'_class_meta_cache'}{$data_source_name}) {
554             my @classes =
555 0           grep { not $_->class_name->isa('UR::Object::Ghost') }
  0            
556             UR::Object::Type->get(data_source_id => $data_source);
557            
558 0           for my $class (@classes) {
559 0           my $table_name = $class->table_name;
560 0 0         next unless $table_name;
561 0           $self->{'_class_meta_cache'}->{$data_source_name}->{$table_name} = $class;
562             }
563             }
564            
565 0           $obj = $self->{'_class_meta_cache'}->{$data_source_name}->{$table_name};
566 0 0         return $obj if $obj;
567 0           return;
568             }
569              
570              
571             sub _update_class_metadata_objects_to_match_database_metadata_changes {
572 0     0     my ($self, %params) = @_;
573              
574 0           my $data_dictionary_objects = delete $params{data_dictionary_objects};
575 0 0         if (%params) {
576 0           $self->error_message("Unknown params!");
577 0           return;
578             }
579              
580             #
581             # INITIALIZATION AND SANITY CHECKING
582             #
583              
584 0           my $namespace = $self->namespace_name;
585              
586 0           $self->status_message("Updating classes...");
587              
588 0           my %dd_changes_by_class = (
589             'UR::DataSource::RDBMS::Table' => [],
590             'UR::DataSource::RDBMS::TableColumn' => [],
591             'UR::DataSource::RDBMS::FkConstraint' => [],
592             'UR::DataSource::RDBMS::Table::Ghost' => [],
593             'UR::DataSource::RDBMS::TableColumn::Ghost' => [],
594             'UR::DataSource::RDBMS::FkConstraint::Ghost' => [],
595             );
596 0           for my $changed_obj (@$data_dictionary_objects) {
597 0           my $changed_class = $changed_obj->class;
598 0           my $bucket = $dd_changes_by_class{$changed_class};
599 0           push @$bucket, $changed_obj;
600             }
601 2     2   14 my $sorter = sub($$) { no warnings 'uninitialized';
  2         4  
  2         1239  
602 0 0   0     $_[0]->table_name cmp $_[1]->table_name || $_[0]->id cmp $_[1]->id
603 0           };
604              
605             # FKs are special, in that they might change names, but we use the name as the "id".
606             # This should change, really, but until it does we need to identify them by their "content",
607              
608             #
609             # DELETIONS
610             #
611              
612             # DELETED FK CONSTRAINTS
613             # Just detach the object reference meta-data from the constraint.
614             # We only actually delete references when their properties all go away,
615             # which can happen when the columns go away (through table deletion or alteration).
616             # It can also happen when one of the involved classes is deleted, which never happens
617             # automatically.
618            
619 0           for my $fk (sort $sorter @{ $dd_changes_by_class{'UR::DataSource::RDBMS::FkConstraint::Ghost'} }) {
  0            
620 0 0         unless ($fk->table_name) {
621 0           $self->status_message(sprintf("~ No table name for deleted foreign key constraint %-32s\n", $fk->id));
622 0           next;
623             }
624              
625 0           my $table = $fk->get_table;
626 0           my $class = $self->_get_class_meta_for_table_name(data_source => $table->data_source,
627             table_name => $table->table_name);
628              
629 0 0         unless ($class) {
630             ##$DB::single = 1;
631 0           $self->status_message(sprintf("~ No class found for deleted foreign key constraint %-32s %-32s\n",$table->table_name, $fk->id));
632 0           next;
633             }
634 0           my $class_name = $class->class_name;
635 0           my $property = UR::Object::Property->get(class_name => $class_name, constraint_name => $fk->fk_constraint_name);
636 0 0         unless ($property) {
637 0           $self->status_message(sprintf("~ No property found for deleted foreign key constraint %-32s %-32s class $class_name\n",
638             $table->table_name, $fk->fk_constraint_name));
639 0           next;
640             }
641 0           $property->delete;
642            
643             }
644              
645             # DELETED UNIQUE CONSTRAINTS
646             # DELETED PK CONSTRAINTS
647             # We do nothing here, because we don't track these as individual DD objects, just values on the table object.
648             # If a table changes constraints, that is handled below after table/column add/update.
649             # If a table is dropped entirely, we leave all pk/unique constraints in place,
650             # since, if the class is not manually deleted by the developer, it should continue
651             # to function as it did before.
652              
653             # DELETED COLUMNS
654 0           my @saved_removed_column_messages; # Delete them now, but report about them later in the 'Updating class properties' section
655 0           for my $column (sort $sorter @{ $dd_changes_by_class{"UR::DataSource::RDBMS::TableColumn::Ghost"} }) {
  0            
656 0           my $table = $column->get_table;
657 0 0         unless ($table) {
658 0           $self->status_message(sprintf("~ No table found for deleted column %-32s\n", $column->id));
659 0           next;
660             }
661 0           my $column_name = $column->column_name;
662              
663 0           my $class = $self->_get_class_meta_for_table_name(data_source => $table->data_source,
664             table_name => $table->table_name);
665 0 0         unless ($class) {
666 0           $self->status_message(sprintf("~ No class found for deleted column %-32s %-32s\n", $table->table_name, $column_name));
667 0           next;
668             }
669 0           my $class_name = $class->class_name;
670              
671 0           my ($property) = $class->direct_property_metas(
672             column_name => $column_name
673             );
674 0 0         unless ($property) {
675 0           $self->status_message(sprintf("~ No property found for deleted column %-32s %-32s\n",$table->table_name, $column_name));
676 0           next;
677             }
678              
679 0 0         unless ($table->isa("UR::DataSource::RDBMS::Table::Ghost")) {
680 0           push(@saved_removed_column_messages,
681             sprintf("D %-40s property %-16s for removed column %s.%s\n",
682             $class->class_name,
683             $property->property_name,
684             $column->table_name,
685             $column->column_name,
686             )
687             );
688             }
689              
690 0           $property->delete;
691              
692 0 0         unless ($property->isa("UR::DeletedRef")) {
693 0           Carp::confess("Error deleting property " . $property->id);
694             }
695             }
696              
697             # DELETED TABLES
698 0           my %classes_with_deleted_tables;
699 0           for my $table (sort $sorter @{ $dd_changes_by_class{"UR::DataSource::RDBMS::Table::Ghost"} }) {
  0            
700             # Though we create classes for tables, we don't immediately delete them, just deflate them.
701 0           my $table_name = $table->table_name;
702 0 0         unless ($table_name) {
703 0           $self->status_message("~ No table_name for deleted table object ".$table->id);
704 0           next;
705             }
706              
707 0 0         if (not defined UR::Context->_get_committed_property_value($table,'table_name')) {
708 0           print Data::Dumper::Dumper($table);
709             ##$DB::single = 1;
710             }
711             # FIXME should this use $data_source->get_class_meta_for_table($table) instead?
712 0           my $committed_data_source_id = UR::Context->_get_committed_property_value($table,'data_source');
713 0           my $committed_table_name = UR::Context->_get_committed_property_value($table,'table_name');
714 0           my $class = UR::Object::Type->get(
715             data_source_id => $committed_data_source_id,
716             table_name => $committed_table_name,
717             );
718 0 0         unless ($class) {
719 0           $self->status_message(sprintf("~ No class found for deleted table %-32s" . "\n",$table->id));
720 0           next;
721             }
722 0           $classes_with_deleted_tables{$table_name} = $class;
723 0           $class->data_source(undef);
724 0           $class->table_name(undef);
725             } # next deleted table
726              
727 0           for my $table_name (keys %classes_with_deleted_tables) {
728 0           my $class = $classes_with_deleted_tables{$table_name};
729 0           my $class_name = $class->class_name;
730              
731 0           my %ancestory = map { $_ => 1 } $class->inheritance;
  0            
732             my @ancestors_with_tables =
733             grep {
734 0   0       $a = UR::Object::Type->get(class_name => $_)
  0            
735             || UR::Object::Type::Ghost->get(class_name => $_);
736 0 0         $a && $a->table_name;
737             } sort keys %ancestory;
738 0 0         if (@ancestors_with_tables) {
739 0           $self->status_message(
740             sprintf("U %-40s class is now detached from deleted table %-32s. It still inherits from classes with persistent storage." . "\n",$class_name,$table_name)
741             );
742             }
743             else {
744 0           $self->status_message(
745             sprintf("D %-40s class deleted for deleted table %s" . "\n",$class_name,$table_name)
746             );
747             }
748             } # next deleted table
749              
750             # This is the data structure used by _get_class_meta_for_table_name
751             # There's a bad interaction with software transactions that can lead
752             # to this cache containing deleted class objects if the caller holds
753             # on to a reference to this command object and repetedly calls execute()
754             # but rolls back transactions between those calls.
755 0           $self->{'_class_meta_cache'} = {};
756              
757             ##$DB::single = 1;
758              
759             #
760             # EXISTING DD OBJECTS
761             #
762             # TABLE
763 0           for my $table (sort $sorter @{ $dd_changes_by_class{"UR::DataSource::RDBMS::Table"} }) {
  0            
764 0           my $table_name = $table->table_name;
765 0           my $data_source = $table->data_source;
766              
767 0           my $class = $self->_get_class_meta_for_table_name(data_source => $data_source,
768             table_name => $table_name);
769            
770 0 0         if ($class) {
771             # update
772              
773 0 0         if ($class->data_source ne $table->data_source) {
774 0           $class->data_source($table->data_source);
775             }
776              
777 0           my $class_name = $class->class_name;
778 2     2   13 no warnings;
  2         3  
  2         1102  
779 0 0         if ($table->remarks ne UR::Context->_get_committed_property_value($table,'remarks')) {
780 0           $class->doc($table->remarks);
781             }
782 0 0         if ($table->data_source ne UR::Context->_get_committed_property_value($table,'data_source')) {
783 0           $class->data_source($table->data_source);
784             }
785            
786 0 0         if ($class->__changes__) {
787 0           $self->status_message(
788             sprintf("U %-40s class uses %s %s %s" . "\n",
789             $class_name,
790             $table->data_source->get_name,
791             lc($table->table_type),
792             $table_name)
793             );
794             }
795             }
796             else {
797             # create
798 0           my $data_source = $table->data_source;
799 0 0         my $data_source_id = (ref $data_source ? $data_source->id : $data_source);
800 0           my $class_name = $data_source->resolve_class_name_for_table_name($table_name,$table->table_type);
801 0 0         unless ($class_name) {
802 0           Carp::confess(
803             "Failed to resolve a class name for new table "
804             . $table_name
805             );
806             }
807              
808             # if the original table_name was empty (ie. not backed by a table), and the
809             # new one actually has a table, then this is just another schema change and
810             # not an error. Set the table_name attribute and go on...
811 0           my $class = UR::Object::Type->get(class_name => $class_name);
812 0 0         my $prev_table_name = ($class ? $class->table_name : undef);
813 0 0         my $prev_data_source_id = ($class ? $class->data_source_id : undef);
814 0 0 0       if ($class && $prev_table_name) {
815              
816 0           Carp::confess(
817             "Class $class_name already exists for table '$prev_table_name' in $prev_data_source_id."
818             . " Cannot generate class for $table_name in $data_source_id."
819             );
820             }
821              
822             $self->status_message(
823 0           sprintf("A %-40s class uses %s %s %s" . "\n",
824             $class_name,
825             $table->data_source->get_name,
826             lc($table->table_type),
827             $table_name)
828             );
829              
830 0 0         if ($class) {
831 0 0         $class->doc($table->remarks ? $table->remarks: undef);
832 0           $class->data_source($data_source);
833 0           $class->table_name($table_name);
834 0           $class->er_role($table->er_type);
835             } else {
836 0 0         $class = UR::Object::Type->create(
837             class_name => $class_name,
838             doc => ($table->remarks ? $table->remarks: undef),
839             data_source_id => $data_source,
840             table_name => $table_name,
841             er_role => $table->er_type,
842             # generate => 0,
843             );
844 0 0         unless ($class) {
845 0           Carp::confess(
846             "Failed to create class $class_name for new table "
847             . $table_name
848             . ". " . UR::Object::Type->error_message
849             );
850             }
851             }
852             }
853             } # next table
854              
855 0           $self->status_message("Updating direct class properties...\n");
856              
857 0           $self->status_message($_) foreach @saved_removed_column_messages;
858              
859             # COLUMN
860            
861 0           for my $column (sort $sorter @{ $dd_changes_by_class{'UR::DataSource::RDBMS::TableColumn'} }) {
  0            
862 0           my $table = $column->get_table;
863 0           my $column_name = $column->column_name;
864 0           my $data_source = $table->data_source;
865 0           my($ur_data_type, $default_length) = @{ $data_source->ur_data_type_for_data_source_data_type($column->data_type) };
  0            
866 0 0         my $ur_data_length = defined($column->data_length) ? $column->data_length : $default_length;
867              
868 0           my $class = $self->_get_class_meta_for_table_name(data_source => $data_source,
869             table_name => $table->table_name);
870              
871 0 0         unless ($class) {
872 0           $class = $self->_get_class_meta_for_table_name(data_source => $data_source,
873             table_name => $table->table_name);
874 0 0         Carp::confess("Class object missing for table " . $table->table_name) unless $class;
875             }
876 0           my $class_name = $class->class_name;
877 0           my $property;
878 0           foreach my $prop_object ( $class->direct_property_metas ) {
879 0 0 0       if (defined $prop_object->column_name and lc($prop_object->column_name) eq lc($column_name)) {
880 0           $property = $prop_object;
881 0           last;
882             }
883             }
884              
885             # We care less whether the column is new/updated, than whether there is property metadata for it.
886 0 0         if ($property) {
887             my @column_property_translations = (
888             # [ column_name, property_name, conversion_sub(column_obj, value) ]
889             ['data_length' => 'data_length',
890             # lengths for these data types are based on the number of bytes used internally in the
891             # database. The UR-based objects will store the text version, which will always be longer,
892             # making $obj->__errors__() complain about the length being out of bounds
893 0 0   0     sub { my ($c, $av) = @_; defined($av) ? $av : ($c->is_time_data ? undef : $ur_data_length) } ],
  0 0          
894             ['data_type' => 'data_type',
895 0 0   0     sub { my ($c, $av) = @_; defined($ur_data_type) ? $ur_data_type : $av } ],
  0            
896             ['nullable' => 'is_optional',
897 0 0 0 0     sub { my ($c, $av) = @_; (defined($av) and ($av eq "Y")) ? 1 : 0 } ],
  0            
898             ['remarks' => 'doc',
899             # Ideally this would only use DB value ($av) if the last_ddl_time was newer.
900 0 0   0     sub { my ($c, $av) = @_; defined($av) ? $av : $property->doc } ],
  0            
  0            
901             );
902             # update
903 0           for my $translation (@column_property_translations) {
904 0           my ($column_attr, $property_attr, $conversion_sub) = @$translation;
905 0   0       $property_attr ||= $column_attr;
906              
907 2     2   12 no warnings;
  2         3  
  2         142  
908 0 0         if (UR::Context->_get_committed_property_value($column,$column_attr) ne $column->$column_attr) {
909 0 0         if ($conversion_sub) {
910 0           $property->$property_attr($conversion_sub->($column, $column->$column_attr));
911             }
912             else {
913 0           $property->$property_attr($column->$column_attr);
914             }
915             }
916             }
917              
918 0 0         if ($property->__changes__) {
919 2     2   8 no warnings;
  2         2  
  2         278  
920 0           $self->status_message(
921             sprintf("U %-40s property %-20s for column %s.%s (%s %s)\n",
922             $class_name,
923             $property->property_name,
924             $table->table_name,
925             $column_name,
926             $column->data_type,
927             $column->data_length)
928             );
929             }
930             }
931             else {
932             # create
933 0           my $property_name = $data_source->resolve_property_name_for_column_name($column->column_name);
934 0 0         unless ($property_name) {
935 0           Carp::confess(
936             "Failed to resolve a property name for new column "
937             . $column->column_name
938             );
939             }
940              
941 0           my $create_exception;
942 0           for (my $attempt = 0; $attempt < 3; $attempt++) {
943 0 0         $property_name = '_' . $property_name if $attempt;
944              
945 0           $create_exception = do {
946 0           local $@;
947 0           eval {
948 0 0         $property = UR::Object::Property->create(
949             class_name => $class_name,
950             property_name => $property_name,
951             column_name => $column_name,
952             data_type => $ur_data_type,
953             data_length => $ur_data_length,
954             is_optional => $column->nullable eq "Y" ? 1 : 0,
955             is_volatile => 0,
956             doc => $column->remarks,
957             is_specified_in_module_header => 1,
958             );
959             };
960 0           $@;
961             };
962 0 0         last if $property;
963             }
964              
965 2     2   14 no warnings 'uninitialized';
  2         2  
  2         2512  
966 0           $self->status_message(
967             sprintf("A %-40s property %-16s for column %s.%s (%s %s)\n",
968             $class_name,
969             $property->property_name,
970             $table->table_name,
971             $column_name,
972             $column->data_type,
973             $column->data_length)
974             );
975            
976 0 0         unless ($property) {
977 0 0         if ($create_exception =~ m/An object of class UR::Object::Property already exists/) {
978 0           $self->warning_message("Conflicting property names already exist in class $class_name for column $column_name in table ".$table->table_name);
979             } else {
980 0           Carp::confess(
981             "Failed to create property $property_name on class $class_name. "
982             . UR::Object::Property->error_message
983             );
984             }
985             }
986             }
987             } # next column
988              
989 0           $self->status_message("Updating class ID properties...\n");
990              
991             # PK CONSTRAINTS (loop table objects again, since the DD doesn't do individual ID objects)
992 0           for my $table (sort $sorter @{ $dd_changes_by_class{'UR::DataSource::RDBMS::Table'} }) {
  0            
993             # created/updated/unchanged
994             # delete and re-create these objects: they're "bridges", so no developer supplied data is presesent
995 0           my $table_name = $table->table_name;
996              
997 0           my $class = $self->_get_class_meta_for_table_name(data_source => $table->data_source,
998             table_name => $table_name);
999 0           my $class_name = $class->class_name;
1000 0           my @properties = UR::Object::Property->get(class_name => $class_name);
1001              
1002 0 0         unless (@properties) {
1003 0           $self->warning_message("no properties on class $class_name?");
1004             ##$DB::single = 1;
1005             }
1006              
1007 0           my @expected_pk_cols = grep { defined }
1008 0           map { $_->column_name }
1009 0           grep { defined $_->is_id }
  0            
1010             @properties;
1011            
1012 0           my @pk_cols = $table->primary_key_constraint_column_names;
1013            
1014 0 0         if ("@expected_pk_cols" eq "@pk_cols") {
1015 0           next;
1016             }
1017            
1018 0 0         unless (@pk_cols) {
1019             # If there are no primary keys defined, then treat _all_ the columns
1020             # as primary keys. This means we don't support multiple rows in a
1021             # table containing the same data.
1022 0           @pk_cols = $table->column_names;
1023             }
1024              
1025 0           my %pk_cols;
1026 0           for my $pos (1 .. @pk_cols) {
1027 0           my $pk_col = $pk_cols[$pos-1];
1028 0 0         my ($property) = grep { defined($_->column_name) and ($_->column_name eq $pk_col) } @properties;
  0            
1029            
1030 0 0         unless ($property) {
1031             # the column has been removed
1032 0           next;
1033             }
1034 0           $pk_cols{$property->property_name} = $pos;
1035             }
1036              
1037             # all primary key properties are non-nullable, regardless of what the DB allows
1038 0           for my $property (@properties) {
1039 0           my $name = $property->property_name;
1040 0 0         if ($pk_cols{$name}) {
1041 0           $property->is_optional(0);
1042 0           $property->is_id($pk_cols{$name});
1043             }
1044             }
1045             } # next table (looking just for PK constraint changes)
1046              
1047             # Make another pass to make sure if a class has a property called 'id' with a column attached,
1048             # then it must be the only ID property of that class
1049 0           my %classes_to_check_id_properties;
1050 0           foreach my $thing ( qw(UR::DataSource::RDBMS::Table UR::DataSource::RDBMS::TableColumn ) ) {
1051 0           foreach my $item ( @{ $dd_changes_by_class{$thing} } ) {
  0            
1052 0           my $class_meta = $self->_get_class_meta_for_table_name(data_source => $item->data_source,
1053             table_name => $item->table_name);
1054 0   0       $classes_to_check_id_properties{$class_meta->class_name} ||= $class_meta;
1055             }
1056             }
1057 0           foreach my $class_name ( keys %classes_to_check_id_properties ) {
1058 0           my $class_meta = $classes_to_check_id_properties{$class_name};
1059 0           my $property_meta = $class_meta->property_meta_for_name('id');
1060 0 0 0       if ($property_meta && $property_meta->column_name && scalar($class_meta->direct_id_property_metas) > 1) {
      0        
1061 0           $self->warning_message("Class $class_name cannot have multiple ID properties when one concrete ID property is named 'id'. It will likely not function correctly unless it is renamed");
1062             }
1063 0 0         unless (defined $property_meta->is_id) {
1064 0           $self->warning_message("Class $class_name has a property named 'id' that is not an ID property. It will likely not function correctly unless it is renamed");
1065             }
1066             }
1067            
1068              
1069              
1070 0           $self->status_message("Updating class unique constraints...\n");
1071              
1072             # UNIQUE CONSTRAINT / UNIQUE INDEX -> UNIQUE GROUP (loop table objecs since we have no PK DD objects)
1073 0           for my $table (sort $sorter @{ $dd_changes_by_class{'UR::DataSource::RDBMS::Table'} }) {
  0            
1074             # created/updated/unchanged
1075             # delete and re-create
1076              
1077 0           my $class = $self->_get_class_meta_for_table_name(data_source => $table->data_source,
1078             table_name => $table->table_name);
1079 0           my $class_name = $class->class_name;
1080              
1081 0           my @properties = UR::Object::Property->get(class_name => $class_name);
1082              
1083 0           my @uc_names = $table->unique_constraint_names;
1084 0           for my $uc_name (@uc_names)
1085             {
1086 0           eval { $class->remove_unique_constraint($uc_name) };
  0            
1087 0 0         if ($@ =~ m/There is no constraint named/) {
    0          
1088 0           next; # it's OK if there's no UR metadata for this constraint yet
1089             } elsif ($@) {
1090 0           die $@;
1091             }
1092              
1093 0 0         my @uc_cols = map { ref($_) ? @$_ : $_ } $table->unique_constraint_column_names($uc_name);
  0            
1094 0           my @uc_property_names;
1095 0           for my $uc_col (@uc_cols)
1096             {
1097 0 0         my ($property) = grep { defined($_->column_name) and ($_->column_name eq $uc_col) } @properties;
  0            
1098 0 0         unless ($property) {
1099 0           $self->warning_message("No property found for column $uc_col for unique constraint $uc_name");
1100             #$DB::single = 1;
1101 0           next;
1102             }
1103 0           push @uc_property_names, $property->property_name;
1104             }
1105 0           $class->add_unique_constraint($uc_name, @uc_property_names);
1106             }
1107             } # next table (checking separately for unique constraints)
1108              
1109              
1110             # FK CONSTRAINTS
1111             # These often change name, and as such need to be identified by their actual content.
1112             # Each constraint must match some relationship in the system, or a new one will be added.
1113              
1114 0           $self->status_message("Updating class relationships...\n");
1115              
1116 0           my $last_class_name = '';
1117             FK:
1118 0           for my $fk (sort $sorter @{ $dd_changes_by_class{'UR::DataSource::RDBMS::FkConstraint'} }) {
  0            
1119              
1120 0           my $table = $fk->get_table;
1121 0           my $data_source = $fk->data_source;
1122              
1123 0           my $table_name = $fk->table_name;
1124 0           my $r_table_name = $fk->r_table_name;
1125              
1126 0           my $class = $self->_get_class_meta_for_table_name(data_source => $data_source,
1127             table_name => $table_name);
1128 0 0         unless ($class) {
1129 0           $self->warning_message(
1130             sprintf("No class found for table for foreign key constraint %-32s %s" . "\n",$table_name, $fk->id)
1131             );
1132 0           next;
1133             }
1134              
1135 0           my $r_class = $self->_get_class_meta_for_table_name(data_source => $data_source,
1136             table_name => $r_table_name);
1137 0 0         unless ($r_class) {
1138 0           $self->warning_message(
1139             sprintf("No class found for r_table for foreign key constraint %-32s %-32s" . "\n",$r_table_name, $fk->id)
1140             );
1141 0           next;
1142             }
1143              
1144 0           my $class_name = $class->class_name;
1145 0           my $r_class_name = $r_class->class_name;
1146              
1147             # Create an object-accessor property to go with this FK
1148             # First we have to figure out a proper delegation name
1149             # which is a rather convoluted process
1150              
1151 0           my @column_names = $fk->column_names;
1152 0           my @r_column_names = $fk->r_column_names;
1153 0           my (@properties,@property_names,@r_properties,@r_property_names,$prefix,$suffix,$matched);
1154 0           foreach my $i ( 0 .. $#column_names ) {
1155 0           my $column_name = $column_names[$i];
1156 0           my $property = UR::Object::Property->get(
1157             class_name => $class_name,
1158             column_name => $column_name,
1159             );
1160 0 0         unless ($property) {
1161 0           Carp::confess("Failed to find a property for column $column_name on class $class_name");
1162             }
1163 0           push @properties,$property;
1164 0           my $property_name = $property->property_name;
1165 0           push @property_names,$property_name;
1166              
1167 0           my $r_column_name = $r_column_names[$i];
1168 0           my $r_property = UR::Object::Property->get(
1169             class_name => $r_class_name,
1170             column_name => $r_column_name,
1171             );
1172 0 0         unless ($r_property) {
1173 0           Carp::cluck("Failed to find a property for column $r_column_name on class $r_class_name");
1174             #$DB::single = 1;
1175 0           next FK;
1176             }
1177 0           push @r_properties,$r_property;
1178 0           my $r_property_name = $r_property->property_name;
1179 0           push @r_property_names,$r_property_name;
1180              
1181 0 0 0       if ($property_name =~ /^(.*)$r_property_name(.*)$/
1182             or $property_name =~ /^(.*)_id$/) {
1183              
1184 0           $prefix = $1;
1185 0 0         $prefix =~ s/_$//g if defined $prefix;
1186 0           $suffix = $2;
1187 0 0         $suffix =~ s/^_//g if defined $suffix;
1188 0           $matched = 1;
1189             }
1190             }
1191              
1192 0           my @r_class_name_parts = split('::', $r_class->class_name);
1193 0           shift @r_class_name_parts; # drop the namespace name
1194 0           my $delegation_name = lc(join('_', @r_class_name_parts));
1195              
1196 0 0         if ($matched) {
1197 0 0         $delegation_name = $delegation_name . "_" . $prefix if $prefix;
1198 0 0         $delegation_name .= ($suffix !~ /\D/ ? "" : "_") . $suffix if $suffix;
    0          
1199             }
1200             else {
1201 0           $delegation_name = join("_", @property_names) . "_" . $delegation_name;
1202             }
1203              
1204             # Generate a delegation name that dosen't conflict with another already in use
1205 0           my %property_names_used = map { $_ => 1 }
  0            
1206             $class->all_property_names;
1207 0           while($property_names_used{$delegation_name}) {
1208 0           $delegation_name =~ /^(.*?)(\d*)$/;
1209 0 0         $delegation_name = $1 . ( ($2 ? $2 : 0) + 1 );
1210             }
1211              
1212             # FK columns may have been in an odd order. Get the reference columns in ID order.
1213 0           for my $i (0..$#column_names)
1214             {
1215 0           my $column_name = $column_names[$i];
1216 0           my $property = $properties[$i];
1217 0           my $property_name = $property_names[$i];
1218              
1219 0           my $r_column_name = $r_column_names[$i];
1220 0           my $r_property = $r_properties[$i];
1221 0           my $r_property_name = $r_property_names[$i];
1222             }
1223              
1224             # Pick a name that isn't already a property in that class
1225             PICK_A_NAME:
1226 0           for ( 1 ) {
1227 0 0         if (UR::Object::Property->get(class_name => $class_name,
1228             property_name => $delegation_name)) {
1229 0 0         if (UR::Object::Property->get(class_name => $class_name,
1230             property_name => $delegation_name.'_obj')) {
1231 0           foreach my $i ( 1 .. 10 ) {
1232 0 0         unless (UR::Object::Property->get(class_name => $class_name,
1233             property_name => $delegation_name."_$i")) {
1234 0           $delegation_name .= "_$i";
1235 0           last PICK_A_NAME;
1236             }
1237             }
1238 0           $self->warning_message("Can't generate a relationship property name for $class_name table name $table_name constraint_name ",$fk->fk_constraint_name);
1239 0           next FK;
1240             } else {
1241 0           $delegation_name = $delegation_name.'_obj';
1242             }
1243             }
1244             }
1245              
1246 0 0         unless ($class->property_meta_for_name($delegation_name)) {
1247 0           my $property = UR::Object::Property->create(class_name => $class_name,
1248             property_name => $delegation_name,
1249             data_type => $r_class_name,
1250             id_by => \@property_names,
1251             constraint_name => $fk->fk_constraint_name,
1252             is_delegated => 1,
1253             is_specified_in_module_header => 1,
1254             );
1255 2     2   10 no warnings;
  2         2  
  2         1346  
1256 0           $self->status_message(
1257             sprintf("A %-40s property %-16s id by %-16s (%s)\n",
1258             $class_name,
1259             $delegation_name,
1260             join(',',@property_names),
1261             $r_class_name
1262             )
1263             );
1264             }
1265              
1266             } # next fk constraint
1267              
1268 0           return 1;
1269             }
1270              
1271              
1272             sub _foreign_key_fingerprint {
1273 0     0     my($self,$fk) = @_;
1274              
1275 0           my $class = $self->_get_class_meta_for_table_name(data_source => $fk->data_source,
1276             table_name => $fk->table_name);
1277              
1278 0           return $class->class_name . ':' . join(',',sort $fk->column_names) . ':' . join(',',sort $fk->r_column_names);
1279             }
1280              
1281              
1282              
1283              
1284             sub _sync_filesystem {
1285 0     0     my $self = shift;
1286 0           my %params = @_;
1287              
1288 0           my $changed_class_names = delete $params{changed_class_names};
1289 0 0         if (%params) {
1290 0           Carp::confess("Invalid params passed to _sync_filesystem: " . join(",", keys %params) . "\n");
1291             }
1292              
1293 0           my $obsolete_module_directory = $self->namespace_name->get_deleted_module_directory_name;
1294              
1295 0           my $namespace = $self->namespace_name;
1296 0           my $no_commit = UR::DBI->no_commit;
1297 0 0         $no_commit = 0 if $self->{'_override_no_commit_for_filesystem_items'};
1298              
1299 0           for my $class_name (@$changed_class_names) {
1300 0           my $status_message_this_update = '';
1301 0           my $class_obj;
1302             my $prev;
1303 0 0         if ($class_obj = UR::Object::Type->get(class_name => $class_name)) {
    0          
1304 0 0 0       if ($class_obj->{is}[0] =~ /::Type$/ and $class_obj->{is}[0]->isa('UR::Object::Type')) {
1305 0           next;
1306             }
1307 0 0         if ($class_obj->{db_committed}) {
1308 0           $status_message_this_update .= "U " . $class_obj->module_path;
1309             }
1310             else {
1311 0           $status_message_this_update .= "A " . $class_obj->module_path;
1312             }
1313 0 0         $class_obj->rewrite_module_header() unless ($no_commit);
1314             # FIXME A test of automatically making DBIx::Class modules
1315             #$class_obj->dbic_rewrite_module_header() unless ($no_commit);
1316              
1317             }
1318             elsif ($class_obj = UR::Object::Type::Ghost->get(class_name => $class_name)) {
1319 0 0         if ($class_obj->{is}[0] eq 'UR::Object::Type') {
1320 0           next;
1321             }
1322            
1323 0           $status_message_this_update = "D " . $class_obj->module_path;
1324            
1325 0 0         unless ($no_commit) {
1326 0 0         unless (-d $obsolete_module_directory) {
1327 0           mkdir $obsolete_module_directory;
1328 0 0         unless (-d $obsolete_module_directory) {
1329 0           $self->error_message("Unable to create $obsolete_module_directory for the deleted module for $class_name.");
1330 0           next;
1331             }
1332             }
1333              
1334 0           my $f = IO::File->new($class_obj->module_path);
1335 0           my $old_file_data = join('',$f->getlines);
1336 0           $f->close();
1337              
1338 0           my $old_module_path = $class_obj->module_path;
1339 0           my $new_module_path = $old_module_path;
1340 0           $new_module_path =~ s/\/$namespace\//\/$namespace\/\.deleted\//;
1341 0           $status_message_this_update .= " (moving $old_module_path to $new_module_path)";
1342 0           rename $old_module_path, $new_module_path;
1343              
1344 0           UR::Context::Transaction->log_change($class_obj, $class_obj->class_name, $class_obj->id, 'rewrite_module_header', Data::Dumper::Dumper({path => $new_module_path, data => $old_file_data}));
1345             }
1346             }
1347             else {
1348 0           Carp::confess("Failed to find regular or ghost class meta-object for class $class_name!?");
1349             }
1350            
1351 0 0         if ($no_commit) {
1352 0           $status_message_this_update .= ' (ignored - no-commit)';
1353             }
1354 0           $self->status_message($status_message_this_update);
1355              
1356             }
1357              
1358 0           return 1;
1359             }
1360              
1361             1;
1362