Group
Extension

DBIx-Class-Sims/lib/DBIx/Class/Sims/Item.pm

# This class exists to represent a row requested (and subsequently created) by
# the Sims. It will have a link back to a Sims::Source which will have the link
# back to the $schema object.

package DBIx::Class::Sims::Item;

use 5.010_001;

use strictures 2;

use DDP;

use List::PowerSet qw(powerset);
use Hash::Merge;
use Scalar::Util qw( blessed );

use DBIx::Class::Sims::Util qw(
  normalize_aoh reftype compare_values
);

sub new {
  my $class = shift;
  my $self = bless {@_}, $class;
  $self->initialize;
  return $self;
}

sub initialize {
  my $self = shift;

  $self->{original_spec} = MyCloner::clone($self->spec);

  # Lots of code assumes __META__ exists.
  # TODO: Should we check for _META__ or __META_ or __MTA__ etc?
  $self->{meta} = $self->spec->{__META__} // {};

  $self->{create} = {};
  $self->{parents} = {};

  $self->{still_to_use} = { map { $_ => 1 } keys %{$self->spec} };
  delete $self->{still_to_use}{__META__};

  $self->{skip_relationship} = {};

  return;
}

sub runner { $_[0]{runner} }
sub source { $_[0]{source} }
sub spec   { $_[0]{spec}   }
sub meta   { $_[0]{meta} }

sub source_name { shift->source->name }

sub allow_pk_set_value { shift->meta->{allow_pk_set_value} }
sub set_allow_pk_to {
  my $self = shift;
  my ($proto) = @_;

  $self->meta->{allow_pk_set_value} = blessed($proto)
    ? $proto->meta->{allow_pk_set_value}
    : $proto;

  return;
}

sub row {
  my $self = shift;
  $self->{row} = shift if @_;
  return $self->{row};
}

sub has_value {
  my $self = shift;
  my ($col) = @_;
  return exists $self->{create}{$col} || exists $self->spec->{$col};
}
sub value {
  my $self = shift;
  my ($col) = @_;

  return unless $self->has_value($col);
  return exists $self->{create}{$col}
    ? $self->{create}{$col}
    : $self->spec->{$col};
}
sub set_value {
  my $self = shift;
  my ($col, $val) = @_;
  $self->{create}{$col} = $val;
}

sub has_parent_values {
  my $self = shift;

  foreach my $r ( $self->source->parent_relationships ) {
    # FIXME: Is there a problem if there's a multi-col relationship with the
    # same name as another unrelated column?
    return 1 if $self->spec->{$r->name};

    # We need to have an entry for all the columns in the parent relationship.
    return 1 if ! grep {
      ! exists $self->spec->{$_}
    } $r->self_fk_cols;
  }

  return;
}

sub make_jsonable {
  my $self = shift;
  my ($item) = @_;

  # Deference all scalar references. This happens when we retrieve a row and
  # it has something like { update_time => \'current_timestamp' }
  $item->{$_} = reftype($item->{$_}) eq 'SCALAR'
    ? ${$item->{$_}} : $item->{$_}
    for keys %{$item};

  # Stringify everything, otherwise JSON::MaybeXS gets confused
  $item->{$_} = defined $item->{$_}
    ? '' . $item->{$_} : undef
    foreach keys %{$item};

  return $item;
}

################################################################################
#
# These are the helper methods
#
################################################################################

sub is_real_value {
  my $self = shift;
  my ($col) = @_;
  return unless $self->has_value($col);
  my $v = $self->value($col);
  return 1 unless defined $v;
  return if ref($v);
  return 1;
}

sub build_searcher_for_constraints {
  my $self = shift;
  my (@constraints) = @_;

  my $to_find = {};
  my $matched_all_columns = 1;
  foreach my $c ( map { @$_ } @constraints ) {
    unless ($self->is_real_value($c->name)) {
      $matched_all_columns = 0;
      last;
    }

    # Undefined values cannot be searched over because undefined values appear
    # different to the UK, but appear the same in a query. Therefore, this
    # searcher cannot work.
    $to_find->{$c->name} = $self->value($c->name) // return;
  }

  return $to_find if keys(%$to_find) && $matched_all_columns;
  return;
}

sub unique_id {
  my $self = shift;
  my ($row) = @_;

  my @cols = $row->result_source->columns;
  my %data = $row->get_columns;
  my @vals = @data{@cols};
  return ref($row) . join(',',@cols) . join(',',map {$_//''} @vals);
}

sub find_unique_match {
  my $self = shift;

  my @uniques = $self->source->uniques;

  my $rs = $self->source->resultset;

  if ( my $to_find = $self->build_searcher_for_constraints(@uniques) ) {
    my $row = $rs->search($to_find, { rows => 1 })->first;
    if ($row) {
      push @{$self->runner->{duplicates}{$self->source_name}}, {
        criteria => [$to_find],
        found    => { $row->get_columns },
      };
      $self->row($row);

      $self->{trace}{find} = $self->{runner}{ids}{find}++;
      $self->{trace}{row} = $self->make_jsonable( { $row->get_columns } );
      $self->{trace}{criteria} = [$to_find];
      $self->{trace}{unique} = 1;

      return;
    }
  }

  # Search through all the possible iterations of unique keys.
  #  * Don't populate $self->{create}
  #  * If found with all keys, great.
  #  * Otherwise, keep track of what we find for each combination (if at all)
  #    * If we have multiple finds, die.
  # TODO: Use List::Powerset->powerset_lazy() instead of powerset()
  my %rows_found;
  foreach my $bundle (@{powerset(@uniques)}) {
    # Skip the all (already handled) and the empty (unneeded).
    next if @$bundle == 0 || @$bundle == @uniques;

    my $finder = $self->build_searcher_for_constraints(@$bundle)
      or next;

    my $row = $rs->search($finder, { rows => 1 })->first;
    if ($row) {
      my $unique_id = $self->unique_id($row);
      $rows_found{$unique_id} //= {
        row => $row,
        finders => [],
      };
      push @{$rows_found{$unique_id}{finders}}, $finder;
    }
  }

  if (keys(%rows_found) > 1) {
    die "Rows found by multiple unique constraints";
  }

  if (keys(%rows_found) == 1) {
    my $x = (values %rows_found)[0];
    my ($finders, $row) = @{$x}{qw(finders row)};
    push @{$self->runner->{duplicates}{$self->source_name}}, {
      criteria => $finders,
      found    => { $row->get_columns },
    };
    $self->row($row);

    $self->{trace}{find} = $self->{runner}{ids}{find}++;
    $self->{trace}{row} = $self->make_jsonable( { $row->get_columns } );
    $self->{trace}{criteria} = $finders;
    $self->{trace}{unique} = 1;

    return;
  }

  return;
}

sub find_any_match {
  my $self = shift;

  my $rs = $self->source->resultset;

  # This is for handling the case where a FK is within a UK in the child. This
  # ensures we cannot select a parent whose PK is already in use in the child.
  # We don't check for this in find_unique_match() because the UK is all that
  # matters there.
  if ($self->meta->{restriction}) {
    my $c = $self->meta->{restriction};
    $rs = $rs->search( $c->{cond}, $c->{extra} );
  }

  my $cond = {};
  foreach my $colname ( map { $_->name } $self->source->columns ) {
    next unless $self->has_value($colname);
    $cond->{'me.' . $colname} = $self->value($colname);
  }

  my $row = $rs->search($cond, { rows => 1 })->single;
  if ($row) {
    $self->row($row);

    $self->{trace}{find} = $self->{runner}{ids}{find}++;
    $self->{trace}{row} = $self->make_jsonable( { $row->get_columns } );
    $self->{trace}{criteria} = [ $cond ];
    $self->{trace}{unique} = 0;
  }

  return $self->row;
}

sub attempt_to_find {
  my $self = shift;
  my ($opts) = @_;

  $opts //= {};
  $opts->{unique} //= 1;
  $opts->{any} //= 1;
  $opts->{no_parent_values} //= 0;

  # First, find_unique_match.
  # If row, handle die_on_unique_mismatch
  if ($opts->{unique}) {
    unless ($self->row) {
      $self->find_unique_match;
      if ($self->row && $self->runner->{die_on_unique_mismatch}) {
        my @failed;
        foreach my $c ( $self->source->columns ) {
          my $col_name = $c->name;

          next unless $self->has_value($col_name);

          my $row_value = $self->row->get_column($col_name);
          my $spec_value = $self->value($col_name);
          unless (compare_values($row_value, $spec_value)) {
            push @failed, "\t$col_name: row(@{[$row_value//'[undef]']}) spec(@{[$spec_value//'[undef]']})\n";
          }
        }
        if (@failed) {
          die "ERROR Retrieving unique @{[$self->source_name]} (".np($self->spec).") (".np($self->{create}).")\n" . join('', sort @failed) . $/ . np($self->runner->{duplicates}{$self->source_name}[-1]{criteria});
        }
      }
    }
  }

  # Else, find_any_match, but only if parent_values matches
  if ($opts->{any}) {
    if ( ! $self->row && ! $self->meta->{create} ) {
      if ( $opts->{no_parent_values} ) {
        $self->find_any_match if ! $self->has_parent_values;
      }
      else {
        $self->find_any_match;
      }
    }
  }

  return $self->row;
}

sub resolve_direct_values {
  my $self = shift;

  my @unknowns;
  while ( my ($k,$v) = each %{$self->spec} ) {
    # Ignore __META__
    next if $k eq '__META__';

    # If $k is a relationship, handle that.
    if ( my $r = $self->source->relationship($k) ) {
      if (blessed($v)) {
        die "$k is a multi-column FK, so cannot be directly set\n"
          if $r->is_multi_col;

        my $fkcol = $r->foreign_fk_col;
        $self->set_value($r->self_fk_col, $v->get_column($fkcol));
        $self->{skip_relationship}{$r->name} = 1;

        # Otherwise, the tracer will try and write out a blessed object.
        $self->{trace}{spec}{$r->self_fk_col} = $self->{create}{$r->self_fk_col};
      }
      elsif (ref($v) eq 'SCALAR') {
        die "$k is a multi-column FK, so cannot be directly set\n"
          if $r->is_multi_col;

        my $fkcol = $r->foreign_fk_col;
        $self->set_value($r->self_fk_col, $self->runner->convert_backreference(
          $self->runner->backref_name($self, $k),
          ${$v},
          $fkcol,
        ));
      }
    }
    # If $k is a column,
    elsif ( my $c = $self->source->column($k) ) {
      # If $k is in a relationship, find the appropriate one via the class.
      if ( $c->is_in_fk ) {
        # Find the appropriate FK.
        if ( my $kls = blessed $v ) {
          my @rels = grep {
            $kls eq $_->foreign_class
          } $c->fks;

          if ( @rels != 1 ) {
            die "ERROR: @{[$self->source_name]} Cannot figure out what relationship belongs to $k (@{[np $v]})!\n@{[join ',', sort map{$_->name}@rels]}";
          }
          my $r = $rels[0];

          die "$k is a multi-column FK, so cannot be directly set\n"
            if $r->is_multi_col;

          $self->set_value($k, $v->get_column($r->foreign_fk_col));
          $self->{skip_relationship}{$_->name} = 1 for $c->fks;

          # Otherwise, the tracer will try and write out a blessed object.
          $self->{trace}{spec}{$k} = $self->{create}{$k};
        }
        elsif (ref($v) eq 'SCALAR') {
          my ($kls_base) = ${$v} =~ /([^:]+)\[/;
          my @rels = grep {
            my ($k) = $_->foreign_class =~ /([^:]+)$/;
            ${$v} =~ /$k/
          } $c->fks;

          if ( @rels != 1 ) {
            die "ERROR: @{[$self->source_name]} Cannot figure out what relationship belongs to $k (@{[np $v]})!\n@{[join ',', sort map{$_->name}@rels]}";
          }
          my $r = $rels[0];

          die "$k is a multi-column FK, so cannot be directly set\n"
            if $r->is_multi_col;

          $self->set_value($k, $self->runner->convert_backreference(
            $self->runner->backref_name($self, $r->name),
            ${$v},
            $r->foreign_fk_col,
          ));
        }
      }
      # Else handle it via column
      else {
        if (reftype($v) eq 'SCALAR') {
          $self->set_value($k, $self->runner->convert_backreference(
            $self->runner->backref_name($self, $k),
            ${$v},
          ));
        }
      }
    }
    # Otherwise, DIE DIE DIE
    else {
      push @unknowns, $k;
    }
  }

  # Things were passed in, but don't exist in the table.
  if (!$self->runner->{ignore_unknown_columns} && @unknowns) {
    my $msg = "The following names are in the spec, but not the table @{[$self->source_name]}\n";
    $msg .= join ',', sort @unknowns;
    $msg .= "\n";
    die $msg;
  }

  return;
}

################################################################################
#
# These are the expected interface methods
#
################################################################################

sub create {
  my $self = shift;

  warn "Received @{[$self->source_name]}($self) (".np($self->spec).") (".np($self->{create}).")\n" if $ENV{SIMS_DEBUG};

  # If, in the current stack of in-flight items, we've attempted to make this
  # exact item, die because we've obviously entered an infinite loop.
  if ($self->runner->has_item($self)) {
    die "ERROR: @{[$self->source_name]} (".np($self->spec).") was seen more than once\n";
  }
  $self->runner->add_item($self);

  # Try to find a match with what was given if this is a parent request. But,
  # we cannot do that if we have parent values because we haven't resolved FKs
  # yet.
  warn "Trying to find a parent match @{[$self->source_name]}($self) (".np($self->spec).") (".np($self->{create}).")\n" if $ENV{SIMS_DEBUG};
  if ( $self->attempt_to_find({ unique => 0, no_parent_values => 1 }) ) {
    # If there are any children specified, figure them out here.
    $self->build_children;

    $self->runner->remove_item($self);

    return $self->row;
  }

  $self->runner->call_hook(preprocess =>
    $self->source, $self->spec,
  );
  warn "After preprocess @{[$self->source_name]}($self) (".np($self->spec).") (".np($self->{create}).")\n" if $ENV{SIMS_DEBUG};

  # This resolves all of the values that can be resolved immediately.
  #   * Back references
  #   * Objects
  $self->resolve_direct_values;
  warn "After RDV @{[$self->source_name]}($self) (".np($self->spec).") (".np($self->{create}).")\n" if $ENV{SIMS_DEBUG};

  $self->attempt_to_find({ any => 0 });

  unless ($self->row) {
    $self->populate_parents(nullable => 0);
    warn "After populate_parents @{[$self->source_name]}($self) (".np($self->spec).") (".np($self->{create}).")\n" if $ENV{SIMS_DEBUG};
  }

  $self->attempt_to_find({ any => 0 });

  unless ($self->row) {
    $self->populate_columns;
    warn "After populate_columns @{[$self->source_name]}($self) (".np($self->spec).") (".np($self->{create}).")\n" if $ENV{SIMS_DEBUG};
    $self->oracle_ensure_populated_pk;
  }

  $self->attempt_to_find;

  unless ($self->row) {
    $self->runner->call_hook(before_create =>
      $self->source, $self,
    );

    warn "Creating @{[$self->source_name]}($self) (".np($self->spec).") (".np($self->{create}).")\n" if $ENV{SIMS_DEBUG};
    my $row = eval {
      #use Carp; local $SIG{__DIE__} = \&Carp::confess;
      $self->source->resultset->create($self->{create});
    }; if ($@) {
      my $e = $@;
      warn "ERROR Creating @{[$self->source_name]} (".np($self->spec).") (".np($self->{create}).")\n";
      die $e;
    }
    $self->row($row);

    # This tracks everything that was created, not just what was requested.
    $self->runner->{created}{$self->source_name}++;

    $self->{trace}{made} = $self->{runner}{ids}{made}++;
    $self->{trace}{create_params} = $self->make_jsonable( $self->{create} );
    $self->{trace}{row} = $self->make_jsonable( { $row->get_columns } );

    # This occurs when a FK condition was specified, but the column is
    # nullable and we didn't find an existing parent row. We want to defer these
    # because self-referential values need to be set after creation.
    $self->populate_parents(nullable => 1);
  }
  $self->build_children;

  $self->runner->call_hook(postprocess =>
    $self->source, $self->row,
  );

  $self->runner->remove_item($self);

  if ($ENV{SIMS_DEBUG}) {
    my %x = $self->row->get_columns;
    warn "Finished @{[$self->source_name]}($self) (".np($self->spec).") (".np($self->{create}).") (" . np(%x) . ")\n";
  }

  return $self->row;
}

sub value_from_spec {
  my $self = shift;
  my ($c, $spec) = @_;

  # Try N times to find a value that's not in value_not

  my $n = 0;
  my $max = 25;
  my $v;
  do {
    $n++;
    die "Cannot find a value for @{[$c->source->name]}\.@{[$c->name]} after $max tries" if $n >= $max;

    if ( ref($spec->{func} // '') eq 'CODE' ) {
      $v = $spec->{func}->($c->info);
    }
    elsif ( exists $spec->{value} ) {
      if (ref($spec->{value} // '') eq 'ARRAY') {
        $v = $c->random_item( $spec->{value} );
      }
      else {
        $v = $spec->{value};
      }
    }
    elsif ( $spec->{type} ) {
      my $meth = $self->runner->parent->sim_type($spec->{type})
        // die "Type '$spec->{type}' is not loaded";
      $v = $meth->($c->info, $spec, $c);
    }
    else {
      $v = $c->generate_value(die_on_unknown => 0);
    }
  } while ( $spec->{value_not} && $spec->{value_not}->($v) );
  return $v;
}

sub populate_column {
  my $self = shift;
  my ($c) = @_;

  my $col_name = $c->name;
  return if exists $self->{create}->{$col_name};

  my $spec;
  if ( exists $self->spec->{$col_name} ) {
    if (
      $c->is_in_pk && $c->is_auto_increment &&
      !$self->allow_pk_set_value
    ) {
      warn sprintf(
        "Primary-key autoincrement columns should not be hardcoded in tests (%s.%s = %s)",
        $self->source_name, $col_name, $self->spec->{$col_name},
      );
    }

    # This is the original way of specifying an override with a HASHREFREF.
    # Reflection has realized it was an unnecessary distinction to a parent
    # specification. Either it's a relationship hashref or a simspec hashref.
    # We can never have both. It will be deprecated.
    if (
      reftype($self->spec->{$col_name}) eq 'REF' &&
      reftype(${$self->spec->{$col_name}}) eq 'HASH'
    ) {
      warn "DEPRECATED: Use a regular HASHREF for overriding simspec. HASHREFREF will be removed in a future release.";
      $spec = ${ $self->spec->{$col_name} };
    }
    elsif (
      reftype($self->spec->{$col_name}) eq 'HASH' &&
      # Assume a blessed hash is a DBIC object
      !blessed($self->spec->{$col_name}) &&
      # Do not assume we understand something to be inflated/deflated
      !$c->is_inflated
    ) {
      $spec = $self->spec->{$col_name};
    }
    elsif (reftype($self->spec->{$col_name}) eq 'SCALAR') {
      $self->set_value($col_name, $self->runner->convert_backreference(
        $self->runner->backref_name($self, $c->name),
        ${$self->spec->{$col_name}},
      ));
      return;
    }
    else {
      $self->set_value($col_name, $self->spec->{$col_name});
      return;
    }
  }

  # If the spec is a hashref containing "value_not" and nothing else, then merge
  # it with the spec from the column. Otherwise, it overrides the column.
  my $merge_spec = sub {
    my ($s) = @_;
    return unless $s;

    # At this point, we can presume that we have a HASHREF because the only way
    # we get a per-entry spec is if it's a HASHREF.

    # Handle the optional plural
    $s->{value_not} = delete $s->{values_not} if exists $s->{values_not};

    return unless keys %$s == 1;
    return unless exists $s->{value_not};
    return 1;
  };

  if ( $merge_spec->( $spec ) ) {
    my $merger = Hash::Merge->new('RIGHT_PRECEDENT');
    $spec = $merger->merge( $c->sim_spec // {}, $spec );
  }
  else {
    $spec //= $c->sim_spec;
  }

  if ($spec) {
    if ( exists $spec->{value_not} && reftype($spec->{value_not}) ne 'CODE' ) {
      if ( reftype($spec->{value_not}) ne 'ARRAY' ) {
        $spec->{value_not} = [ $spec->{value_not} ];
      }

      my $x = $spec->{value_not};
      $spec->{value_not} = sub {
        my ($v) = @_;
        return grep { $v eq $_ } @{$x};
      };
    }

    if (ref($spec // '') eq 'HASH') {
      if ( exists $spec->{null_chance} && $c->is_nullable ) {
        # Add check for not a number
        if ( $c->random_choice($spec->{null_chance}) ) {
          $self->set_value($col_name, undef);
          return;
        }
      }
      $self->set_value($col_name, $self->value_from_spec($c, $spec));
    }
  }
  elsif (
    !$c->is_nullable &&
    !$c->is_in_pk &&
    !$c->has_default_value
    # These clauses were in the original code. Do they still need to exist?
    # && !$c->is_in_uk
  ) {
    $self->set_value($col_name, $c->generate_value(die_on_unknown => 1));
  }

  return;
}

sub populate_columns {
  my $self = shift;

  foreach my $c ( $self->source->columns_not_in_parent_relationships ) {
    $self->populate_column($c);
  } continue {
    delete $self->{still_to_use}{$c->name};
  }

  return;
}

sub parent {
  my $self = shift;
  my ($relname) = @_;

  return $self->{parents}{$relname};
}

sub populate_parent {
  my $self = shift;
  my ($r, %opts) = @_;

  my $col = $r->self_fk_col;

  # Assumptions:
  #   * If someone sets $col, then they intend to use that.
  #   * If someone sets $col and $col is for multiple relationships, use it.
  #   * If someone sets $col *and* $r->name, then we're confused. Raise error.
  #     - What happens if there are two parents for $col?
  #     - What happens if one of them is nullable?

  # TODO: Write a test if both the rel and the FK col are specified
  my $proto = $self->has_value($col)
    ? $self->value($col)
    : $self->value($r->name);

  my $fkcol = $r->foreign_fk_col;

  my $spec;
  if ($proto) {
    # Convert backreferences first.
    if (ref($proto) eq 'SCALAR') {
      $proto = $self->runner->convert_backreference(
        $self->runner->backref_name($self, $r->name), $$proto, $fkcol,
      );
    }

    if (blessed($proto)) {
      if ($opts{nullable}) {
        $self->row->set_column($col => $proto->get_column($fkcol));
        $self->row->update;
      }
      else {
        $self->set_value($col, $proto->get_column($fkcol));

        # Otherwise, the tracer will try and write out a blessed object.
        warn "Converting $col to @{[$self->{create}{$col}]}\n";
        $self->{trace}{spec}{$col} = $self->{create}{$col};
      }
      return;
    }

    # Assume any hashref is a Sims specification
    if (ref($proto) eq 'HASH') {
      $spec = $proto;
    }
    # Assume any unblessed scalar is a column value.
    elsif (!ref($proto)) {
      $spec = { $fkcol => $proto };
    }
    else {
      die "Unsure what to do about @{[$r->full_name]}():" . np($proto);
    }
  }
  elsif ($self->source->column($col)->sim_spec) {
    my $c = $self->source->column($col);
    my $sp = $c->sim_spec;
    if ( exists $sp->{null_chance} && $c->is_nullable ) {
      # Add check for not a number
      if ( $c->random_choice($sp->{null_chance}) ) {
        return;
      }
    }
    $spec = {
      $fkcol => $self->value_from_spec($c, $sp),
    };
  }

  unless ( $spec ) {
    if ( $self->source->column($col)->is_nullable ) {
      return;
    }

    $spec = {};
  }

  my $fk_source = $r->target;
  # If the child's column is within a UK, add a check to the $rs that ensures
  # we cannot pick a parent that's already being used.
  my @constraints = $self->source->unique_constraints_containing($col);
  if (@constraints) {
    # First, find the inverse relationship. If it doesn't exist or if there
    # is more than one, then die.
    my @inverse = $self->source->find_inverse_relationships(
      $fk_source, $fkcol,
    );
    if (@inverse == 0) {
      die "Cannot find an inverse relationship for @{[$r->full_name]}\n";
    }
    elsif (@inverse > 1) {
      die "Too many inverse relationships for @{[$r->full_name]} (@{[$fk_source->name]} / $fkcol)\n" . np(@inverse);
    }

    # We cannot add this relationship to the $spec because that would result
    # in an infinite loop. So, add a restriction to the parent's __META__
    $spec->{__META__} //= {};
    $spec->{__META__}{restriction} = {
      cond  => { join('.', $inverse[0]{rel}, $inverse[0]{col}) => undef },
      extra => { join => $inverse[0]{rel} },
    };
  }

  warn "Parent (@{[$fk_source->name]}): " . np($spec) .$/ if $ENV{SIMS_DEBUG};
  push @{$self->{runner}{traces}}, {
    table  => $fk_source->name,
    spec   => MyCloner::clone($spec // {}),
    seen   => $self->{runner}{ids}{seen}++,
    parent => $self->{trace}{seen},
    via    => "populate_parents/@{[$r->name]}",
  };
  my $fk_item = DBIx::Class::Sims::Item->new(
    runner => $self->runner,
    source => $fk_source,
    spec   => MyCloner::clone($spec // {}),
    trace  => $self->{runner}{traces}[-1],
  );
  $fk_item->set_allow_pk_to($self);
  $fk_item->create;

  $self->{parents}{$r->name} = $fk_item;

  if ($opts{nullable}) {
    $self->row->set_column($col => $fk_item->row->get_column($fkcol));
    $self->row->update;
  }
  else {
    $self->set_value($col, $fk_item->row->get_column($fkcol));
  }
}

sub populate_parents {
  my $self = shift;
  my %opts = @_;

  my $has_value = sub {
    my $r = shift;
    return $self->has_value($r->self_fk_col) || $self->has_value($r->name);
  };

  RELATIONSHIP:
  foreach my $r (
    sort {
      $has_value->($b) <=> $has_value->($a)
    } $self->source->parent_relationships
  ) {
    my $col = $r->self_fk_col;

    if ( $opts{nullable} xor $self->source->column($col)->is_nullable ) {
      next RELATIONSHIP;
    }

    delete $self->{still_to_use}{$_} for ($r->name, $col);

    if (!$self->runner->{allow_relationship_column_names}) {
      if ($col ne $r->name && exists $self->spec->{$col}) {
        die "Cannot use column $col - use relationship @{[$r->name]}";
      }
    }

    if ($self->{skip_relationship}{$r->name}) {
      next RELATIONSHIP;
    }

    $self->populate_parent($r, %opts);
  }

  return;
}

sub build_children {
  my $self = shift;

  # 1. If we have something, then:
  #   a. If it's not an array, then make it an array
  # 2. If we don't have something,
  #   a. Make an array with an empty item
  #   XXX This is more than one item would be supported
  # In all cases, make sure to add { $fkcol => $row->get_column($col) } to the
  # child's $item
  foreach my $r ( $self->source->child_relationships ) {
    if ($r->constraints) {
      $self->runner->ensure_children($self, $r, $r->constraints);
    }

    next unless $self->has_value($r->name);

    my $normalized = normalize_aoh($self->value($r->name))
      or die "Don't know what to do with @{[$r->full_name]}\n\t".np($self->{original_spec});

    my @specified = grep { keys %$_ } @$normalized;

    # Only run everything through ensure_children() if all the children are
    # unspecified. We do need to figure out how to handle specified children,
    # but this should be "good nuff" for now.
    # In essence, this is saying x => [ {}, {} ] is equivalent to x => 2
    unless (@specified) {
      $self->runner->ensure_children(
        $self, $r, @$normalized + 0,
      );
      next;
    }

    my $fkcol = $r->foreign_fk_col;
    my $fk_source = $r->target;

    my @inverse = $self->source->find_inverse_relationships(
      $fk_source, $fkcol,
    );

    foreach my $child (@{$normalized}) {
      # FIXME $child is a hashref, not a ::Item. add_child() needs to be able to
      # handle ::Item's, which requires ::Item's to be Comparable. It also means
      # the ::Runner's spec has been converted to ::Item before iteration.
      ($child->{__META__} //= {})->{allow_pk_set_value} = 1;

      # If there isn't an inverse relationship from the child back to here, then
      # we need to specifically set the column. This could happen when you have
      # a "types" or "preferences" table that's used for many tables.
      if ( @inverse == 0 ) {
        $child->{$fkcol} = $self->row->get_column($r->self_fk_col);
      }
      # But, if there *is* any inverse relationship (even if there's several),
      # do not do $self->row->get_column($col). This causes an infinite loop
      # because the child then needs a parent ::Item that tries to create a
      # child, and so forth.
      else {
        $child->{$fkcol} = $self->row;
      }

      $self->runner->add_child({
        adder  => $self->source_name,
        source => $fk_source,
        fkcol  => $fkcol,
        child  => $child,
        trace  => {
          table  => $fk_source->name,
          spec   => MyCloner::clone($child),
          seen   => $self->{runner}{ids}{seen}++,
          parent => $self->{trace}{seen},
          via    => 'add_child',
        },
      });
    }
  } continue {
    delete $self->{still_to_use}{$r->name};
  }
}

sub oracle_ensure_populated_pk {
  my $self = shift;

  # Oracle does not allow the "INSERT INTO x DEFAULT VALUES" syntax that DBIC
  # wants to use. Therefore, find a PK column and set it to NULL. If there
  # isn't one, complain loudly.
  if ($self->runner->is_oracle && keys(%{$self->{create}}) == 0) {
    my @pk_columns = grep {
      $_->is_in_pk
    } $self->source->columns;

    die "Must specify something about some column or have a PK in Oracle"
      unless @pk_columns;

    # This will work even if there are multiple columns in the PK.
    $self->set_value($pk_columns[0]->name, undef);
  }
}

1;
__END__

=head1 NAME

DBIx::Class::Sims::Item - An item being created by the Sims

=head1 PURPOSE

This object encapsulates an item being managed by the Sims. It can either be
a specification you provided or a row that must be created due to constraints
in your database schema.

The initial spec is available as L</spec>. This is mutable, but will not be
used to create the object. Instead, a I<create-hash> is generated by iterating
over all the columns and relationships of the underlying ResultSource for this
item. That I<create-hash> is used to create the object.

You are likely to see an object of this class if you have a B<before_create>
hook.

=head1 METHODS

=head2 spec()

Returns the specification as received by this object. This is mutable.

=head2 has_value($colname)

This returns a boolean indicating if either the spec or the create-hash has a
value for this column. This value could be undefined. The create-hash is checked
first.

=head2 value($colname)

This returns the value for the column. It will return the value in the
create-hash first.

Note: If you receive undef, it could be either an undefined value or that there
is no set value. Check L</has_value> to disambiguate.

=head2 set_value($colname, $value)

This will set the value of the column in the create-hash. C<$value> can be
undefined.

This is the only way to set a value in the create-hash.

=head2 populate_column($column)

This takes a L<DBIx::Class::Sims::Column> object and does all the appropriate
work necessary to populate that column in the create-hash.

=head2 parent($relname)

This returns the L<DBIx::Class::Sims::Item/> object for the relationship.

=head2 source()

This returns the L<DBIx::Class::Sims::Source/> object for this item.

=head1 AUTHOR

Rob Kinyon <rob.kinyon@gmail.com>

=head1 LICENSE

Copyright (c) 2013 Rob Kinyon. All Rights Reserved.
This is free software, you may use it and distribute it under the same terms
as Perl itself.

=cut


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.