File Coverage

blib/lib/Set/Relation/V2.pm
Criterion Covered Total %
statement 605 1823 33.1
branch 149 580 25.6
condition 46 277 16.6
subroutine 70 171 40.9
pod 0 78 0.0
total 870 2929 29.7


line stmt bran cond sub pod time code
1 3     3   4029 use 5.008000;
  3         11  
2 3     3   16 use utf8;
  3         7  
  3         15  
3 3     3   73 use strict;
  3         6  
  3         72  
4 3     3   14 use warnings;
  3         5  
  3         243  
5              
6             ###########################################################################
7             ###########################################################################
8              
9             { package Set::Relation::V2; # class
10             our $VERSION = '0.013002';
11             $VERSION = eval $VERSION;
12              
13 3     3   23 use Carp 'confess';
  3         6  
  3         179  
14 3     3   17 use Scalar::Util 'blessed', 'refaddr';
  3         6  
  3         160  
15 3     3   19 use List::Util 'any', 'all', 'notall', 'uniqstr';
  3         14  
  3         64333  
16              
17             # with Set::Relation
18             sub does {
19 32     32 0 71 my ($self, $role_name) = @_;
20 32 50       92 confess q{This does() may only be invoked on an object.}
21             if not blessed $self;
22 32 50       74 confess q{does(): Bad $role_name arg; it must be a defined string.}
23             if !defined $role_name;
24 32 50       101 return 0
25             if !$self->isa( __PACKAGE__ );
26 32 50       268 return 1
27             if $role_name eq 'Set::Relation';
28 0         0 return 0;
29             }
30              
31             # has _heading
32             # isa HashRef
33             # One elem per attribute:
34             # hkey is Str attr name
35             # hval is undef+unused
36             # default {}
37             sub _heading {
38 391     391   883 my $self = shift;
39 391 100       747 $self->{_heading} = $_[0] if scalar @_;
40 391         838 return $self->{_heading};
41             }
42              
43             # has _body
44             # isa HashRef
45             # One elem per tuple:
46             # hkey is refaddr of hval
47             # hval is HashRef that is the coll of separate tuple attrs:
48             # hkey is Str attr name
49             # hval is the actual tuple attr value
50             # default {}
51             sub _body {
52 393     393   510 my $self = shift;
53 393 100       700 $self->{_body} = $_[0] if scalar @_;
54 393         765 return $self->{_body};
55             }
56              
57             # Note, this flag expressly only applies to the object it is a
58             # member of, and it doesn't say anything about this object's RVAs.
59             # Note, this flag is always true when an index exists over all attrs,
60             # because said index is always made in such a way to ensure this,
61             # but this flag may also be true when said index doesn't exist,
62             # depending on how the V2 object was created.
63             # has _is_known_dup_free
64             # isa Bool
65             # default 0
66             sub _is_known_dup_free {
67 309     309   417 my $self = shift;
68 309 100       596 $self->{_is_known_dup_free} = $_[0] if scalar @_;
69 309         647 return $self->{_is_known_dup_free};
70             }
71              
72             # has _which
73             # isa Maybe[Str]
74             # default undef
75             sub _which {
76 61     61   86 my $self = shift;
77 61 50       126 $self->{_which} = $_[0] if scalar @_;
78 61         79 return $self->{_which};
79             }
80              
81             # has _indexes
82             # isa HashRef
83             # One elem per index:
84             # hkey is index name;
85             # - is Str ident gen f head subset tha index ranges ovr
86             # hval is 2-elem ArrayRef that is the index itself + meta
87             # [0] is HashRef of atnms that index ranges over
88             # - structure same as '_heading'
89             # [1] is index itself;
90             # - HashRef; one elem per tup of projection of body
91             # on attrs that index ranges over
92             # hkey is Str ident gen fr distinct projection tupl
93             # hval is set of body tup having projection tuples
94             # in comn; is HashRef; one elem per body tuple
95             # - structure same as '_body', is slice o _body
96             # default {}
97             sub _indexes {
98 121     121   198 my $self = shift;
99 121 100       241 $self->{_indexes} = $_[0] if scalar @_;
100 121         178 return $self->{_indexes};
101             }
102              
103             # has _keys
104             # isa HashRef
105             # One elem per key:
106             # hkey is key name;
107             # - is Str ident gen f head subset tha index ranges ovr
108             # hval is HashRef of atnms that index ranges over
109             # - structure same as '_heading'
110             # default {}
111             sub _keys {
112 61     61   90 my $self = shift;
113 61 50       126 $self->{_keys} = $_[0] if scalar @_;
114 61         82 return $self->{_keys};
115             }
116              
117             ###########################################################################
118              
119             sub new {
120 61     61 0 65022 my ($class, @args) = @_;
121 61   33     274 $class = (blessed $class) || $class;
122              
123 61         146 my $params = $class->BUILDARGS( @args );
124              
125 61         109 my $self = bless {}, $class;
126              
127             # Set attribute default values.
128 61         153 $self->_heading( {} );
129 61         143 $self->_body( {} );
130 61         149 $self->_is_known_dup_free( 0 );
131 61         138 $self->_which( undef );
132 61         141 $self->_indexes( {} );
133 61         140 $self->_keys( {} );
134              
135 61         146 $self->BUILD( $params );
136              
137 61         176 return $self;
138             }
139              
140             ###########################################################################
141              
142             sub BUILDARGS {
143 61     61 0 140 my ($class, @args) = @_;
144 61 100       162 if (@args == 1) {
    50          
145 22 50       59 if (ref $args[0] ne 'HASH') {
146             # Constructor was called with a single positional argument.
147 22         79 return { members => $args[0] };
148             }
149             else {
150             # Constructor was called with (possibly zero) named arguments.
151 0         0 return { %{$args[0]} };
  0         0  
152             }
153             }
154             elsif ((scalar @args % 2) == 0) {
155             # Constructor was called with (possibly zero) named arguments.
156 39         95 return { @args };
157             }
158             else {
159             # Constructor was called with odd number positional arguments >= 3.
160 0         0 confess q{new(): Bad arguments list; it must either have an even}
161             . q{ number of elements or exactly 1 element.};
162             }
163             }
164              
165             ###########################################################################
166              
167             sub BUILD {
168 61     61 0 104 my ($self, $args) = @_;
169 61         88 my ($members, $keys) = @{$args}{'members', 'keys'};
  61         120  
170              
171             # Note, $members may be in all of the same formats as a HDMD_Perl_STD
172             # Relation value literal payload, but with a few extra trivial options.
173              
174 61 100 33     229 if (!defined $members) {
    50 33        
    50 0        
175             # Extra option 1.
176 39         62 $members = [];
177             }
178             elsif (!ref $members) {
179             # Extra option 2.
180 0         0 $members = [$members];
181             }
182             elsif (blessed $members and $members->can( 'does' )
183             and $members->does( 'Set::Relation' )
184             and !$members->isa( __PACKAGE__ )) {
185             # We got a $members that is a Set::Relation-doing class where that
186             # class isn't us; so clone it from a dump using public interface.
187 0         0 $members = $self->_new( $members->export_for_new() );
188             }
189 61 50 0     183 confess q{new(): Bad :$members arg; it must be either undefined}
      33        
190             . q{ or an array-ref or a non-ref or a Set::Relation object.}
191             if ref $members ne 'ARRAY'
192             and not (blessed $members and $members->isa( __PACKAGE__ ));
193              
194             # If we get here, $members is either a Set::Relation::V2 or an ary-ref.
195              
196 61 50       121 if (!defined $keys) {
    0          
197 61         101 $keys = [];
198             }
199             elsif (ref $keys ne 'ARRAY') {
200 0         0 $keys = [$keys];
201             }
202 61 50   0   202 if (any { ref $_ ne 'ARRAY' } @{$keys}) {
  0         0  
  61         231  
203 0         0 $keys = [$keys];
204             }
205 61         215 for my $key (@{$keys}) {
  61         131  
206             confess q{new(): Bad $keys arg; it is not correctly formatted.}
207             if ref $key ne 'ARRAY'
208 0 0 0 0   0 or notall { defined $_ and !ref $_ } @{$key};
  0 0       0  
  0         0  
209             }
210              
211             # If we get here, $keys is an Array of Array of Str.
212              
213             # Note, we expressly do not set _is_known_dup_free to true when
214             # we are not cloning an S::R object and we know we have an empty body
215             # because often our other methods will be directly adding tuples to
216             # result objects after simply asking new() to make an empty starter
217             # object, and so we are trying to avoid bugs due to those other methods
218             # forgetting to expressly set _is_known_dup_free to false.
219             # But if we know we have exactly 1 input tuple, we set the flag to
220             # true, since its unlikely the meths add tuples both on new and after.
221              
222 61 50 33     206 if (blessed $members and $members->isa( __PACKAGE__ )) {
    100          
223             # We'll just shallow-clone anoth Set::Relation::V2 obj's memb-set.
224 0         0 $self->_heading( $members->_heading() );
225 0         0 $self->_body( $members->_body() );
226 0         0 $self->_is_known_dup_free( $members->_is_known_dup_free() );
227 0         0 $self->_which( $members->_which() );
228 0         0 $self->_indexes( $members->_indexes() );
229 0         0 $self->_keys( $members->_keys() );
230             }
231 61         129 elsif (@{$members} == 0) {
232             # Input specifies zero attrs + zero tuples.
233             # No-op; attr defaults are fine.
234             }
235             else {
236             # Input specifies at least one attr or at least one tuple.
237 21         40 my $member0 = $members->[0];
238 21 50       89 if (!defined $member0) {
    50          
    100          
    50          
239 0         0 confess q{new(): Bad :$members arg; it is an array ref}
240             . q{ but it directly has an undefined element.};
241             }
242             elsif (!ref $member0) {
243             # Input spec at least 1 attr + zero tuples.
244             # Each $members elem is expected to be an atnm.
245             confess q{new(): Bad :$members arg; it has a non-ref elem,}
246             . q{ indicating it should just be a list of attr}
247             . q{ names, but at least one other elem is}
248             . q{ undefined or is a ref.}
249 0 0   0   0 if notall { defined $_ and !ref $_ } @{$members};
  0 0       0  
  0         0  
250             confess q{new(): Bad :$members arg; it specifies a list of}
251             . q{ attr names with at least one duplicated name.}
252 0 0       0 if (uniqstr @{$members}) != @{$members};
  0         0  
  0         0  
253 0         0 $self->_heading( {CORE::map { ($_ => undef) } @{$members}} );
  0         0  
  0         0  
254             }
255             elsif (ref $member0 eq 'HASH') {
256             # Input spec at least 1 tuple, in named attr format.
257             my $heading
258 12         21 = {CORE::map { ($_ => undef) } CORE::keys %{$member0}};
  43         88  
  12         37  
259 12         31 my $body = {};
260 12         19 for my $tuple (@{$members}) {
  12         22  
261 44 50 33     135 confess q{new(): Bad :$members arg; it has a hash-ref}
262             . q{ elem, indicating it should just be a list of}
263             . q{ tuples in named-attr format, but at least one}
264             . q{ other elem is not a hash-ref, or the 2 elems}
265             . q{ don't have exactly the same set of hkeys.}
266             if ref $tuple ne 'HASH'
267             or !$self->_is_identical_hkeys( $heading, $tuple );
268 44 50       131 confess q{new(): Bad :$members arg;}
269             . q{ at least one of its hash-ref elems is such}
270             . q{ that there exists circular refs between}
271             . q{ itself or its tuple-valued components.}
272             if $self->_tuple_arg_has_circular_refs( $tuple );
273 44         90 $tuple = $self->_import_nfmt_tuple( $tuple );
274 44         142 $body->{refaddr $tuple} = $tuple;
275             }
276 12         55 $self->_heading( $heading );
277 12         31 $self->_body( $body );
278 12 100       18 if (@{$members} == 1) {
  12         32  
279 3         6 $self->_is_known_dup_free( 1 );
280             }
281             }
282             elsif (ref $member0 eq 'ARRAY') {
283             # Input is in ordered attr format.
284 9         15 my $member1 = $members->[1];
285             confess q{new(): Bad :$members arg; it has an array-ref first}
286             . q{ elem, indicating it should just be a list of}
287             . q{ tuples in ordered-attr format, but either}
288             . q{ :$members doesn't have exactly 2 elements or its}
289             . q{ second element isn't also an array-ref.}
290 9 50 33     15 if @{$members} != 2 or ref $member1 ne 'ARRAY';
  9         45  
291             # Each $member0 elem is expected to be an atnm.
292             confess q{new(): Bad :$members array-ref arg array-ref}
293             . q{ first elem; it should be just be a list of}
294             . q{ attr names, but at least one name}
295             . q{ is undefined or is a ref.}
296 9 50   33   35 if notall { defined $_ and !ref $_ } @{$member0};
  33 50       98  
  9         25  
297             confess q{new(): Bad :$members arg; it specifies a list of}
298             . q{ attr names with at least one duplicated name.}
299 9 50       24 if (uniqstr @{$member0}) != @{$member0};
  9         99  
  9         28  
300 9         22 my $heading = {CORE::map { ($_ => undef) } @{$member0}};
  33         70  
  9         20  
301 9         23 my $body = {};
302 9         14 for my $tuple (@{$member1}) {
  9         20  
303             confess q{new(): Bad :$members array-ref arg array-ref}
304             . q{ second elem; at least one elem isn't an}
305             . q{ array-ref, or that doesn't have the same}
306             . q{ count of elems as the :$members first elem.}
307 35 50 33     82 if ref $tuple ne 'ARRAY' or @{$tuple} != @{$member0};
  35         49  
  35         83  
308             # Each $tuple elem is expected to be an atvl.
309             confess q{new(): Bad :$members arg;}
310             . q{ at least one of its array-ref elems}
311             . q{ is such that there exists circular refs}
312             . q{ between its tuple-valued components.}
313 125 50   125   227 if any { ref $_ eq 'HASH'
314             and $self->_tuple_arg_has_circular_refs( $_ )
315 35 50       102 } (@{$tuple});
  35         72  
316 35         109 $tuple = $self->_import_ofmt_tuple( $member0, $tuple );
317 35         115 $body->{refaddr $tuple} = $tuple;
318             }
319 9         29 $self->_heading( $heading );
320 9         36 $self->_body( $body );
321 9 100       12 if (@{$member1} == 1) {
  9         26  
322 1         4 $self->_is_known_dup_free( 1 );
323             }
324             }
325             else {
326 0         0 confess q{new(): Bad :$members arg; it is an array-ref but it}
327             . q{ has an elem that is neither a defined scalar nor}
328             . q{ an array-ref nor a hash-ref.};
329             }
330             }
331              
332 61         110 my $self_h = $self->_heading();
333              
334 61         92 for my $key (@{$keys}) {
  61         103  
335             confess q{new(): At least one of the relation keys defined by the}
336             . q{ $keys arg isn't a subset of the heading of the}
337             . q{ relation defined by the $members arg.}
338 0 0   0   0 if notall { exists $self_h->{$_} } @{$key};
  0         0  
  0         0  
339 0 0       0 confess q{new(): The relation defined by the $members arg violates}
340             . q{ at least one of the candidate unique key constraints}
341 0         0 . qq{ defined by the $members arg: [@{$key}].}
342             if !$self->_has_key( $key );
343             }
344              
345 61         113 return;
346             }
347              
348             ###########################################################################
349              
350             sub _new {
351 39     39   67 my ($self, @args) = @_;
352 39         159 return (blessed $self)->new( @args );
353             }
354              
355             ###########################################################################
356              
357             sub export_for_new {
358 0     0 0 0 my ($self, $want_ord_attrs, $allow_dup_tuples) = @_;
359             return {
360 0         0 'members' => $self->_members( 'export_for_new',
361             '$want_ord_attrs', $want_ord_attrs, $allow_dup_tuples ),
362             'keys' => $self->keys(),
363             };
364             }
365              
366             ###########################################################################
367              
368             sub which {
369 0     0 0 0 my ($self) = @_;
370 0         0 my $ident_str = $self->_which();
371 0 0       0 if (!defined $ident_str) {
372 0         0 my $index = $self->_dup_free_want_index_over_all_attrs();
373 0         0 my $hs = $self->_heading_ident_str( $self->_heading() );
374 0         0 my $bs = CORE::join qq{,\n}, sort (CORE::keys %{$index});
  0         0  
375 0         0 my $vstr = "H=$hs;\nB={$bs}";
376 0         0 $ident_str = 'Relation:' . (length $vstr) . ':{' . $vstr . '}';
377 0         0 $self->_which( $ident_str );
378             }
379 0         0 return $ident_str;
380             }
381              
382             ###########################################################################
383              
384             sub members {
385 33     33 0 3653 my ($self, $want_ord_attrs, $allow_dup_tuples) = @_;
386 33         87 return $self->_members( 'members',
387             '$want_ord_attrs', $want_ord_attrs, $allow_dup_tuples );
388             }
389              
390             sub _members {
391 33     33   71 my ($self, $rtn_nm, $arg_nm, $want_ord_attrs, $allow_dup_tuples) = @_;
392 33 100 66     97 if (!$allow_dup_tuples and !$self->_is_known_dup_free()) {
393 5         15 $self->_dup_free_want_index_over_all_attrs();
394             }
395 33 100       65 if ($want_ord_attrs) {
    50          
396 2         7 my $ord_attr_names = $self->_normalize_true_want_ord_attrs_arg(
397             $rtn_nm, $arg_nm, $want_ord_attrs );
398             return [$ord_attr_names, [CORE::map {
399 5         10 $self->_export_ofmt_tuple( $ord_attr_names, $_ )
400 2         4 } values %{$self->_body()}]];
  2         5  
401             }
402 31         57 elsif ((CORE::keys %{$self->_body()}) == 0) {
403             # We have zero tuples, just export attr names.
404 0         0 return [sort (CORE::keys %{$self->_heading()})];
  0         0  
405             }
406             else {
407             # We have at least one tuple, export in named-attr format.
408 107         191 return [CORE::map { $self->_export_nfmt_tuple( $_ ) }
409 31         46 values %{$self->_body()}];
  31         51  
410             }
411             }
412              
413             ###########################################################################
414              
415             sub heading {
416 1     1 0 4 my ($self) = @_;
417 1         3 return [sort (CORE::keys %{$self->_heading()})];
  1         3  
418             }
419              
420             ###########################################################################
421              
422             sub body {
423 0     0 0 0 my ($self, $want_ord_attrs, $allow_dup_tuples) = @_;
424 0 0 0     0 if (!$allow_dup_tuples and !$self->_is_known_dup_free()) {
425 0         0 $self->_dup_free_want_index_over_all_attrs();
426             }
427 0 0       0 if ($want_ord_attrs) {
428 0         0 my $ord_attr_names = $self->_normalize_true_want_ord_attrs_arg(
429             'body', '$want_ord_attrs', $want_ord_attrs );
430 0         0 return [CORE::map { $self->_export_ofmt_tuple(
431 0         0 $ord_attr_names, $_ ) } values %{$self->_body()}];
  0         0  
432             }
433             else {
434 0         0 return [CORE::map { $self->_export_nfmt_tuple( $_ ) }
435 0         0 values %{$self->_body()}];
  0         0  
436             }
437             }
438              
439             ###########################################################################
440              
441             sub _normalize_true_want_ord_attrs_arg {
442 2     2   5 my ($self, $rtn_nm, $arg_nm, $want_ord_attrs) = @_;
443              
444 2         5 my $heading = $self->_heading();
445              
446 2         3 my $attr_names = [CORE::keys %{$heading}];
  2         8  
447             confess qq{$rtn_nm(): Bad $arg_nm arg;}
448             . q{ it must be either undefined|false or the scalar value '1'}
449             . q{ or an array-ref of attr names whose degree and}
450             . q{ elements match the heading of the invocant.}
451             if not (!ref $want_ord_attrs and $want_ord_attrs eq '1'
452             or ref $want_ord_attrs eq 'ARRAY'
453 0         0 and @{$want_ord_attrs} == @{$attr_names}
  0         0  
454 2 0 33 0   23 and all { exists $heading->{$_} } @{$want_ord_attrs});
  0   0     0  
  0   0     0  
      33        
455              
456             return
457 2 50       23 $want_ord_attrs eq '1' ? [sort @{$attr_names}] : $want_ord_attrs;
  2         11  
458             }
459              
460             ###########################################################################
461              
462             sub slice {
463 0     0 0 0 my ($self, $attr_names, $want_ord_attrs, $allow_dup_tuples) = @_;
464              
465 0         0 (my $proj_h, $attr_names)
466             = $self->_atnms_hr_from_assert_valid_atnms_arg(
467             'slice', '$attr_names', $attr_names );
468 0         0 my (undef, undef, $proj_only)
469             = $self->_ptn_conj_and_disj( $self->_heading(), $proj_h );
470             confess q{slice(): Bad $attr_names arg; that attr list}
471             . q{ isn't a subset of the invocant's heading.}
472 0 0       0 if @{$proj_only} > 0;
  0         0  
473              
474 0 0 0     0 if (!$allow_dup_tuples and !$self->_is_known_dup_free()) {
475 0         0 $self->_dup_free_want_index_over_all_attrs();
476             }
477              
478 0 0       0 if ($want_ord_attrs) {
479 0 0       0 confess q{slice(): Bad $want_ord_attrs arg; it must be}
480             . q{ either undefined|false or the scalar value '1'.}
481             if $want_ord_attrs ne '1';
482 0         0 return [CORE::map { $self->_export_ofmt_tuple(
483 0         0 $attr_names, $_ ) } values %{$self->_body()}];
  0         0  
484             }
485             else {
486             return [CORE::map {
487 0         0 my $t = $_;
488 0         0 $t = {CORE::map { ($_ => $t->{$_}) } @{$attr_names}};
  0         0  
  0         0  
489 0         0 $self->_export_nfmt_tuple( $t );
490 0         0 } values %{$self->_body()}];
  0         0  
491             }
492             }
493              
494             ###########################################################################
495              
496             sub attr {
497 4     4 0 9 my ($self, $name, $allow_dup_tuples) = @_;
498              
499 4         12 $self->_assert_valid_atnm_arg( 'attr', '$name', $name );
500             confess q{attr(): Bad $name arg; that attr name}
501             . q{ doesn't match an attr of the invocant's heading.}
502 4 50       10 if !exists $self->_heading()->{$name};
503              
504 4 50 33     14 if (!$allow_dup_tuples and !$self->_is_known_dup_free()) {
505 0         0 $self->_dup_free_want_index_over_all_attrs();
506             }
507              
508             return [CORE::map {
509 6         11 my $atvl = $_->{$name};
510 6 50       15 if (ref $atvl eq 'HASH') {
511 0         0 $atvl = $self->_export_nfmt_tuple( $atvl );
512             }
513 6         22 $atvl;
514 4         7 } values %{$self->_body()}];
  4         7  
515             }
516              
517             ###########################################################################
518              
519             sub keys {
520 0     0 0 0 my ($self) = @_;
521 0         0 return [CORE::map { [sort @{$_}] } values %{$self->_keys()}];
  0         0  
  0         0  
  0         0  
522             }
523              
524             ###########################################################################
525              
526             sub _normalize_same_heading_tuples_arg {
527 13     13   37 my ($r, $rtn_nm, $arg_nm, $t) = @_;
528              
529 13         23 my $r_h = $r->_heading();
530              
531 13 50       34 if (ref $t eq 'HASH') {
532 13         23 $t = [$t];
533             }
534 13 50       30 confess qq{$rtn_nm(): Bad $arg_nm arg; it must be}
535             . q{ an array-ref or a hash-ref.}
536             if ref $t ne 'ARRAY';
537 13         18 for my $tuple (@{$t}) {
  13         24  
538 13 50 33     42 confess qq{$rtn_nm(): Bad $arg_nm arg elem;}
539             . q{ it isn't a hash-ref, or it doesn't have exactly the}
540             . q{ same set of attr names as the invocant.}
541             if ref $tuple ne 'HASH'
542             or !$r->_is_identical_hkeys( $r_h, $tuple );
543 13 50       42 confess qq{$rtn_nm(): Bad $arg_nm arg elem;}
544             . q{ it is a hash-ref, and there exist circular refs}
545             . q{ between itself or its tuple-valued components.}
546             if $r->_tuple_arg_has_circular_refs( $tuple );
547             }
548              
549 13         25 return $t;
550             }
551              
552             ###########################################################################
553              
554             sub _tuple_arg_has_circular_refs {
555             # This routine just checks that no Hash which would be treated as
556             # being of a value type contains itself as a component, where the
557             # component and any intermediate components are treated as value types.
558             # It *is* fine for a Hash to contain the same other Hash more than once
559             # such that the other is a sibling/cousin/etc to itself.
560 57     57   112 my ($self, $tuple, $ancs_of_tup_atvls) = @_;
561 57 50       109 $ancs_of_tup_atvls = $ancs_of_tup_atvls ? {%{$ancs_of_tup_atvls}} : {};
  0         0  
562 57         157 $ancs_of_tup_atvls->{refaddr $tuple} = undef;
563 57         80 for my $atvl (values %{$tuple}) {
  57         117  
564 231 50       400 if (ref $atvl eq 'HASH') {
565             return 1
566 0 0       0 if exists $ancs_of_tup_atvls->{refaddr $atvl};
567 0 0       0 return 1
568             if $self->_tuple_arg_has_circular_refs(
569             $atvl, $ancs_of_tup_atvls );
570             }
571             }
572 57         148 return 0;
573             }
574              
575             ###########################################################################
576              
577             sub _is_identical_hkeys {
578 93     93   168 my ($self, $h1, $h2) = @_;
579 93         136 my $h1_hkeys = [CORE::keys %{$h1}];
  93         262  
580 93         155 my $h2_hkeys = [CORE::keys %{$h2}];
  93         222  
581             return (@{$h1_hkeys} == @{$h2_hkeys}
582 93   33 392   194 and all { exists $h1->{$_} } @{$h2_hkeys});
  392         911  
583             }
584              
585             ###########################################################################
586              
587             sub _heading_ident_str {
588 60     60   102 my ($self, $heading) = @_;
589             my $vstr = CORE::join q{,}, CORE::map {
590 204         521 'Atnm:' . (length $_) . ':<' . $_ . '>'
591 60         77 } sort (CORE::keys %{$heading});
  60         223  
592 60         186 return 'Heading:' . (length $vstr) . ':{' . $vstr . '}';
593             }
594              
595             sub _ident_str {
596             # Note, we assume that any hash-ref arg we get is specifically in
597             # internal tuple format, meaning each hval is a 2-elem array etc,
598             # and that this is recursive for hash-ref hvals of said.
599 701     701   1035 my ($self, $value) = @_;
600 701         829 my $ident_str;
601 701 50       1386 if (!defined $value) {
    100          
    50          
602             # The Perl undef is equal to itself, distinct from all def values.
603 0         0 $ident_str = 'Undef';
604             }
605             elsif (!ref $value) {
606             # Treat all defined non-ref values as their string representation.
607 543         913 $ident_str = 'Scalar:' . (length $value) . ':<' . $value . '>';
608             }
609             elsif (!blessed $value) {
610             # By default, every non-object reference is distinct, and its
611             # identity is its memory address; the exception is if the reference
612             # is a hash-ref, in which case it is treated as an internal tuple.
613 158 50       268 if (ref $value eq 'HASH') {
614             my $vstr = CORE::join q{,}, CORE::map {
615 543         1012 my $atnm = 'Atnm:' . (length $_) . ':<' . $_ . '>';
616 543         996 my $atvl = $self->_ident_str( $value->{$_} );
617 543         1254 "N=$atnm;V=$atvl";
618 158         222 } sort (CORE::keys %{$value});
  158         473  
619 158         391 $ident_str = 'Tuple:' . (length $vstr) . ':{' . $vstr . '}';
620             }
621             else {
622 0         0 my $vstr = "$value";
623 0         0 $ident_str = 'Ref:' . (length $vstr) . ':<' . $vstr . '>';
624             }
625             }
626             else {
627             # By default, every object instance is distinct, and its identity
628             # is its memory address; the exception is if the object is a
629             # Set::Relation::V2 or if it overloads stringification.
630 0 0       0 if ($value->isa( __PACKAGE__ )) {
631 0         0 $ident_str = $value->which(); # 'Relation:...'
632             }
633             else {
634 0         0 my $vstr = "$value";
635 0         0 $ident_str = 'Object[' . (blessed $value) . ']:'
636             . (length $vstr) . ':<' . $vstr . '>';
637             }
638             }
639 701         1092 return $ident_str;
640             }
641              
642             ###########################################################################
643              
644             sub _import_nfmt_tuple {
645 57     57   92 my ($self, $tuple) = @_;
646             return {CORE::map {
647 231         294 my $atnm = $_;
648 231         306 my $atvl = $tuple->{$_};
649 231 50 33     618 if (ref $atvl eq 'HASH') {
    50 33        
      0        
650 0         0 $atvl = $self->_import_nfmt_tuple( $atvl );
651             }
652             elsif (blessed $atvl and $atvl->can( 'does' )
653             and $atvl->does( 'Set::Relation' )
654             and !$atvl->isa( __PACKAGE__ )) {
655 0         0 $atvl = $self->_new( $atvl );
656             }
657 231         572 ($atnm => $atvl);
658 57         69 } CORE::keys %{$tuple}};
  57         112  
659             }
660              
661             sub _export_nfmt_tuple {
662 181     181   291 my ($self, $tuple) = @_;
663             return {CORE::map {
664 724         881 my $atnm = $_;
665 724         931 my $atvl = $tuple->{$_};
666 724 50       1167 if (ref $atvl eq 'HASH') {
667 0         0 $atvl = $self->_export_nfmt_tuple( $atvl );
668             }
669 724         1597 ($atnm => $atvl);
670 181         216 } CORE::keys %{$tuple}};
  181         350  
671             }
672              
673             sub _import_ofmt_tuple {
674 35     35   59 my ($self, $atnms, $atvls) = @_;
675             return {CORE::map {
676 125         167 my $atnm = $atnms->[$_];
677 125         160 my $atvl = $atvls->[$_];
678 125 50 33     336 if (ref $atvl eq 'HASH') {
    50 33        
      0        
679 0         0 $atvl = $self->_import_nfmt_tuple( $atvl );
680             }
681             elsif (blessed $atvl and $atvl->can( 'does' )
682             and $atvl->does( 'Set::Relation' )
683             and !$atvl->isa( __PACKAGE__ )) {
684 0         0 $atvl = $self->_new( $atvl );
685             }
686 125         269 ($atnm => $atvl);
687 35         57 } 0..$#{$atnms}};
  35         58  
688             }
689              
690             sub _export_ofmt_tuple {
691 5     5   12 my ($self, $atnms, $tuple) = @_;
692             return [CORE::map {
693 12         19 my $atvl = $tuple->{$_};
694 12 50       19 if (ref $atvl eq 'HASH') {
695 0         0 $atvl = $self->_export_nfmt_tuple( $atvl );
696             }
697 12         27 $atvl;
698 5         6 } @{$atnms}];
  5         8  
699             }
700              
701             ###########################################################################
702              
703             sub degree {
704 55     55 0 87 my ($topic) = @_;
705 55         73 return scalar CORE::keys %{$topic->_heading()};
  55         89  
706             }
707              
708             sub is_nullary {
709 4     4 0 18 my ($topic) = @_;
710 4         12 return $topic->degree() == 0;
711             }
712              
713             sub has_attrs {
714 0     0 0 0 my ($topic, $attr_names) = @_;
715 0         0 (my $proj_h, $attr_names)
716             = $topic->_atnms_hr_from_assert_valid_atnms_arg(
717             'has_attrs', '$attr_names', $attr_names );
718 0         0 my (undef, undef, $proj_only)
719             = $topic->_ptn_conj_and_disj( $topic->_heading(), $proj_h );
720 0         0 return @{$proj_only} == 0;
  0         0  
721             }
722              
723             sub attr_names {
724 0     0 0 0 my ($topic) = @_;
725 0         0 return [sort (CORE::keys %{$topic->_heading()})];
  0         0  
726             }
727              
728             ###########################################################################
729              
730             sub cardinality {
731 119     119 0 239 my ($topic, $allow_dup_tuples) = @_;
732 119         142 my $on_entry_body_cardinality = scalar CORE::keys %{$topic->_body()};
  119         188  
733 119 100       207 if ($topic->_is_known_dup_free()) {
    100          
    100          
734 87         283 return $on_entry_body_cardinality;
735             }
736             elsif ($on_entry_body_cardinality == 0) {
737 1         3 $topic->_is_known_dup_free( 1 );
738 1         4 return 0;
739             }
740             elsif ($allow_dup_tuples) {
741 15         45 return $on_entry_body_cardinality;
742             }
743             else {
744 16         62 $topic->_dup_free_want_index_over_all_attrs();
745 16         29 return scalar CORE::keys %{$topic->_body()};
  16         30  
746             }
747             }
748              
749             sub count {
750 0     0 0 0 my ($self, @args) = @_;
751 0         0 return $self->cardinality( @args );
752             }
753              
754             sub is_empty {
755 58     58 0 90 my ($topic) = @_;
756 58         105 return $topic->cardinality( 1 ) == 0;
757             }
758              
759             sub has_member {
760 0     0 0 0 my ($r, $t) = @_;
761 0         0 $t = $r->_normalize_same_heading_tuples_arg( 'has_member', '$t', $t );
762 0         0 my $r_i = $r->_dup_free_want_index_over_all_attrs();
763             return all {
764 0     0   0 exists $r_i->{$r->_ident_str( $r->_import_nfmt_tuple( $_ ) )}
765 0         0 } @{$t};
  0         0  
766             }
767              
768             ###########################################################################
769              
770             sub has_key {
771 0     0 0 0 my ($topic, $attr_names) = @_;
772 0         0 (undef, $attr_names) = $topic->_atnms_hr_from_assert_valid_atnms_arg(
773             'has_key', '$attr_names', $attr_names );
774 0         0 my $topic_h = $topic->_heading();
775             confess q{has_key(): Bad $attr_names arg; that attr list}
776             . q{ isn't a subset of the invocant's heading.}
777 0 0   0   0 if notall { exists $topic_h->{$_} } @{$attr_names};
  0         0  
  0         0  
778 0         0 return $topic->_has_key( $attr_names );
779             }
780              
781             sub _has_key {
782 0     0   0 my ($topic, $attr_names) = @_;
783              
784 0         0 my $subheading = {CORE::map { ($_ => undef) } @{$attr_names}};
  0         0  
  0         0  
785 0         0 my $subheading_ident_str = $topic->_heading_ident_str( $subheading );
786 0         0 my $keys = $topic->_keys();
787              
788             return 1
789 0 0       0 if exists $keys->{$subheading_ident_str};
790              
791 0         0 my $index = $topic->_want_index( $attr_names );
792              
793             return 0
794 0 0   0   0 if notall { (CORE::keys %{$_}) == 1 } values %{$index};
  0         0  
  0         0  
  0         0  
795              
796 0         0 $keys->{$subheading_ident_str} = $subheading;
797 0         0 return 1;
798             }
799              
800             ###########################################################################
801              
802             sub empty {
803 32     32 0 51 my ($topic) = @_;
804 32 100       55 if ($topic->is_empty()) {
805 1         3 return $topic;
806             }
807 31         73 my $result = $topic->_new();
808 31         59 $result->_heading( $topic->_heading() );
809 31         50 return $result;
810             }
811              
812             sub insertion {
813 13     13 0 25787 my ($r, $t) = @_;
814              
815 13         29 $t = $r->_normalize_same_heading_tuples_arg( 'insertion', '$t', $t );
816              
817 13 50       17 if (@{$t} == 0) {
  13         26  
818 0         0 return $r;
819             }
820              
821             my $tuples = {CORE::map {
822 13         27 my $tuple = $r->_import_nfmt_tuple( $_ );
823 13         56 (refaddr $tuple, $tuple);
824 13         19 } @{$t}};
  13         22  
825              
826 13         34 my $result = $r->empty();
827              
828 13         20 $result->_body( {%{$r->_body()}, %{$tuples}} );
  13         22  
  13         70  
829              
830 13         77 return $result;
831             }
832              
833             sub deletion {
834 0     0 0 0 my ($r, $t) = @_;
835              
836 0         0 $t = $r->_normalize_same_heading_tuples_arg( 'deletion', '$t', $t );
837              
838 0 0       0 if (@{$t} == 0) {
  0         0  
839 0         0 return $r;
840             }
841              
842             my $tuples = {CORE::map {
843 0         0 my $tuple = $r->_import_nfmt_tuple( $_ );
844 0         0 ($r->_ident_str( $tuple ), $tuple);
845 0         0 } @{$t}};
  0         0  
846              
847 0         0 my $result = $r->empty();
848              
849 0         0 my $r_i = $r->_dup_free_want_index_over_all_attrs();
850 0         0 my $result_b = $result->_body();
851              
852 0         0 for my $tuple_ident_str (CORE::keys %{$r_i}) {
  0         0  
853 0 0       0 if (!exists $tuples->{$tuple_ident_str}) {
854 0         0 my ($tuple_refaddr, $tuple) = %{$r_i->{$tuple_ident_str}};
  0         0  
855 0         0 $result_b->{$tuple_refaddr} = $tuple;
856             }
857             }
858              
859 0         0 $result->_is_known_dup_free( 1 );
860              
861 0         0 return $result;
862             }
863              
864             ###########################################################################
865              
866             sub rename {
867 1     1 0 7895 my ($topic, $map) = @_;
868              
869 1 50       6 confess q{rename(): Bad $map arg; it must be a hash-ref.}
870             if ref $map ne 'HASH';
871             confess q{rename(): Bad $map arg;}
872             . q{ its hash elem values should be just be a list of attr}
873             . q{ names, but at least one name is undefined or isa ref.}
874 1 50   4   47 if notall { defined $_ and !ref $_ } values %{$map};
  4 50       16  
  1         8  
875             confess q{rename(): Bad $map arg;}
876             . q{ its hash elem values specify a list of}
877             . q{ attr names with at least one duplicated name.}
878 1 50       4 if (uniqstr values %{$map}) != (CORE::keys %{$map});
  1         6  
  1         4  
879              
880             my ($topic_attrs_to_ren, $topic_attrs_no_ren, $map_hvals_not_in_topic)
881             = $topic->_ptn_conj_and_disj(
882 1         5 $topic->_heading(), {reverse %{$map}} );
  1         5  
883             confess q{rename(): Bad $map arg; that list of attrs to be renamed,}
884             . q{ the hash values, isn't a subset of th invocant's heading.}
885 1 50       4 if @{$map_hvals_not_in_topic} > 0;
  1         5  
886              
887             my ($map_hkeys_same_as_topic_no_ren, undef, undef)
888             = $topic->_ptn_conj_and_disj(
889 1         2 {CORE::map { ($_ => undef) } @{$topic_attrs_no_ren}}, $map );
  0         0  
  1         4  
890             confess q{rename(): Bad $map arg; at least one key of that hash,}
891             . q{ a new name for an attr of the invocant to rename,}
892             . q{ duplicates an attr of the invocant not being renamed.}
893 1 50       3 if @{$map_hkeys_same_as_topic_no_ren} > 0;
  1         4  
894              
895 1         4 return $topic->_rename( $map );
896             }
897              
898             sub _rename {
899 1     1   3 my ($topic, $map) = @_;
900              
901             # Remove any explicit no-ops of an attr being renamed to the same name.
902 4         9 $map = {CORE::map { ($_ => $map->{$_}) }
903 1         2 grep { $map->{$_} ne $_ } CORE::keys %{$map}};
  4         11  
  1         13  
904              
905 1 50       3 if ((scalar CORE::keys %{$map}) == 0) {
  1         5  
906             # Rename of zero attrs of input yields the input.
907 0         0 return $topic;
908             }
909              
910             # Expand map to specify all topic attrs being renamed to something.
911 1         3 my $inv_map = {reverse %{$map}};
  1         3  
912             $map = {CORE::map { ((
913 4 50       13 exists $inv_map->{$_} ? $inv_map->{$_} : $_
914 1         3 ) => $_) } CORE::keys %{$topic->_heading()}};
  1         4  
915              
916 1         4 my $result = $topic->_new();
917              
918 1         3 $result->_heading( {CORE::map { ($_ => undef) } CORE::keys %{$map}} );
  4         10  
  1         3  
919              
920 1         4 my $result_b = $result->_body();
921              
922 1         2 for my $topic_t (values %{$topic->_body()}) {
  1         2  
923             my $result_t = {CORE::map {
924 20         38 ($_ => $topic_t->{$map->{$_}})
925 5         7 } CORE::keys %{$map}};
  5         11  
926 5         18 $result_b->{refaddr $result_t} = $result_t;
927             }
928              
929 1 50       3 if ($topic->_is_known_dup_free()) {
930 1         3 $result->_is_known_dup_free( 1 );
931             }
932              
933 1         6 return $result;
934             }
935              
936             ###########################################################################
937              
938             sub projection {
939 1     1 0 9 my ($topic, $attr_names) = @_;
940              
941 1         8 (my $proj_h, $attr_names)
942             = $topic->_atnms_hr_from_assert_valid_atnms_arg(
943             'projection', '$attr_names', $attr_names );
944 1         3 my (undef, undef, $proj_only)
945             = $topic->_ptn_conj_and_disj( $topic->_heading(), $proj_h );
946             confess q{projection(): Bad $attr_names arg; that attr list}
947             . q{ isn't a subset of the invocant's heading.}
948 1 50       2 if @{$proj_only} > 0;
  1         5  
949              
950 1         4 return $topic->_projection( $attr_names );
951             }
952              
953             sub _projection {
954 1     1   3 my ($topic, $attr_names) = @_;
955              
956 1 50       2 if (@{$attr_names} == 0) {
  1         4  
957             # Projection of zero attrs yields identity relation zero or one.
958 0 0       0 if ($topic->is_empty()) {
959 0         0 return $topic->_new();
960             }
961             else {
962 0         0 return $topic->_new( [ {} ] );
963             }
964             }
965 1 50       2 if (@{$attr_names} == $topic->degree()) {
  1         3  
966             # Projection of all attrs of input yields the input.
967 0         0 return $topic;
968             }
969              
970 1         4 my $result = $topic->_new();
971              
972 1         3 $result->_heading( {CORE::map { ($_ => undef) } @{$attr_names}} );
  1         4  
  1         3  
973              
974 1         2 my $result_b = $result->_body();
975              
976 1         2 for my $topic_t (values %{$topic->_body()}) {
  1         3  
977             my $result_t
978 5         7 = {CORE::map { ($_ => $topic_t->{$_}) } @{$attr_names}};
  5         14  
  5         7  
979 5         15 $result_b->{refaddr $result_t} = $result_t;
980             }
981              
982 1         5 return $result;
983             }
984              
985             sub cmpl_proj {
986 0     0 0 0 my ($topic, $attr_names) = @_;
987              
988 0         0 my $topic_h = $topic->_heading();
989              
990 0         0 (my $cproj_h, $attr_names)
991             = $topic->_atnms_hr_from_assert_valid_atnms_arg(
992             'cmpl_proj', '$attr_names', $attr_names );
993 0         0 my (undef, undef, $cproj_only)
994             = $topic->_ptn_conj_and_disj( $topic_h, $cproj_h );
995             confess q{cmpl_proj(): Bad $attr_names arg; that attr list}
996             . q{ isn't a subset of the invocant's heading.}
997 0 0       0 if @{$cproj_only} > 0;
  0         0  
998              
999             return $topic->_projection(
1000 0         0 [grep { !$cproj_h->{$_} } CORE::keys %{$topic_h}] );
  0         0  
  0         0  
1001             }
1002              
1003             ###########################################################################
1004              
1005             sub wrap {
1006 0     0 0 0 my ($topic, $outer, $inner) = @_;
1007              
1008 0         0 $topic->_assert_valid_atnm_arg( 'wrap', '$outer', $outer );
1009 0         0 (my $inner_h, $inner) = $topic->_atnms_hr_from_assert_valid_atnms_arg(
1010             'wrap', '$inner', $inner );
1011              
1012 0         0 my (undef, $topic_attrs_no_wr, $inner_attrs_not_in_topic)
1013             = $topic->_ptn_conj_and_disj( $topic->_heading(), $inner_h );
1014             confess q{wrap(): Bad $inner arg; that list of attrs to be wrapped}
1015             . q{ isn't a subset of the invocant's heading.}
1016 0 0       0 if @{$inner_attrs_not_in_topic} > 0;
  0         0  
1017             confess q{wrap(): Bad $outer arg; that name for a new attr to add}
1018             . q{ to the invocant, consisting of wrapped invocant attrs,}
1019             . q{ duplicates an attr of the invocant not being wrapped.}
1020 0 0   0   0 if any { $_ eq $outer } @{$topic_attrs_no_wr};
  0         0  
  0         0  
1021              
1022 0         0 return $topic->_wrap( $outer, $inner, $topic_attrs_no_wr );
1023             }
1024              
1025             sub _wrap {
1026 0     0   0 my ($topic, $outer, $inner, $topic_attrs_no_wr) = @_;
1027              
1028 0         0 my $result = $topic->_new();
1029              
1030             $result->_heading(
1031 0         0 {CORE::map { ($_ => undef) } @{$topic_attrs_no_wr}, $outer} );
  0         0  
  0         0  
1032              
1033 0         0 my $topic_b = $topic->_body();
1034 0         0 my $result_b = $result->_body();
1035              
1036 0 0       0 if ($topic->is_empty()) {
    0          
    0          
1037             # An empty $topic means an empty result.
1038             # So $result_b is already correct.
1039             }
1040 0         0 elsif (@{$inner} == 0) {
1041             # Wrap zero $topic attrs as new attr.
1042             # So this is a simple static extension of $topic w static $outer.
1043 0         0 my $inner_t = {};
1044 0         0 for my $topic_t (values %{$topic_b}) {
  0         0  
1045 0         0 my $result_t = {$outer => $inner_t, {%{$topic_t}}};
  0         0  
1046 0         0 $result_b->{refaddr $result_t} = $result_t;
1047             }
1048             }
1049 0         0 elsif (@{$topic_attrs_no_wr} == 0) {
1050             # Wrap all $topic attrs as new attr.
1051 0         0 for my $topic_t_refaddr (CORE::keys %{$topic_b}) {
  0         0  
1052 0         0 my $result_t = {$outer => $topic_b->{$topic_t_refaddr}};
1053 0         0 $result_b->{refaddr $result_t} = $result_t;
1054             }
1055             }
1056             else {
1057             # Wrap at least one but not all $topic attrs as new attr.
1058 0         0 for my $topic_t (values %{$topic_b}) {
  0         0  
1059 0         0 my $inner_t = {CORE::map { ($_ => $topic_t->{$_}) } @{$inner}};
  0         0  
  0         0  
1060             my $result_t = {
1061             $outer => $inner_t,
1062 0         0 CORE::map { ($_ => $topic_t->{$_}) } @{$topic_attrs_no_wr}
  0         0  
  0         0  
1063             };
1064 0         0 $result_b->{refaddr $result_t} = $result_t;
1065             }
1066             }
1067              
1068 0 0       0 if ($topic->_is_known_dup_free()) {
1069 0         0 $result->_is_known_dup_free( 1 );
1070             }
1071              
1072 0         0 return $result;
1073             }
1074              
1075             sub cmpl_wrap {
1076 0     0 0 0 my ($topic, $outer, $cmpl_inner) = @_;
1077              
1078 0         0 $topic->_assert_valid_atnm_arg( 'cmpl_wrap', '$outer', $outer );
1079 0         0 (my $cmpl_inner_h, $cmpl_inner)
1080             = $topic->_atnms_hr_from_assert_valid_atnms_arg(
1081             'cmpl_wrap', '$cmpl_inner', $cmpl_inner );
1082              
1083 0         0 my $topic_h = $topic->_heading();
1084              
1085             confess q{cmpl_wrap(): Bad $cmpl_inner arg; that attr list}
1086             . q{ isn't a subset of the invocant's heading.}
1087 0 0   0   0 if notall { exists $topic_h->{$_} } @{$cmpl_inner};
  0         0  
  0         0  
1088              
1089 0         0 my $inner = [grep { !$cmpl_inner_h->{$_} } CORE::keys %{$topic_h}];
  0         0  
  0         0  
1090 0         0 my $inner_h = {CORE::map { $_ => undef } @{$inner}};
  0         0  
  0         0  
1091              
1092 0         0 my (undef, $topic_attrs_no_wr, undef)
1093             = $topic->_ptn_conj_and_disj( $topic_h, $inner_h );
1094             confess q{cmpl_wrap(): Bad $outer arg; that name for a new attr to add}
1095             . q{ to the invocant, consisting of wrapped invocant attrs,}
1096             . q{ duplicates an attr of the invocant not being wrapped.}
1097 0 0   0   0 if any { $_ eq $outer } @{$topic_attrs_no_wr};
  0         0  
  0         0  
1098              
1099 0         0 return $topic->_wrap( $outer, $inner, $topic_attrs_no_wr );
1100             }
1101              
1102             ###########################################################################
1103              
1104             sub unwrap {
1105 0     0 0 0 my ($topic, $inner, $outer) = @_;
1106              
1107 0         0 (my $inner_h, $inner) = $topic->_atnms_hr_from_assert_valid_atnms_arg(
1108             'unwrap', '$inner', $inner );
1109 0         0 $topic->_assert_valid_atnm_arg( 'unwrap', '$outer', $outer );
1110              
1111 0         0 my $topic_h = $topic->_heading();
1112              
1113             confess q{unwrap(): Bad $outer arg; that attr name}
1114             . q{ doesn't match an attr of the invocant's heading.}
1115 0 0       0 if !exists $topic_h->{$outer};
1116              
1117 0         0 my $topic_h_except_outer = {%{$topic_h}};
  0         0  
1118 0         0 delete $topic_h_except_outer->{$outer};
1119              
1120 0         0 my ($inner_attrs_dupl_topic, $topic_attrs_no_uwr, undef)
1121             = $topic->_ptn_conj_and_disj( $topic_h_except_outer, $inner_h );
1122             confess q{unwrap(): Bad $inner arg; at least one name in that attr}
1123             . q{ list, which the invocant would be extended with when}
1124             . q{ unwrapping $topic{$outer}, duplicates an attr of the}
1125             . q{ invocant not being unwrapped.}
1126 0 0       0 if @{$inner_attrs_dupl_topic} > 0;
  0         0  
1127              
1128 0         0 my $topic_b = $topic->_body();
1129              
1130 0         0 for my $topic_t (values %{$topic_b}) {
  0         0  
1131 0         0 my $inner_t = $topic_t->{$outer};
1132 0 0 0     0 confess q{unwrap(): Can't unwrap $topic{$outer} because there is}
1133             . q{ not a same-heading tuple value for the $outer attr of}
1134             . q{ every tuple of $topic whose heading matches $inner.}
1135             if ref $inner_t ne 'HASH'
1136             or !$topic->_is_identical_hkeys( $inner_h, $inner_t );
1137             }
1138              
1139 0         0 my $result = $topic->_new();
1140              
1141 0         0 $result->_heading( {%{$topic_h_except_outer}, %{$inner_h}} );
  0         0  
  0         0  
1142              
1143 0         0 my $result_b = $result->_body();
1144              
1145 0 0       0 if ($topic->is_empty()) {
    0          
    0          
1146             # An empty $topic means an empty result.
1147             # So $result_b is already correct.
1148             }
1149 0         0 elsif (@{$topic_attrs_no_uwr} == 0) {
1150             # Only $topic attr is $outer, all result attrs from $outer unwrap.
1151 0         0 for my $topic_t (values %{$topic_b}) {
  0         0  
1152 0         0 my $result_t = $topic_t->{$outer};
1153 0         0 $result_b->{refaddr $result_t} = $result_t;
1154             }
1155             }
1156 0         0 elsif (@{$inner} == 0) {
1157             # Unwrap of $outer adds zero attrs to $topic.
1158             # So this is a simple projection of $topic excising $outer.
1159 0         0 for my $topic_t (values %{$topic_b}) {
  0         0  
1160             my $result_t = {
1161 0         0 CORE::map { ($_ => $topic_t->{$_}) } @{$topic_attrs_no_uwr}
  0         0  
  0         0  
1162             };
1163 0         0 $result_b->{refaddr $result_t} = $result_t;
1164             }
1165             }
1166             else {
1167             # Result has at least 1 attr from $outer, at least 1 not from it.
1168 0         0 for my $topic_t (values %{$topic_b}) {
  0         0  
1169             my $result_t = {
1170 0         0 %{$topic_t->{$outer}},
1171 0         0 CORE::map { ($_ => $topic_t->{$_}) } @{$topic_attrs_no_uwr}
  0         0  
  0         0  
1172             };
1173 0         0 $result_b->{refaddr $result_t} = $result_t;
1174             }
1175             }
1176              
1177 0 0       0 if ($topic->_is_known_dup_free()) {
1178 0         0 $result->_is_known_dup_free( 1 );
1179             }
1180              
1181 0         0 return $result;
1182             }
1183              
1184             ###########################################################################
1185              
1186             sub group {
1187 1     1 0 114504 my ($topic, $outer, $inner) = @_;
1188              
1189 1         12 $topic->_assert_valid_atnm_arg( 'group', '$outer', $outer );
1190 1         6 (my $inner_h, $inner) = $topic->_atnms_hr_from_assert_valid_atnms_arg(
1191             'group', '$inner', $inner );
1192              
1193 1         5 my (undef, $topic_attrs_no_gr, $inner_attrs_not_in_topic)
1194             = $topic->_ptn_conj_and_disj( $topic->_heading(), $inner_h );
1195             confess q{group(): Bad $inner arg; that list of attrs to be grouped}
1196             . q{ isn't a subset of the invocant's heading.}
1197 1 50       3 if @{$inner_attrs_not_in_topic} > 0;
  1         4  
1198             confess q{group(): Bad $outer arg; that name for a new attr to add}
1199             . q{ to the invocant, consisting of grouped invocant attrs,}
1200             . q{ duplicates an attr of the invocant not being grouped.}
1201 1 50   1   6 if any { $_ eq $outer } @{$topic_attrs_no_gr};
  1         4  
  1         5  
1202              
1203 1         9 return $topic->_group( $outer, $inner, $topic_attrs_no_gr, $inner_h );
1204             }
1205              
1206             sub _group {
1207 1     1   4 my ($topic, $outer, $inner, $topic_attrs_no_gr, $inner_h) = @_;
1208              
1209 1         6 my $result = $topic->_new();
1210              
1211             $result->_heading(
1212 1         3 {CORE::map { ($_ => undef) } @{$topic_attrs_no_gr}, $outer} );
  2         8  
  1         3  
1213              
1214 1 50       6 if ($topic->is_empty()) {
    50          
    50          
1215             # An empty $topic means an empty result.
1216             # So result body is already correct.
1217             }
1218 1         5 elsif (@{$inner} == 0) {
1219             # Group zero $topic attrs as new attr.
1220             # So this is a simple static extension of $topic w static $outer.
1221 0         0 my $result_b = $result->_body();
1222 0         0 my $inner_r = $topic->_new( [ {} ] );
1223 0         0 for my $topic_t (values %{$topic->_body()}) {
  0         0  
1224 0         0 my $result_t = {$outer => $inner_r, {%{$topic_t}}};
  0         0  
1225 0         0 $result_b->{refaddr $result_t} = $result_t;
1226             }
1227             }
1228 1         4 elsif (@{$topic_attrs_no_gr} == 0) {
1229             # Group all $topic attrs as new attr.
1230             # So $topic is just used as sole attr of sole tuple of result.
1231 0         0 my $result_t = {$outer => $topic};
1232 0         0 $result->_body( {refaddr $result_t => $result_t} );
1233             }
1234             else {
1235             # Group at least one but not all $topic attrs as new attr.
1236 1         3 my $result_b = $result->_body();
1237 1         5 my $topic_index = $topic->_want_index( $topic_attrs_no_gr );
1238 1         2 for my $matched_topic_b (values %{$topic_index}) {
  1         5  
1239              
1240 3         7 my $inner_r = $topic->_new();
1241 3         9 $inner_r->_heading( $inner_h );
1242 3         7 my $inner_b = $inner_r->_body();
1243 3         5 for my $topic_t (values %{$matched_topic_b}) {
  3         8  
1244             my $inner_t
1245 5         9 = {CORE::map { ($_ => $topic_t->{$_}) } @{$inner}};
  15         36  
  5         9  
1246 5         22 $inner_b->{refaddr $inner_t} = $inner_t;
1247             }
1248              
1249 3         5 my $any_mtpt = (values %{$matched_topic_b})[0];
  3         8  
1250              
1251             my $result_t = {
1252             $outer => $inner_r,
1253 3         6 CORE::map { ($_ => $any_mtpt->{$_}) } @{$topic_attrs_no_gr}
  3         7  
  3         6  
1254             };
1255 3         13 $result_b->{refaddr $result_t} = $result_t;
1256             }
1257             }
1258              
1259 1 50       3 if ($topic->_is_known_dup_free()) {
1260 1         4 $result->_is_known_dup_free( 1 );
1261             }
1262              
1263 1         5 return $result;
1264             }
1265              
1266             sub cmpl_group {
1267 0     0 0 0 my ($topic, $outer, $group_per) = @_;
1268              
1269 0         0 $topic->_assert_valid_atnm_arg( 'cmpl_group', '$outer', $outer );
1270 0         0 (my $group_per_h, $group_per)
1271             = $topic->_atnms_hr_from_assert_valid_atnms_arg(
1272             'cmpl_group', '$group_per', $group_per );
1273              
1274 0         0 my $topic_h = $topic->_heading();
1275              
1276             confess q{cmpl_group(): Bad $group_per arg; that attr list}
1277             . q{ isn't a subset of the invocant's heading.}
1278 0 0   0   0 if notall { exists $topic_h->{$_} } @{$group_per};
  0         0  
  0         0  
1279              
1280 0         0 my $inner = [grep { !$group_per_h->{$_} } CORE::keys %{$topic_h}];
  0         0  
  0         0  
1281 0         0 my $inner_h = {CORE::map { $_ => undef } @{$inner}};
  0         0  
  0         0  
1282              
1283 0         0 my (undef, $topic_attrs_no_gr, undef)
1284             = $topic->_ptn_conj_and_disj( $topic_h, $inner_h );
1285             confess q{cmpl_group(): Bad $outer arg; that name for a new attr to}
1286             . q{ add to th invocant, consisting of grouped invocant attrs,}
1287             . q{ duplicates an attr of the invocant not being grouped.}
1288 0 0   0   0 if any { $_ eq $outer } @{$topic_attrs_no_gr};
  0         0  
  0         0  
1289              
1290 0         0 return $topic->_group( $outer, $inner, $topic_attrs_no_gr, $inner_h );
1291             }
1292              
1293             ###########################################################################
1294              
1295             sub ungroup {
1296 0     0 0 0 my ($topic, $inner, $outer) = @_;
1297              
1298 0         0 (my $inner_h, $inner) = $topic->_atnms_hr_from_assert_valid_atnms_arg(
1299             'ungroup', '$inner', $inner );
1300 0         0 $topic->_assert_valid_atnm_arg( 'ungroup', '$outer', $outer );
1301              
1302 0         0 my $topic_h = $topic->_heading();
1303              
1304             confess q{ungroup(): Bad $outer arg; that attr name}
1305             . q{ doesn't match an attr of the invocant's heading.}
1306 0 0       0 if !exists $topic_h->{$outer};
1307              
1308 0         0 my $topic_h_except_outer = {%{$topic_h}};
  0         0  
1309 0         0 delete $topic_h_except_outer->{$outer};
1310              
1311 0         0 my ($inner_attrs_dupl_topic, $topic_attrs_no_ugr, undef)
1312             = $topic->_ptn_conj_and_disj( $topic_h_except_outer, $inner_h );
1313             confess q{ungroup(): Bad $inner arg; at least one name in that attr}
1314             . q{ list, which the invocant would be extended with when}
1315             . q{ ungrouping $topic{$outer}, duplicates an attr of the}
1316             . q{ invocant not being ungrouped.}
1317 0 0       0 if @{$inner_attrs_dupl_topic} > 0;
  0         0  
1318              
1319 0         0 my $topic_b = $topic->_body();
1320              
1321 0         0 for my $topic_t (values %{$topic_b}) {
  0         0  
1322 0         0 my $inner_r = $topic_t->{$outer};
1323 0 0 0     0 confess q{ungroup(): Can't ungroup $topic{$outer} because there is}
      0        
1324             . q{ not a same-heading relation val for the $outer attr}
1325             . q{ of every tuple of $topic whose head matches $inner.}
1326             if !blessed $inner_r or !$inner_r->isa( __PACKAGE__ )
1327             or !$topic->_is_identical_hkeys(
1328             $inner_h, $inner_r->_heading() );
1329             }
1330              
1331 0 0       0 if ($topic->degree() == 1) {
1332             # Ungroup of a unary relation is the N-adic union of its sole
1333             # attribute's value across all tuples.
1334             return $topic->_new( $inner )
1335 0         0 ->_union( [CORE::map { $_->{$outer} } values %{$topic_b}] );
  0         0  
  0         0  
1336             }
1337              
1338             # If we get here, the input relation is not unary.
1339              
1340 0         0 my $result = $topic->_new();
1341              
1342 0         0 $result->_heading( {%{$topic_h_except_outer}, %{$inner_h}} );
  0         0  
  0         0  
1343              
1344             my $topic_tuples_w_nonemp_inn
1345 0         0 = [grep { !$_->{$outer}->is_empty() } values %{$topic_b}];
  0         0  
  0         0  
1346              
1347 0 0       0 if (@{$topic_tuples_w_nonemp_inn} == 0) {
  0 0       0  
1348             # An empty post-basic-filtering $topic means an empty result.
1349             # So result body is already correct.
1350             }
1351 0         0 elsif (@{$inner} == 0) {
1352             # Ungroup of $outer adds zero attrs to $topic.
1353             # So this is a simple proj of post-basic-filt $topic excis $outer.
1354 0         0 my $result_b = $result->_body();
1355 0         0 for my $topic_t (@{$topic_tuples_w_nonemp_inn}) {
  0         0  
1356             my $result_t = {
1357 0         0 CORE::map { ($_ => $topic_t->{$_}) } @{$topic_attrs_no_ugr}
  0         0  
  0         0  
1358             };
1359 0         0 $result_b->{refaddr $result_t} = $result_t;
1360             }
1361             }
1362             else {
1363             # Result has at least 1 attr from $outer, at least 1 not from it.
1364 0         0 my $result_b = $result->_body();
1365 0         0 for my $topic_t (@{$topic_tuples_w_nonemp_inn}) {
  0         0  
1366 0         0 my $no_ugr_t = {CORE::map { ($_ => $topic_t->{$_}) }
1367 0         0 @{$topic_attrs_no_ugr}};
  0         0  
1368 0         0 my $inner_r = $topic_t->{$outer};
1369 0         0 for my $inner_t (values %{$inner_r->_body()}) {
  0         0  
1370 0         0 my $result_t = {%{$inner_t}, %{$no_ugr_t}};
  0         0  
  0         0  
1371 0         0 $result_b->{refaddr $result_t} = $result_t;
1372             }
1373             }
1374             }
1375              
1376 0         0 return $result;
1377             }
1378              
1379             ###########################################################################
1380              
1381             sub tclose {
1382 0     0 0 0 my ($topic) = @_;
1383              
1384 0 0       0 confess q{tclose(): This method may only be invoked on a}
1385             . q{ Set::Relation object with exactly 2 (same-typed) attrs.}
1386             if $topic->degree() != 2;
1387              
1388 0 0       0 if ($topic->cardinality() < 2) {
1389             # Can't create paths of 2+ arcs when not more than 1 arc exists.
1390 0         0 return $topic;
1391             }
1392              
1393             # If we get here, there are at least 2 arcs, so there is a chance they
1394             # may connect into longer paths.
1395              
1396 0         0 my ($atnm1, $atnm2) = sort (CORE::keys %{$topic->_heading()});
  0         0  
1397              
1398 0         0 return $topic->_rename( { 'x' => $atnm1, 'y' => $atnm2 } )
1399             ->_tclose_of_xy()
1400             ->_rename( { $atnm1 => 'x', $atnm2 => 'y' } );
1401             }
1402              
1403             # TODO: Reimplement tclose to do all the work internally rather
1404             # than farming out to rename/join/projection/union/etc; this should make
1405             # performance an order of magnitude better and without being complicated.
1406              
1407             sub _tclose_of_xy {
1408 0     0   0 my ($xy) = @_;
1409              
1410 0 0       0 if (!$xy->_is_known_dup_free()) {
1411 0         0 $xy->_dup_free_want_index_over_all_attrs();
1412             }
1413              
1414 0         0 my $xyz = $xy->_rename( { 'y' => 'x', 'z' => 'y' } )
1415             ->_regular_join( $xy, ['y'], ['z'], ['x'] );
1416              
1417 0 0       0 if ($xyz->is_empty()) {
1418             # No paths of xy connect to any other paths of xy.
1419 0         0 return $xy;
1420             }
1421              
1422             # If we get here, then at least one pair of paths in xy can connect
1423             # to form a longer path.
1424              
1425 0         0 my $ttt = $xyz->_projection( ['x', 'z'] )
1426             ->_rename( { 'y' => 'z' } )
1427             ->_union( [$xy] );
1428              
1429 0 0       0 if ($ttt->_is_identical( $xy )) {
1430             # All the longer paths resulting from conn were already in xy.
1431 0         0 return $xy;
1432             }
1433              
1434             # If we get here, then at least one longer path produced above was not
1435             # already in xy and was added; so now we need to check if any
1436             # yet-longer paths can be made from the just-produced.
1437              
1438 0         0 return $ttt->_tclose_of_xy();
1439             }
1440              
1441             ###########################################################################
1442              
1443             sub restriction {
1444 16     16 0 700 my ($topic, $func, $allow_dup_tuples) = @_;
1445              
1446 16         47 $topic->_assert_valid_func_arg( 'restriction', '$func', $func );
1447              
1448 16 50       34 if ($topic->is_empty()) {
1449 0         0 return $topic;
1450             }
1451              
1452 16 50 33     88 if (!$allow_dup_tuples and !$topic->_is_known_dup_free()) {
1453 0         0 $topic->_dup_free_want_index_over_all_attrs();
1454             }
1455              
1456 16         40 my $result = $topic->empty();
1457              
1458 16         31 my $topic_b = $topic->_body();
1459 16         26 my $result_b = $result->_body();
1460              
1461 16         26 for my $topic_t_refaddr (CORE::keys %{$topic_b}) {
  16         53  
1462 74         106 my $topic_t = $topic_b->{$topic_t_refaddr};
1463 74         93 my $is_matched;
1464             {
1465 74         89 local $_ = $topic->_export_nfmt_tuple( $topic_t );
  74         123  
1466 74         167 $is_matched = $func->();
1467             }
1468 74 100       344 if ($is_matched) {
1469 16         34 $result_b->{$topic_t_refaddr} = $topic_t;
1470             }
1471             }
1472              
1473 16 50       36 if ($topic->_is_known_dup_free()) {
1474 16         32 $result->_is_known_dup_free( 1 );
1475             }
1476              
1477 16         38 return $result;
1478             }
1479              
1480             sub restr_and_cmpl {
1481 0     0 0 0 my ($topic, $func, $allow_dup_tuples) = @_;
1482 0         0 $topic->_assert_valid_func_arg( 'restr_and_cmpl', '$func', $func );
1483 0         0 return $topic->_restr_and_cmpl( $func, $allow_dup_tuples );
1484             }
1485              
1486             sub _restr_and_cmpl {
1487 0     0   0 my ($topic, $func, $allow_dup_tuples) = @_;
1488              
1489 0 0       0 if ($topic->is_empty()) {
1490 0         0 return [$topic, $topic];
1491             }
1492              
1493 0 0 0     0 if (!$allow_dup_tuples and !$topic->_is_known_dup_free()) {
1494 0         0 $topic->_dup_free_want_index_over_all_attrs();
1495             }
1496              
1497 0         0 my $pass_result = $topic->empty();
1498 0         0 my $fail_result = $topic->empty();
1499              
1500 0         0 my $topic_b = $topic->_body();
1501 0         0 my $pass_result_b = $pass_result->_body();
1502 0         0 my $fail_result_b = $fail_result->_body();
1503              
1504 0         0 for my $topic_t_refaddr (CORE::keys %{$topic_b}) {
  0         0  
1505 0         0 my $topic_t = $topic_b->{$topic_t_refaddr};
1506 0         0 my $is_matched;
1507             {
1508 0         0 local $_ = $topic->_export_nfmt_tuple( $topic_t );
  0         0  
1509 0         0 $is_matched = $func->();
1510             }
1511 0 0       0 if ($is_matched) {
1512 0         0 $pass_result_b->{$topic_t_refaddr} = $topic_t;
1513             }
1514             else {
1515 0         0 $fail_result_b->{$topic_t_refaddr} = $topic_t;
1516             }
1517             }
1518              
1519 0 0       0 if ($topic->_is_known_dup_free()) {
1520 0         0 $pass_result->_is_known_dup_free( 1 );
1521 0         0 $fail_result->_is_known_dup_free( 1 );
1522             }
1523              
1524 0         0 return [$pass_result, $fail_result];
1525             }
1526              
1527             sub cmpl_restr {
1528 0     0 0 0 my ($topic, $func, $allow_dup_tuples) = @_;
1529              
1530 0         0 $topic->_assert_valid_func_arg( 'cmpl_restr', '$func', $func );
1531              
1532 0 0       0 if ($topic->is_empty()) {
1533 0         0 return $topic;
1534             }
1535              
1536 0 0 0     0 if (!$allow_dup_tuples and !$topic->_is_known_dup_free()) {
1537 0         0 $topic->_dup_free_want_index_over_all_attrs();
1538             }
1539              
1540 0         0 my $result = $topic->empty();
1541              
1542 0         0 my $topic_b = $topic->_body();
1543 0         0 my $result_b = $result->_body();
1544              
1545 0         0 for my $topic_t_refaddr (CORE::keys %{$topic_b}) {
  0         0  
1546 0         0 my $topic_t = $topic_b->{$topic_t_refaddr};
1547 0         0 my $is_matched;
1548             {
1549 0         0 local $_ = $topic->_export_nfmt_tuple( $topic_t );
  0         0  
1550 0         0 $is_matched = $func->();
1551             }
1552 0 0       0 if (!$is_matched) {
1553 0         0 $result_b->{$topic_t_refaddr} = $topic_t;
1554             }
1555             }
1556              
1557 0 0       0 if ($topic->_is_known_dup_free()) {
1558 0         0 $result->_is_known_dup_free( 1 );
1559             }
1560              
1561 0         0 return $result;
1562             }
1563              
1564             ###########################################################################
1565              
1566             sub classification {
1567 0     0 0 0 my ($topic, $func, $class_attr_name, $group_attr_name,
1568             $allow_dup_tuples) = @_;
1569              
1570 0         0 $topic->_assert_valid_func_arg( 'classification', '$func', $func );
1571 0         0 $topic->_assert_valid_atnm_arg(
1572             'classification', '$class_attr_name', $class_attr_name );
1573 0         0 $topic->_assert_valid_atnm_arg(
1574             'classification', '$group_attr_name', $group_attr_name );
1575              
1576 0         0 my $result = $topic->_new();
1577              
1578 0         0 $result->_heading(
1579             {$class_attr_name => undef, $group_attr_name => undef} );
1580              
1581 0 0       0 if ($topic->is_empty()) {
1582             # An empty $topic means an empty result.
1583             # So result body is already correct.
1584 0         0 return $result;
1585             }
1586              
1587 0 0 0     0 if (!$allow_dup_tuples and !$topic->_is_known_dup_free()) {
1588 0         0 $topic->_dup_free_want_index_over_all_attrs();
1589             }
1590              
1591 0         0 my $topic_h = $topic->_heading();
1592 0         0 my $topic_b = $topic->_body();
1593              
1594 0         0 my $tuples_per_class = {};
1595              
1596 0         0 for my $topic_t (values %{$topic_b}) {
  0         0  
1597 0         0 my $class;
1598             {
1599 0         0 local $_ = $topic->_export_nfmt_tuple( $topic_t );
  0         0  
1600 0         0 $class = $func->();
1601             }
1602 0         0 my $class_ident_str = $topic->_ident_str( $class );
1603 0 0       0 if (!exists $tuples_per_class->{$class_ident_str}) {
1604 0         0 $tuples_per_class->{$class_ident_str} = [$class, []];
1605             }
1606 0         0 push @{$tuples_per_class->{$class_ident_str}->[1]}, $topic_t;
  0         0  
1607             }
1608              
1609 0         0 my $result_b = $result->_body();
1610 0         0 for my $class_ident_str (CORE::keys %{$tuples_per_class}) {
  0         0  
1611             my ($class, $tuples_in_class)
1612 0         0 = @{$tuples_per_class->{$class_ident_str}};
  0         0  
1613              
1614 0         0 my $inner_r = $topic->_new();
1615 0         0 $inner_r->_heading( $topic_h );
1616             $inner_r->_body(
1617 0         0 {CORE::map { (refaddr $_ => $_) } @{$tuples_in_class}} );
  0         0  
  0         0  
1618              
1619 0         0 my $result_t = {
1620             $class_attr_name => $class,
1621             $group_attr_name => $inner_r,
1622             };
1623 0         0 $result_b->{refaddr $result_t} = $result_t;
1624             }
1625              
1626 0 0       0 if ($topic->_is_known_dup_free()) {
1627 0         0 $result->_is_known_dup_free( 1 );
1628             }
1629              
1630 0         0 return $result;
1631             }
1632              
1633             ###########################################################################
1634              
1635             sub extension {
1636 0     0 0 0 my ($topic, $attr_names, $func, $allow_dup_tuples) = @_;
1637              
1638 0         0 (my $exten_h, $attr_names)
1639             = $topic->_atnms_hr_from_assert_valid_atnms_arg(
1640             'extension', '$attr_names', $attr_names );
1641 0         0 $topic->_assert_valid_func_arg( 'extension', '$func', $func );
1642              
1643 0         0 my ($both, undef, undef)
1644             = $topic->_ptn_conj_and_disj( $topic->_heading(), $exten_h );
1645             confess q{extension(): Bad $attr_names arg; that attr list}
1646             . q{ isn't disjoint with the invocant's heading.}
1647 0 0       0 if @{$both} > 0;
  0         0  
1648              
1649 0         0 return $topic->_extension(
1650             $attr_names, $func, $exten_h, $allow_dup_tuples );
1651             }
1652              
1653             sub _extension {
1654 0     0   0 my ($topic, $attr_names, $func, $exten_h, $allow_dup_tuples) = @_;
1655              
1656 0 0       0 if (@{$attr_names} == 0) {
  0         0  
1657             # Extension of input by zero attrs yields the input.
1658 0         0 return $topic;
1659             }
1660              
1661 0 0 0     0 if (!$allow_dup_tuples and !$topic->_is_known_dup_free()) {
1662 0         0 $topic->_dup_free_want_index_over_all_attrs();
1663             }
1664              
1665 0         0 my $result = $topic->_new();
1666              
1667 0         0 $result->_heading( {%{$topic->_heading()}, %{$exten_h}} );
  0         0  
  0         0  
1668              
1669 0         0 my $result_b = $result->_body();
1670              
1671 0         0 for my $topic_t (values %{$topic->_body()}) {
  0         0  
1672 0         0 my $exten_t;
1673             {
1674 0         0 local $_ = $topic->_export_nfmt_tuple( $topic_t );
  0         0  
1675 0         0 $exten_t = $func->();
1676             }
1677             $topic->_assert_valid_tuple_result_of_func_arg(
1678 0         0 'extension', '$func', '$attr_names', $exten_t, $exten_h );
1679 0         0 $exten_t = $topic->_import_nfmt_tuple( $exten_t );
1680 0         0 my $result_t = {%{$topic_t}, %{$exten_t}};
  0         0  
  0         0  
1681 0         0 $result_b->{refaddr $result_t} = $result_t;
1682             }
1683              
1684 0 0       0 if ($topic->_is_known_dup_free()) {
1685 0         0 $result->_is_known_dup_free( 1 );
1686             }
1687              
1688 0         0 return $result;
1689             }
1690              
1691             ###########################################################################
1692              
1693             sub static_exten {
1694 0     0 0 0 my ($topic, $attrs) = @_;
1695              
1696 0 0       0 confess q{static_exten(): Bad $attrs arg; it isn't a hash-ref.}
1697             if ref $attrs ne 'HASH';
1698              
1699 0         0 my ($both, undef, undef)
1700             = $topic->_ptn_conj_and_disj( $topic->_heading(), $attrs );
1701             confess q{static_exten(): Bad $attrs arg; that attr list}
1702             . q{ isn't disjoint with the invocant's heading.}
1703 0 0       0 if @{$both} > 0;
  0         0  
1704              
1705 0 0       0 confess q{static_exten(): Bad $attrs arg;}
1706             . q{ it is a hash-ref, and there exist circular refs}
1707             . q{ between itself or its tuple-valued components.}
1708             if $topic->_tuple_arg_has_circular_refs( $attrs );
1709              
1710 0         0 return $topic->_static_exten( $attrs );
1711             }
1712              
1713             sub _static_exten {
1714 0     0   0 my ($topic, $attrs) = @_;
1715              
1716 0 0       0 if ((scalar CORE::keys %{$attrs}) == 0) {
  0         0  
1717             # Extension of input by zero attrs yields the input.
1718 0         0 return $topic;
1719             }
1720              
1721 0         0 $attrs = $topic->_import_nfmt_tuple( $attrs );
1722              
1723 0         0 my $result = $topic->_new();
1724              
1725 0         0 $result->_heading( {%{$topic->_heading()},
1726 0         0 CORE::map { ($_ => undef) } CORE::keys %{$attrs}} );
  0         0  
  0         0  
1727              
1728 0         0 my $result_b = $result->_body();
1729              
1730 0         0 for my $topic_t (values %{$topic->_body()}) {
  0         0  
1731 0         0 my $result_t = {%{$topic_t}, %{$attrs}};
  0         0  
  0         0  
1732 0         0 $result_b->{refaddr $result_t} = $result_t;
1733             }
1734              
1735 0 0       0 if ($topic->_is_known_dup_free()) {
1736 0         0 $result->_is_known_dup_free( 1 );
1737             }
1738              
1739 0         0 return $result;
1740             }
1741              
1742             ###########################################################################
1743              
1744             sub map {
1745 0     0 0 0 my ($topic, $result_attr_names, $func, $allow_dup_tuples) = @_;
1746              
1747 0         0 (my $result_h, $result_attr_names)
1748             = $topic->_atnms_hr_from_assert_valid_atnms_arg(
1749             'map', '$result_attr_names', $result_attr_names );
1750 0         0 $topic->_assert_valid_func_arg( 'map', '$func', $func );
1751              
1752 0 0       0 if (@{$result_attr_names} == 0) {
  0         0  
1753             # Map to zero attrs yields identity relation zero or one.
1754 0 0       0 if ($topic->is_empty()) {
1755 0         0 return $topic->_new();
1756             }
1757             else {
1758 0         0 return $topic->_new( [ {} ] );
1759             }
1760             }
1761              
1762 0 0 0     0 if (!$allow_dup_tuples and !$topic->_is_known_dup_free()) {
1763 0         0 $topic->_dup_free_want_index_over_all_attrs();
1764             }
1765              
1766 0         0 my $result = $topic->_new();
1767              
1768 0         0 $result->_heading( $result_h );
1769              
1770 0         0 my $result_b = $result->_body();
1771              
1772 0         0 for my $topic_t (values %{$topic->_body()}) {
  0         0  
1773 0         0 my $result_t;
1774             {
1775 0         0 local $_ = $topic->_export_nfmt_tuple( $topic_t );
  0         0  
1776 0         0 $result_t = $func->();
1777             }
1778             $topic->_assert_valid_tuple_result_of_func_arg(
1779 0         0 'map', '$func', '$result_attr_names', $result_t, $result_h );
1780 0         0 $result_t = $topic->_import_nfmt_tuple( $result_t );
1781 0         0 $result_b->{refaddr $result_t} = $result_t;
1782             }
1783              
1784 0         0 return $result;
1785             }
1786              
1787             ###########################################################################
1788              
1789             sub summary {
1790 0     0 0 0 my ($topic, $group_per, $summ_attr_names, $summ_func,
1791             $allow_dup_tuples) = @_;
1792              
1793 0         0 (my $group_per_h, $group_per)
1794             = $topic->_atnms_hr_from_assert_valid_atnms_arg(
1795             'summary', '$group_per', $group_per );
1796 0         0 (my $exten_h, $summ_attr_names)
1797             = $topic->_atnms_hr_from_assert_valid_atnms_arg(
1798             'summary', '$summ_attr_names', $summ_attr_names );
1799 0         0 $topic->_assert_valid_func_arg( 'summary', '$summ_func', $summ_func );
1800              
1801 0         0 my $topic_h = $topic->_heading();
1802              
1803             confess q{summary(): Bad $group_per arg; that attr list}
1804             . q{ isn't a subset of the invocant's heading.}
1805 0 0   0   0 if notall { exists $topic_h->{$_} } @{$group_per};
  0         0  
  0         0  
1806              
1807             confess q{summary(): Bad $summ_attr_names arg; one or more of those}
1808             . q{ names for new summary attrs to add to the invocant }
1809             . q{ duplicates an attr of the invocant not being grouped.}
1810 0 0   0   0 if any { exists $group_per_h->{$_} } @{$summ_attr_names};
  0         0  
  0         0  
1811              
1812 0         0 my $inner = [grep { !$group_per_h->{$_} } CORE::keys %{$topic_h}];
  0         0  
  0         0  
1813 0         0 my $inner_h = {CORE::map { $_ => undef } @{$inner}};
  0         0  
  0         0  
1814              
1815 0         0 my (undef, $topic_attrs_no_gr, undef)
1816             = $topic->_ptn_conj_and_disj( $topic_h, $inner_h );
1817              
1818 0         0 my $result = $topic->_new();
1819              
1820 0         0 $result->_heading( {%{$group_per_h}, %{$exten_h}} );
  0         0  
  0         0  
1821              
1822 0 0       0 if ($topic->is_empty()) {
1823             # An empty $topic means an empty result.
1824 0         0 return $result;
1825             }
1826              
1827 0 0 0     0 if (!$allow_dup_tuples and !$topic->_is_known_dup_free()) {
1828 0         0 $topic->_dup_free_want_index_over_all_attrs();
1829             }
1830              
1831             # Note: We skipped a number of shortcuts that _group() has for
1832             # brevity, leaving just the general case; they might come back later.
1833              
1834 0         0 my $result_b = $result->_body();
1835 0         0 my $topic_index = $topic->_want_index( $topic_attrs_no_gr );
1836 0         0 for my $matched_topic_b (values %{$topic_index}) {
  0         0  
1837              
1838 0         0 my $inner_r = $topic->_new();
1839 0         0 $inner_r->_heading( $inner_h );
1840 0         0 my $inner_b = $inner_r->_body();
1841 0         0 for my $topic_t (values %{$matched_topic_b}) {
  0         0  
1842 0         0 my $inner_t = {CORE::map { ($_ => $topic_t->{$_}) } @{$inner}};
  0         0  
  0         0  
1843 0         0 $inner_b->{refaddr $inner_t} = $inner_t;
1844             }
1845              
1846 0         0 my $any_mtpt = (values %{$matched_topic_b})[0];
  0         0  
1847 0         0 my $group_per_t = {CORE::map { ($_ => $any_mtpt->{$_}) }
1848 0         0 @{$topic_attrs_no_gr}};
  0         0  
1849              
1850 0         0 my $exten_t;
1851             {
1852 0         0 local $_ = {
  0         0  
1853             'summarize' => $inner_r,
1854             'per' => $topic->_export_nfmt_tuple( $group_per_t ),
1855             };
1856 0         0 $exten_t = $summ_func->();
1857             }
1858 0         0 $topic->_assert_valid_tuple_result_of_func_arg( 'summary',
1859             '$summ_func', '$summ_attr_names', $exten_t, $exten_h );
1860 0         0 $exten_t = $topic->_import_nfmt_tuple( $exten_t );
1861              
1862 0         0 my $result_t = {%{$group_per_t}, %{$exten_t}};
  0         0  
  0         0  
1863 0         0 $result_b->{refaddr $result_t} = $result_t;
1864             }
1865              
1866 0         0 return $result;
1867             }
1868              
1869             ###########################################################################
1870              
1871             sub cardinality_per_group {
1872 0     0 0 0 my ($topic, $count_attr_name, $group_per, $allow_dup_tuples) = @_;
1873              
1874 0         0 $topic->_assert_valid_atnm_arg(
1875             'cardinality_per_group', '$count_attr_name', $count_attr_name );
1876 0         0 (my $group_per_h, $group_per)
1877             = $topic->_atnms_hr_from_assert_valid_atnms_arg(
1878             'cardinality_per_group', '$group_per', $group_per );
1879              
1880 0         0 my $topic_h = $topic->_heading();
1881              
1882             confess q{cardinality_per_group(): Bad $group_per arg;}
1883             . q{ that attr list isn't a subset of the invocant's heading.}
1884 0 0   0   0 if notall { exists $topic_h->{$_} } @{$group_per};
  0         0  
  0         0  
1885              
1886             confess q{cardinality_per_group(): Bad $count_attr_name arg;}
1887             . q{ that name for a new attr to add to the invocant}
1888             . q{ duplicates an attr of the invocant not being grouped.}
1889 0 0       0 if exists $group_per_h->{$count_attr_name};
1890              
1891 0         0 my $inner = [grep { !$group_per_h->{$_} } CORE::keys %{$topic_h}];
  0         0  
  0         0  
1892 0         0 my $inner_h = {CORE::map { $_ => undef } @{$inner}};
  0         0  
  0         0  
1893              
1894 0         0 my (undef, $topic_attrs_no_gr, undef)
1895             = $topic->_ptn_conj_and_disj( $topic_h, $inner_h );
1896              
1897 0         0 my $result = $topic->_new();
1898              
1899 0         0 $result->_heading( {%{$group_per_h}, $count_attr_name => undef} );
  0         0  
1900              
1901 0 0       0 if ($topic->is_empty()) {
1902             # An empty $topic means an empty result.
1903 0         0 return $result;
1904             }
1905              
1906 0 0 0     0 if (!$allow_dup_tuples and !$topic->_is_known_dup_free()) {
1907 0         0 $topic->_dup_free_want_index_over_all_attrs();
1908             }
1909              
1910 0         0 my $result_b = $result->_body();
1911 0         0 my $topic_index = $topic->_want_index( $topic_attrs_no_gr );
1912 0         0 for my $matched_topic_b (values %{$topic_index}) {
  0         0  
1913 0         0 my $count = scalar CORE::keys %{$matched_topic_b};
  0         0  
1914 0         0 my $any_mtpt = (values %{$matched_topic_b})[0];
  0         0  
1915 0         0 my $group_per_t = {CORE::map { ($_ => $any_mtpt->{$_}) }
1916 0         0 @{$topic_attrs_no_gr}};
  0         0  
1917 0         0 my $result_t = {%{$group_per_t}, $count_attr_name => $count};
  0         0  
1918 0         0 $result_b->{refaddr $result_t} = $result_t;
1919             }
1920              
1921 0         0 return $result;
1922             }
1923              
1924             sub count_per_group {
1925 0     0 0 0 my ($self, @args) = @_;
1926 0         0 return $self->cardinality_per_group( @args );
1927             }
1928              
1929             ###########################################################################
1930              
1931             sub _atnms_hr_from_assert_valid_atnms_arg {
1932 2     2   9 my ($self, $rtn_nm, $arg_nm, $atnms) = @_;
1933              
1934 2 100 66     15 if (defined $atnms and !ref $atnms) {
1935 1         3 $atnms = [$atnms];
1936             }
1937 2 50       10 confess qq{$rtn_nm(): Bad $arg_nm arg;}
1938             . q{ it must be an array-ref or a defined non-ref.}
1939             if ref $atnms ne 'ARRAY';
1940             confess qq{$rtn_nm(): Bad $arg_nm arg;}
1941             . q{ it should be just be a list of attr names,}
1942             . q{ but at least one name is undefined or is a ref.}
1943 2 50   4   13 if notall { defined $_ and !ref $_ } @{$atnms};
  4 50       17  
  2         10  
1944             confess qq{$rtn_nm(): Bad $arg_nm arg;}
1945             . q{ it specifies a list of}
1946             . q{ attr names with at least one duplicated name.}
1947 2 50       10 if (uniqstr @{$atnms}) != @{$atnms};
  2         11  
  2         7  
1948              
1949 2         5 my $heading = {CORE::map { ($_ => undef) } @{$atnms}};
  4         10  
  2         5  
1950 2         9 return ($heading, $atnms);
1951             }
1952              
1953             sub _assert_valid_atnm_arg {
1954 5     5   14 my ($self, $rtn_nm, $arg_nm, $atnm) = @_;
1955 5 50 33     28 confess qq{$rtn_nm(): Bad $arg_nm arg;}
1956             . q{ it should be just be an attr name,}
1957             . q{ but it is undefined or is a ref.}
1958             if !defined $atnm or ref $atnm;
1959             }
1960              
1961             sub _assert_valid_nnint_arg {
1962 0     0   0 my ($self, $rtn_nm, $arg_nm, $atnm) = @_;
1963 0 0 0     0 confess qq{$rtn_nm(): Bad $arg_nm arg;}
      0        
1964             . q{ it should be just be a non-negative integer,}
1965             . q{ but it is undefined or is a ref or is some other scalar.}
1966             if !defined $atnm or ref $atnm or not $atnm =~ /^[0-9]+$/;
1967             }
1968              
1969             sub _assert_valid_func_arg {
1970 16     16   34 my ($self, $rtn_nm, $arg_nm, $func) = @_;
1971 16 50       48 confess qq{$rtn_nm(): Bad $arg_nm arg;}
1972             . q{ it must be a Perl subroutine reference.}
1973             if ref $func ne 'CODE';
1974             }
1975              
1976             sub _assert_valid_tuple_result_of_func_arg {
1977 0     0   0 my ($self, $rtn_nm, $arg_nm_func, $arg_nm_attrs, $result_t, $heading)
1978             = @_;
1979 0 0 0     0 confess qq{$rtn_nm(): Bad $arg_nm_func arg;}
1980             . q{ at least one result of executing that Perl subroutine}
1981             . q{ reference was not a hash-ref or it didn't have the same}
1982             . qq{ set of hkeys as specified by the $arg_nm_attrs arg.}
1983             if ref $result_t ne 'HASH'
1984             or !$self->_is_identical_hkeys( $heading, $result_t );
1985 0 0       0 confess qq{$rtn_nm(): Bad $arg_nm_func arg;}
1986             . q{ at least one result of executing that Perl subroutine}
1987             . q{ reference was a hash-ref, and there exist circular refs}
1988             . q{ between itself or its tuple-valued components.}
1989             if $self->_tuple_arg_has_circular_refs( $result_t );
1990             }
1991              
1992             sub _normalize_same_heading_relation_arg {
1993 0     0   0 my ($self, $rtn_nm, $arg_nm, $other) = @_;
1994 0 0 0     0 if (blessed $other and $other->can( 'does' )
      0        
      0        
1995             and $other->does( 'Set::Relation' )
1996             and !$other->isa( __PACKAGE__ )) {
1997 0         0 $other = $self->_new( $other );
1998             }
1999 0 0 0     0 confess qq{$rtn_nm(): Bad $arg_nm arg; it isn't a Set::Relation}
      0        
2000             . q{ object, or it doesn't have exactly the}
2001             . q{ same set of attr names as the invocant.}
2002             if !blessed $other or !$other->isa( __PACKAGE__ )
2003             or !$self->_is_identical_hkeys(
2004             $self->_heading(), $other->_heading() );
2005 0         0 return $other;
2006             }
2007              
2008             sub _normalize_relation_arg {
2009 22     22   48 my ($self, $rtn_nm, $arg_nm, $other) = @_;
2010 22 50 33     173 if (blessed $other and $other->can( 'does' )
      33        
      33        
2011             and $other->does( 'Set::Relation' )
2012             and !$other->isa( __PACKAGE__ )) {
2013 0         0 $other = $self->_new( $other );
2014             }
2015 22 50 33     124 confess qq{$rtn_nm(): Bad $arg_nm arg;}
2016             . q{ it isn't a Set::Relation object.}
2017             if !blessed $other or !$other->isa( __PACKAGE__ );
2018 22         47 return $other;
2019             }
2020              
2021             ###########################################################################
2022              
2023             sub is_identical {
2024 22     22 0 895 my ($topic, $other) = @_;
2025 22         51 $other = $topic->_normalize_relation_arg(
2026             'is_identical', '$other', $other );
2027 22         66 return $topic->_is_identical( $other );
2028             }
2029              
2030             sub _is_identical {
2031 22     22   43 my ($topic, $other) = @_;
2032 22   66     45 return ($topic->degree() == $other->degree()
2033             and $topic->_is_identical_hkeys(
2034             $topic->_heading(), $other->_heading() )
2035             and $topic->cardinality() == $other->cardinality()
2036             and $topic->_is_identical_hkeys(
2037             $topic->_dup_free_want_index_over_all_attrs(),
2038             $other->_dup_free_want_index_over_all_attrs() ));
2039             }
2040              
2041             ###########################################################################
2042              
2043             sub is_subset {
2044 0     0 0 0 my ($topic, $other) = @_;
2045 0         0 $other = $topic->_normalize_same_heading_relation_arg(
2046             'is_subset', '$other', $other );
2047 0         0 my $other_i = $other->_dup_free_want_index_over_all_attrs();
2048 0     0   0 return all { exists $other_i->{$_} }
2049 0         0 CORE::keys %{$topic->_dup_free_want_index_over_all_attrs()};
  0         0  
2050             }
2051              
2052             sub is_superset {
2053 0     0 0 0 my ($topic, $other) = @_;
2054 0         0 $other = $topic->_normalize_same_heading_relation_arg(
2055             'is_superset', '$other', $other );
2056 0         0 my $topic_i = $topic->_dup_free_want_index_over_all_attrs();
2057 0     0   0 return all { exists $topic_i->{$_} }
2058 0         0 CORE::keys %{$other->_dup_free_want_index_over_all_attrs()};
  0         0  
2059             }
2060              
2061             sub is_proper_subset {
2062 0     0 0 0 my ($topic, $other) = @_;
2063 0         0 $other = $topic->_normalize_same_heading_relation_arg(
2064             'is_proper_subset', '$other', $other );
2065 0         0 my $other_i = $other->_dup_free_want_index_over_all_attrs();
2066             return ($topic->cardinality() < $other->cardinality()
2067 0     0   0 and all { exists $other_i->{$_} }
2068 0   0     0 CORE::keys %{$topic->_dup_free_want_index_over_all_attrs()});
2069             }
2070              
2071             sub is_proper_superset {
2072 0     0 0 0 my ($topic, $other) = @_;
2073 0         0 $other = $topic->_normalize_same_heading_relation_arg(
2074             'is_proper_superset', '$other', $other );
2075 0         0 my $topic_i = $topic->_dup_free_want_index_over_all_attrs();
2076             return ($other->cardinality() < $topic->cardinality()
2077 0     0   0 and all { exists $topic_i->{$_} }
2078 0   0     0 CORE::keys %{$other->_dup_free_want_index_over_all_attrs()});
2079             }
2080              
2081             sub is_disjoint {
2082 0     0 0 0 my ($topic, $other) = @_;
2083 0         0 $other = $topic->_normalize_same_heading_relation_arg(
2084             'is_disjoint', '$other', $other );
2085 0         0 return $topic->_intersection( [$other] )->is_empty();
2086             }
2087              
2088             ###########################################################################
2089              
2090             sub union {
2091 2     2 0 10 my ($topic, $others) = @_;
2092 2         9 $others = $topic->_normalize_same_heading_relations_arg(
2093             'union', '$others', $others );
2094 2         7 return $topic->_union( $others );
2095             }
2096              
2097             sub _union {
2098 2     2   5 my ($topic, $others) = @_;
2099              
2100             my $inputs = [
2101 4         9 grep { !$_->is_empty() } # filter out identity value instances
2102 2         4 $topic, @{$others}];
  2         3  
2103              
2104 2 50       3 if (@{$inputs} == 0) {
  2         6  
2105             # All inputs were the identity value; so is result.
2106 0         0 return $topic->empty();
2107             }
2108 2 50       5 if (@{$inputs} == 1) {
  2         4  
2109             # Only one non-identity value input; so it is the result.
2110 0         0 return $inputs->[0];
2111             }
2112              
2113             # If we get here, there are at least 2 non-empty input relations.
2114              
2115 2         5 my $result = $topic->empty();
2116              
2117 2         4 $result->_body( {CORE::map { %{$_->_body()} } @{$inputs}} );
  4         7  
  4         6  
  2         4  
2118              
2119 2         8 return $result;
2120             }
2121              
2122             ###########################################################################
2123              
2124             sub exclusion {
2125             # Also known as symmetric_diff().
2126 0     0 0 0 my ($topic, $others) = @_;
2127              
2128 0         0 $others = $topic->_normalize_same_heading_relations_arg(
2129             'exclusion', '$others', $others );
2130              
2131             my $inputs = [
2132 0         0 sort { $b->cardinality() <=> $a->cardinality() }
2133 0         0 grep { !$_->is_empty() } # filter out identity value instances
2134 0         0 $topic, @{$others}];
  0         0  
2135              
2136 0 0       0 if (@{$inputs} == 0) {
  0         0  
2137             # All inputs were the identity value; so is result.
2138 0         0 return $topic->empty();
2139             }
2140 0 0       0 if (@{$inputs} == 1) {
  0         0  
2141             # Only one non-identity value input; so it is the result.
2142 0         0 return $inputs->[0];
2143             }
2144              
2145             # If we get here, there are at least 2 non-empty input relations.
2146              
2147 0         0 my $largest = shift @{$inputs};
  0         0  
2148 0         0 my $largest_i = $largest->_dup_free_want_index_over_all_attrs();
2149              
2150             my $t_by_ident_str = {CORE::map {
2151 0         0 ($_ => (values %{$largest_i->{$_}})[0])
  0         0  
2152 0         0 } CORE::keys %{$largest_i}};
  0         0  
2153              
2154 0         0 for my $input (@{$inputs}) {
  0         0  
2155 0         0 my $input_i = $input->_dup_free_want_index_over_all_attrs();
2156 0         0 for my $tuple_ident_str (CORE::keys %{$input_i}) {
  0         0  
2157 0 0       0 if (exists $t_by_ident_str->{$tuple_ident_str}) {
2158 0         0 delete $t_by_ident_str->{$tuple_ident_str};
2159             }
2160             else {
2161             $t_by_ident_str->{$tuple_ident_str}
2162 0         0 = (values %{$input_i->{$tuple_ident_str}})[0];
  0         0  
2163             }
2164             }
2165             }
2166              
2167 0         0 my $result = $topic->empty();
2168              
2169             $result->_body(
2170 0         0 {CORE::map { (refaddr $_ => $_) } values %{$t_by_ident_str}} );
  0         0  
  0         0  
2171              
2172 0         0 $result->_is_known_dup_free( 1 );
2173              
2174 0         0 return $result;
2175             }
2176              
2177             sub symmetric_diff {
2178 0     0 0 0 my ($self, @args) = @_;
2179 0         0 return $self->exclusion( @args );
2180             }
2181              
2182             ###########################################################################
2183              
2184             sub intersection {
2185 1     1 0 4 my ($topic, $others) = @_;
2186 1         4 $others = $topic->_normalize_same_heading_relations_arg(
2187             'intersection', '$others', $others );
2188 1         5 return $topic->_intersection( $others );
2189             }
2190              
2191             sub _intersection {
2192 1     1   3 my ($topic, $others) = @_;
2193              
2194 1 50       2 if (@{$others} == 0) {
  1         5  
2195 0         0 return $topic;
2196             }
2197              
2198             my $inputs = [
2199 1         4 sort { $a->cardinality( 1 ) <=> $b->cardinality( 1 ) }
2200 1         2 $topic, @{$others}];
  1         6  
2201              
2202 1         2 my $smallest = shift @{$inputs};
  1         3  
2203              
2204 1 50       3 if ($smallest->is_empty()) {
2205 0         0 return $smallest;
2206             }
2207              
2208             # If we get here, there are at least 2 non-empty input relations.
2209              
2210 1         3 my $result = $topic->empty();
2211              
2212 1         3 my $smallest_i = $smallest->_dup_free_want_index_over_all_attrs();
2213             my $larger_is = [CORE::map {
2214 1         3 $_->_dup_free_want_index_over_all_attrs()
2215 1         3 } @{$inputs}];
  1         3  
2216 1         7 my $result_b = $result->_body();
2217              
2218             TUPLE:
2219 1         2 for my $tuple_ident_str (CORE::keys %{$smallest_i}) {
  1         5  
2220 3         6 for my $larger_i (@{$larger_is}) {
  3         6  
2221             next TUPLE
2222 3 100       10 if !exists $larger_i->{$tuple_ident_str};
2223             }
2224 2         3 my ($tuple_refaddr, $tuple) = %{$smallest_i->{$tuple_ident_str}};
  2         6  
2225 2         6 $result_b->{$tuple_refaddr} = $tuple;
2226             }
2227              
2228 1         4 $result->_is_known_dup_free( 1 );
2229              
2230 1         5 return $result;
2231             }
2232              
2233             ###########################################################################
2234              
2235             sub _normalize_same_heading_relations_arg {
2236 3     3   8 my ($self, $rtn_nm, $arg_nm, $others) = @_;
2237              
2238 3         6 my $self_h = $self->_heading();
2239              
2240 3 50 33     29 if (blessed $others and $others->can( 'does' )
      33        
2241             and $others->does( 'Set::Relation' )) {
2242 3         6 $others = [$others];
2243             }
2244 3 50       11 confess qq{$rtn_nm(): Bad $arg_nm arg;}
2245             . q{ it must be an array-ref or a Set::Relation object.}
2246             if ref $others ne 'ARRAY';
2247             $others = [CORE::map {
2248 3         4 my $other = $_;
2249 3 50 33     20 if (blessed $other and $other->can( 'does' )
      33        
      33        
2250             and $other->does( 'Set::Relation' )
2251             and !$other->isa( __PACKAGE__ )) {
2252 0         0 $other = $self->_new( $other );
2253             }
2254 3 50 33     38 confess qq{$rtn_nm(): Bad $arg_nm arg elem;}
      33        
2255             . q{ it isn't a Set::Relation object, or it doesn't have}
2256             . q{ exactly the same set of attr names as the invocant.}
2257             if !blessed $other or !$other->isa( __PACKAGE__ )
2258             or !$self->_is_identical_hkeys(
2259             $self_h, $other->_heading() );
2260 3         12 $other;
2261 3         5 } @{$others}];
  3         7  
2262              
2263 3         8 return $others;
2264             }
2265              
2266             sub _normalize_relations_arg {
2267 2     2   9 my ($self, $rtn_nm, $arg_nm, $others) = @_;
2268              
2269 2 50 33     30 if (blessed $others and $others->can( 'does' )
      33        
2270             and $others->does( 'Set::Relation' )) {
2271 2         27 $others = [$others];
2272             }
2273 2 50       12 confess qq{$rtn_nm(): Bad $arg_nm arg;}
2274             . q{ it must be an array-ref or a Set::Relation object.}
2275             if ref $others ne 'ARRAY';
2276             $others = [CORE::map {
2277 2         4 my $other = $_;
2278 2 50 33     70 if (blessed $other and $other->can( 'does' )
      33        
      33        
2279             and $other->does( 'Set::Relation' )
2280             and !$other->isa( __PACKAGE__ )) {
2281 0         0 $other = $self->_new( $other );
2282             }
2283 2 50 33     20 confess qq{$rtn_nm(): Bad $arg_nm arg elem;}
2284             . q{ it isn't a Set::Relation object.}
2285             if !blessed $other or !$other->isa( __PACKAGE__ );
2286 2         11 $other;
2287 2         6 } @{$others}];
  2         5  
2288              
2289 2         6 return $others;
2290             }
2291              
2292             ###########################################################################
2293              
2294             sub diff {
2295 0     0 0 0 my ($source, $filter) = @_;
2296 0         0 $filter = $source->_normalize_same_heading_relation_arg(
2297             'diff', '$other', $filter );
2298 0         0 return $source->_diff( $filter );
2299             }
2300              
2301             sub _diff {
2302 0     0   0 my ($source, $filter) = @_;
2303 0 0 0     0 if ($source->is_empty() or $filter->is_empty()) {
2304 0         0 return $source;
2305             }
2306 0         0 return $source->_regular_diff( $filter );
2307             }
2308              
2309             sub _regular_diff {
2310 0     0   0 my ($source, $filter) = @_;
2311              
2312 0         0 my $result = $source->empty();
2313              
2314 0         0 my $source_i = $source->_dup_free_want_index_over_all_attrs();
2315 0         0 my $filter_i = $filter->_dup_free_want_index_over_all_attrs();
2316 0         0 my $result_b = $result->_body();
2317              
2318 0         0 for my $tuple_ident_str (CORE::keys %{$source_i}) {
  0         0  
2319 0 0       0 if (!exists $filter_i->{$tuple_ident_str}) {
2320 0         0 my ($tuple_refaddr, $tuple) = %{$source_i->{$tuple_ident_str}};
  0         0  
2321 0         0 $result_b->{$tuple_refaddr} = $tuple;
2322             }
2323             }
2324              
2325 0         0 $result->_is_known_dup_free( 1 );
2326              
2327 0         0 return $result;
2328             }
2329              
2330             ###########################################################################
2331              
2332             sub semidiff {
2333 0     0 0 0 my ($source, $filter) = @_;
2334 0         0 $filter = $source->_normalize_relation_arg(
2335             'semidiff', '$filter', $filter );
2336 0 0 0     0 if ($source->is_empty() or $filter->is_empty()) {
2337 0         0 return $source;
2338             }
2339 0         0 return $source->_regular_diff( $source->_semijoin( $filter ) );
2340             }
2341              
2342             sub antijoin {
2343 0     0 0 0 my ($self, @args) = @_;
2344 0         0 return $self->semidiff( @args );
2345             }
2346              
2347             sub semijoin_and_diff {
2348 0     0 0 0 my ($source, $filter) = @_;
2349 0         0 $filter = $source->_normalize_relation_arg(
2350             'semijoin_and_diff', '$filter', $filter );
2351 0         0 return $source->_semijoin_and_diff( $filter );
2352             }
2353              
2354             sub _semijoin_and_diff {
2355 0     0   0 my ($source, $filter) = @_;
2356 0 0       0 if ($source->is_empty()) {
2357 0         0 return [$source, $source];
2358             }
2359 0 0       0 if ($filter->is_empty()) {
2360 0         0 return [$source->empty(), $source];
2361             }
2362 0         0 my $semijoin = $source->_semijoin( $filter );
2363 0         0 return [$semijoin, $source->_regular_diff( $semijoin )];
2364             }
2365              
2366             sub semijoin {
2367 0     0 0 0 my ($source, $filter) = @_;
2368 0         0 $filter = $source->_normalize_relation_arg(
2369             'semijoin', '$filter', $filter );
2370 0         0 return $source->_semijoin( $filter );
2371             }
2372              
2373             sub _semijoin {
2374 0     0   0 my ($source, $filter) = @_;
2375              
2376 0 0       0 if ($source->is_empty()) {
2377 0         0 return $source;
2378             }
2379 0 0       0 if ($filter->is_empty()) {
2380 0         0 return $source->empty();
2381             }
2382              
2383             # If we get here, both inputs have at least one tuple.
2384              
2385 0 0 0     0 if ($source->is_nullary() or $filter->is_nullary()) {
2386 0         0 return $source;
2387             }
2388              
2389             # If we get here, both inputs also have at least one attribute.
2390              
2391 0         0 my ($both, $source_only, $filter_only) = $source->_ptn_conj_and_disj(
2392             $source->_heading(), $filter->_heading() );
2393              
2394 0 0       0 if (@{$both} == 0) {
  0         0  
2395             # The inputs have disjoint headings; result is source.
2396 0         0 return $source;
2397             }
2398 0 0 0     0 if (@{$source_only} == 0 and @{$filter_only} == 0) {
  0         0  
  0         0  
2399             # The inputs have identical headings; result is intersection.
2400 0         0 return $source->_intersection( [$filter] );
2401             }
2402              
2403             # If we get here, the inputs also have overlapping non-ident headings.
2404              
2405 0         0 return $source->_regular_semijoin( $filter, $both );
2406             }
2407              
2408             sub _regular_semijoin {
2409 0     0   0 my ($source, $filter, $both) = @_;
2410              
2411 0         0 my $result = $source->empty();
2412              
2413 0 0       0 my ($sm, $lg) = ($source->cardinality( 1 ) < $filter->cardinality( 1 ))
2414             ? ($source, $filter) : ($filter, $source);
2415              
2416 0         0 my $sm_index = $sm->_want_index( $both );
2417 0         0 my $lg_index = $lg->_want_index( $both );
2418 0         0 my $source_index = $source->_want_index( $both );
2419 0         0 my $result_b = $result->_body();
2420              
2421 0         0 for my $subtuple_ident_str (CORE::keys %{$sm_index}) {
  0         0  
2422 0 0       0 if (exists $lg_index->{$subtuple_ident_str}) {
2423 0         0 my $matched_source_b = $source_index->{$subtuple_ident_str};
2424 0         0 for my $tuple_refaddr (CORE::keys %{$matched_source_b}) {
  0         0  
2425             $result_b->{$tuple_refaddr}
2426 0         0 = $matched_source_b->{$tuple_refaddr};
2427             }
2428             }
2429             }
2430              
2431 0 0 0     0 if ($source->_is_known_dup_free() and $filter->_is_known_dup_free()) {
2432 0         0 $result->_is_known_dup_free( 1 );
2433             }
2434              
2435 0         0 return $result;
2436             }
2437              
2438             ###########################################################################
2439              
2440             sub join {
2441 2     2 0 6497 my ($topic, $others) = @_;
2442 2         11 $others = $topic->_normalize_relations_arg(
2443             'join', '$others', $others );
2444 2         10 return $topic->_join( $others );
2445             }
2446              
2447             sub _join {
2448 2     2   5 my ($topic, $others) = @_;
2449              
2450 2 50       3 if (@{$others} == 0) {
  2         8  
2451 0         0 return $topic;
2452             }
2453              
2454 2         5 my $inputs = [$topic, @{$others}];
  2         4  
2455              
2456 2 50   4   10 if (any { $_->is_empty() } @{$inputs}) {
  4         13  
  2         7  
2457             # At least one input has zero tuples; so does result.
2458 0         0 my $result = $topic->_new();
2459 0         0 my $result_h = {CORE::map { %{$_->_heading()} } @{$inputs}};
  0         0  
  0         0  
  0         0  
2460 0         0 $result->_heading( $result_h );
2461 0         0 return $result;
2462             }
2463              
2464             # If we get here, all inputs have at least one tuple.
2465              
2466             $inputs = [
2467 2         15 sort { $a->cardinality( 1 ) <=> $b->cardinality( 1 ) }
2468 4         12 grep { !$_->is_nullary() } # filter out identity value instances
2469 2         10 @{$inputs}];
  2         7  
2470              
2471 2 50       7 if (@{$inputs} == 0) {
  2         15  
2472             # All inputs were the identity value; so is result.
2473 0         0 return $topic;
2474             }
2475 2 50       6 if (@{$inputs} == 1) {
  2         9  
2476             # Only one non-identity value input; so it is the result.
2477 0         0 return $inputs->[0];
2478             }
2479              
2480             # If we get here, there are at least 2 non-empty non-nullary inp rels.
2481              
2482 2         4 my $result = shift @{$inputs};
  2         4  
2483             INPUT:
2484 2         5 for my $input (@{$inputs}) {
  2         6  
2485             # TODO: Optimize this better by determining more strategic order
2486             # to join the various inputs, such as by doing intersections first,
2487             # then semijoins, then regular joins, then cross-products.
2488             # But at least we're going min to max cardinality meanwhile.
2489              
2490 2         7 my ($both, $result_only, $input_only)
2491             = $result->_ptn_conj_and_disj(
2492             $result->_heading(), $input->_heading() );
2493              
2494 2 50       4 if (@{$both} == 0) {
  2         8  
2495             # The inputs have disjoint headings; result is cross-product.
2496 0         0 $result = $result->_regular_product( $input );
2497 0         0 next INPUT;
2498             }
2499 2 50 33     5 if (@{$result_only} == 0 and @{$input_only} == 0) {
  2         36  
  0         0  
2500             # The inputs have identical headings; result is intersection.
2501 0         0 $result = $result->_intersection( [$input] );
2502 0         0 next INPUT;
2503             }
2504              
2505             # If we get here, the inputs also have overlapping non-ident heads.
2506              
2507 2 50       5 if (@{$result_only} == 0) {
  2         9  
2508             # The first input's attrs are a proper subset of the second's;
2509             # result has same heading as second, a subset of sec's tuples.
2510 0         0 $result = $input->_regular_semijoin( $result, $both );
2511 0         0 next INPUT;
2512             }
2513 2 50       4 if (@{$input_only} == 0) {
  2         8  
2514             # The second input's attrs are a proper subset of the first's;
2515             # result has same heading as first, a subset of first's tuples.
2516 0         0 $result = $result->_regular_semijoin( $input, $both );
2517 0         0 next INPUT;
2518             }
2519              
2520             # If we get here, both inputs also have mini one attr of their own.
2521              
2522 2         10 $result = $result->_regular_join(
2523             $input, $both, $result_only, $input_only );
2524             }
2525 2         9 return $result;
2526             }
2527              
2528             sub _regular_join {
2529 2     2   7 my ($topic, $other, $both, $topic_only, $other_only) = @_;
2530              
2531 2         7 my $result = $topic->_new();
2532              
2533 9         24 $result->_heading( {CORE::map { ($_ => undef) }
2534 2         6 @{$both}, @{$topic_only}, @{$other_only}} );
  2         4  
  2         15  
  2         6  
2535              
2536 2 50       11 my ($sm, $lg) = ($topic->cardinality( 1 ) < $other->cardinality( 1 ))
2537             ? ($topic, $other) : ($other, $topic);
2538              
2539 2         9 my $sm_index = $sm->_want_index( $both );
2540 2         7 my $lg_index = $lg->_want_index( $both );
2541 2         6 my $result_b = {};
2542              
2543 2         5 for my $subtuple_ident_str (CORE::keys %{$sm_index}) {
  2         7  
2544 7 100       19 if (exists $lg_index->{$subtuple_ident_str}) {
2545 5         8 my $matched_sm_b = $sm_index->{$subtuple_ident_str};
2546 5         8 my $matched_lg_b = $lg_index->{$subtuple_ident_str};
2547 5         9 for my $sm_t (values %{$matched_sm_b}) {
  5         12  
2548 5         9 for my $lg_t (values %{$matched_lg_b}) {
  5         11  
2549 14         22 my $result_t = {%{$sm_t}, %{$lg_t}};
  14         25  
  14         56  
2550 14         79 $result_b->{refaddr $result_t} = $result_t;
2551             }
2552             }
2553             }
2554             }
2555 2         16 $result->_body( $result_b );
2556              
2557 2 50 33     6 if ($topic->_is_known_dup_free() and $other->_is_known_dup_free()) {
2558 2         8 $result->_is_known_dup_free( 1 );
2559             }
2560              
2561 2         10 return $result;
2562             }
2563              
2564             ###########################################################################
2565              
2566             sub product {
2567 0     0 0 0 my ($topic, $others) = @_;
2568              
2569 0         0 $others = $topic->_normalize_relations_arg(
2570             'product', '$others', $others );
2571              
2572 0 0       0 if (@{$others} == 0) {
  0         0  
2573 0         0 return $topic;
2574             }
2575              
2576 0         0 my $inputs = [$topic, @{$others}];
  0         0  
2577              
2578             my $attr_names
2579 0         0 = [CORE::map { CORE::keys %{$_->_heading()} } @{$inputs}];
  0         0  
  0         0  
  0         0  
2580              
2581             confess q{product(): Bad $others arg;}
2582             . q{ one of its elems has an attr name duplicated by}
2583             . q{ either the invocant or another $others elem.}
2584 0 0       0 if (uniqstr @{$attr_names}) != @{$attr_names};
  0         0  
  0         0  
2585              
2586 0 0   0   0 if (any { $_->is_empty() } @{$inputs}) {
  0         0  
  0         0  
2587             # At least one input has zero tuples; so does result.
2588 0         0 my $result = $topic->_new();
2589 0         0 $result->_heading( {CORE::map { ($_ => undef) } @{$attr_names}} );
  0         0  
  0         0  
2590 0         0 return $result;
2591             }
2592              
2593             # If we get here, all inputs have at least one tuple.
2594              
2595             $inputs = [
2596 0         0 sort { $a->cardinality( 1 ) <=> $b->cardinality( 1 ) }
2597 0         0 grep { !$_->is_nullary() } # filter out identity value instances
2598 0         0 @{$inputs}];
  0         0  
2599              
2600 0 0       0 if (@{$inputs} == 0) {
  0         0  
2601             # All inputs were the identity value; so is result.
2602 0         0 return $topic;
2603             }
2604 0 0       0 if (@{$inputs} == 1) {
  0         0  
2605             # Only one non-identity value input; so it is the result.
2606 0         0 return $inputs->[0];
2607             }
2608              
2609             # If we get here, there are at least 2 non-empty non-nullary inp rels.
2610              
2611 0         0 my $result = shift @{$inputs};
  0         0  
2612 0         0 for my $input (@{$inputs}) {
  0         0  
2613 0         0 $result = $result->_regular_product( $input );
2614             }
2615 0         0 return $result;
2616             }
2617              
2618             sub _regular_product {
2619 0     0   0 my ($topic, $other) = @_;
2620              
2621 0         0 my $result = $topic->_new();
2622              
2623 0         0 $result->_heading( {%{$topic->_heading()}, %{$other->_heading()}} );
  0         0  
  0         0  
2624              
2625 0 0       0 my ($sm, $lg) = ($topic->cardinality( 1 ) < $other->cardinality( 1 ))
2626             ? ($topic, $other) : ($other, $topic);
2627              
2628 0         0 my $sm_b = $sm->_body();
2629 0         0 my $lg_b = $lg->_body();
2630 0         0 my $result_b = {};
2631              
2632 0         0 for my $sm_t (values %{$sm_b}) {
  0         0  
2633 0         0 for my $lg_t (values %{$lg_b}) {
  0         0  
2634 0         0 my $result_t = {%{$sm_t}, %{$lg_t}};
  0         0  
  0         0  
2635 0         0 $result_b->{refaddr $result_t} = $result_t;
2636             }
2637             }
2638 0         0 $result->_body( $result_b );
2639              
2640 0 0 0     0 if ($topic->_is_known_dup_free() and $other->_is_known_dup_free()) {
2641 0         0 $result->_is_known_dup_free( 1 );
2642             }
2643              
2644 0         0 return $result;
2645             }
2646              
2647             ###########################################################################
2648              
2649             sub quotient {
2650 0     0 0 0 my ($dividend, $divisor) = @_;
2651              
2652 0         0 $divisor = $dividend->_normalize_relation_arg(
2653             'quotient', '$divisor', $divisor );
2654              
2655 0         0 my (undef, $dividend_only, $divisor_only)
2656             = $dividend->_ptn_conj_and_disj(
2657             $dividend->_heading(), $divisor->_heading() );
2658              
2659             confess q{quotient(): Bad $divisor arg;}
2660             . q{ its heading isn't a subset of the invocant's heading.}
2661 0 0       0 if @{$divisor_only} > 0;
  0         0  
2662              
2663 0         0 my $proj_of_dividend_only = $dividend->_projection( $dividend_only );
2664              
2665 0 0 0     0 if ($dividend->is_empty() or $divisor->is_empty()) {
2666             # At least one input has zero tup; res has all tup o dividend proj.
2667 0         0 return $proj_of_dividend_only;
2668             }
2669              
2670             # If we get here, both inputs have at least one tuple.
2671              
2672 0 0 0     0 if ($dividend->is_nullary() or $divisor->is_nullary()) {
2673             # Both inputs or just divisor is ident-one rel; result is dividend.
2674 0         0 return $dividend;
2675             }
2676              
2677             # If we get here, divisor has at least one attribute,
2678             # and divisor heading is proper subset of dividend heading.
2679              
2680 0         0 return $proj_of_dividend_only
2681             ->_diff( $proj_of_dividend_only
2682             ->_regular_product( $divisor )
2683             ->_diff( $dividend )
2684             ->_projection( $dividend_only )
2685             );
2686             }
2687              
2688             ###########################################################################
2689              
2690             sub composition {
2691 0     0 0 0 my ($topic, $other) = @_;
2692              
2693 0         0 $other = $topic->_normalize_relation_arg(
2694             'composition', '$other', $other );
2695              
2696 0         0 my ($both, $topic_only, $other_only) = $topic->_ptn_conj_and_disj(
2697             $topic->_heading(), $other->_heading() );
2698              
2699 0 0 0     0 if ($topic->is_empty() or $other->is_empty()) {
2700             # At least one input has zero tuples; so does result.
2701 0         0 return $topic->_new( [@{$topic_only}, @{$other_only}] );
  0         0  
  0         0  
2702             }
2703              
2704             # If we get here, both inputs have at least one tuple.
2705              
2706 0 0       0 if ($topic->is_nullary()) {
2707             # First input is identity-one relation; result is second input.
2708 0         0 return $other;
2709             }
2710 0 0       0 if ($other->is_nullary()) {
2711             # Second input is identity-one relation; result is first input.
2712 0         0 return $topic;
2713             }
2714              
2715             # If we get here, both inputs also have at least one attribute.
2716              
2717 0 0       0 if (@{$both} == 0) {
  0         0  
2718             # The inputs have disjoint headings; result is cross-product.
2719 0         0 return $topic->_regular_product( $other );
2720             }
2721 0 0 0     0 if (@{$topic_only} == 0 and @{$other_only} == 0) {
  0         0  
  0         0  
2722             # The inputs have identical headings; result is ident-one relation.
2723 0         0 return $topic->_new( [ {} ] );
2724             }
2725              
2726             # If we get here, the inputs also have overlapping non-ident headings.
2727              
2728 0 0       0 if (@{$topic_only} == 0) {
  0         0  
2729             # The first input's attributes are a proper subset of the second's;
2730             # result has same heading as second, a subset of second's tuples.
2731 0         0 return $other->_regular_semijoin( $topic, $both )
2732             ->_projection( $other_only );
2733             }
2734 0 0       0 if (@{$other_only} == 0) {
  0         0  
2735             # The second input's attributes are a proper subset of the first's;
2736             # result has same heading as first, a subset of first's tuples.
2737 0         0 return $topic->_regular_semijoin( $other, $both )
2738             ->_projection( $topic_only );
2739             }
2740              
2741             # If we get here, both inputs also have at least one attr of their own.
2742              
2743             return $topic->_regular_join(
2744             $other, $both, $topic_only, $other_only )
2745 0         0 ->_projection( [@{$topic_only}, @{$other_only}] );
  0         0  
  0         0  
2746             }
2747              
2748             ###########################################################################
2749              
2750             sub _ptn_conj_and_disj {
2751             # inputs are hashes, results are arrays
2752 6     6   16 my ($self, $src1, $src2) = @_;
2753 6         9 my $both = [grep { exists $src1->{$_} } CORE::keys %{$src2}];
  17         107  
  6         96  
2754 6         16 my $both_h = {CORE::map { ($_ => undef) } @{$both}};
  10         27  
  6         14  
2755 6         15 my $only1 = [grep { !exists $both_h->{$_} } CORE::keys %{$src1}];
  18         37  
  6         15  
2756 6         23 my $only2 = [grep { !exists $both_h->{$_} } CORE::keys %{$src2}];
  17         35  
  6         15  
2757 6         24 return ($both, $only1, $only2);
2758             }
2759              
2760             ###########################################################################
2761              
2762             sub _want_index {
2763 5     5   11 my ($self, $atnms) = @_;
2764 5 50       10 if (@{$atnms} == $self->degree()) {
  5         14  
2765             # Special case; index is over all attributes of the relation, so
2766             # create this index in a special way that includes dup elimination.
2767 0         0 return $self->_dup_free_want_index_over_all_attrs();
2768             }
2769 5         12 my $subheading = {CORE::map { ($_ => undef) } @{$atnms}};
  5         25  
  5         10  
2770 5         18 my $subheading_ident_str = $self->_heading_ident_str( $subheading );
2771 5         14 my $indexes = $self->_indexes();
2772 5 50       20 if (!exists $indexes->{$subheading_ident_str}) {
2773 5         18 my $index_and_meta = $indexes->{$subheading_ident_str}
2774             = [ $subheading, {} ];
2775 5         10 my $index = $index_and_meta->[1];
2776 5         11 my $body = $self->_body();
2777 5         12 for my $tuple_refaddr (CORE::keys %{$body}) {
  5         19  
2778 27         44 my $tuple = $body->{$tuple_refaddr};
2779             my $subtuple_ident_str = $self->_ident_str(
2780 27         39 {CORE::map { ($_ => $tuple->{$_}) } @{$atnms}} );
  27         108  
  27         39  
2781 27   100     120 my $matched_b = $index->{$subtuple_ident_str} ||= {};
2782 27         59 $matched_b->{$tuple_refaddr} = $tuple;
2783             }
2784             }
2785 5         15 return $indexes->{$subheading_ident_str}->[1];
2786             }
2787              
2788             ###########################################################################
2789              
2790             sub _dup_free_want_index_over_all_attrs {
2791 55     55   100 my ($self) = @_;
2792              
2793 55         93 my $heading = $self->_heading();
2794 55         102 my $indexes = $self->_indexes();
2795              
2796 55         125 my $heading_ident_str = $self->_heading_ident_str( $heading );
2797              
2798 55 100       140 if (!exists $indexes->{$heading_ident_str}) {
2799 28         50 my $extras_to_delete = {};
2800              
2801 28         58 my $body = $self->_body();
2802 28         89 my $index_and_meta = $indexes->{$heading_ident_str}
2803             = [ $heading, {} ];
2804 28         48 my $index = $index_and_meta->[1];
2805 28         42 for my $tuple_refaddr (CORE::keys %{$body}) {
  28         74  
2806 131         188 my $tuple = $body->{$tuple_refaddr};
2807 131         256 my $tuple_ident_str = $self->_ident_str( $tuple );
2808 131 100       287 if (exists $index->{$tuple_ident_str}) {
2809 4         8 delete $body->{$tuple_refaddr};
2810 4         7 $extras_to_delete->{$tuple_refaddr} = $tuple;
2811 4         8 next;
2812             }
2813 127         407 $index->{$tuple_ident_str} = {$tuple_refaddr => $tuple};
2814             }
2815              
2816 28 100       56 if ((CORE::keys %{$extras_to_delete}) > 0) {
  28         78  
2817 2         4 for my $subheading_ident_str (CORE::keys %{$indexes}) {
  2         5  
2818             next
2819 2 50       5 if $subheading_ident_str eq $heading_ident_str;
2820             my ($subheading, $index)
2821 0         0 = @{$indexes->{$subheading_ident_str}};
  0         0  
2822 0         0 for my $tuple_refaddr (CORE::keys %{$extras_to_delete}) {
  0         0  
2823 0         0 my $tuple = $extras_to_delete->{$tuple_refaddr};
2824             my $subtuple_ident_str = $self->_ident_str(
2825 0         0 {CORE::map { ($_ => $tuple->{$_}) }
2826 0         0 CORE::keys %{$subheading}} );
  0         0  
2827 0         0 delete $index->{$subtuple_ident_str}->{$tuple_refaddr};
2828             }
2829             }
2830             }
2831              
2832 28         68 $self->_is_known_dup_free( 1 );
2833             }
2834              
2835 55         128 return $indexes->{$heading_ident_str}->[1];
2836             }
2837              
2838             ###########################################################################
2839              
2840             sub join_with_group {
2841 0     0 0   my ($primary, $secondary, $group_attr) = @_;
2842              
2843 0           $secondary = $primary->_normalize_relation_arg(
2844             'join_with_group', '$secondary', $secondary );
2845 0           $primary->_assert_valid_atnm_arg(
2846             'join_with_group', '$group_attr', $group_attr );
2847              
2848 0           my $primary_h = $primary->_heading();
2849              
2850             confess q{join_with_group(): Bad $group_attr arg;}
2851             . q{ that name for a new attr to add}
2852             . q{ to $primary, consisting of grouped $secondary-only attrs,}
2853             . q{ duplicates an attr of $primary (not being grouped).}
2854 0 0         if exists $primary_h->{$group_attr};
2855              
2856             # TODO: inline+merge what join/group do for better performance.
2857              
2858 0           my ($both, $primary_only, $inner) = $primary->_ptn_conj_and_disj(
2859             $primary_h, $secondary->_heading() );
2860 0           my $inner_h = {CORE::map { $_ => undef } @{$inner}};
  0            
  0            
2861              
2862             return $primary
2863             ->_join( [$secondary] )
2864 0           ->_group( $group_attr, $inner, [CORE::keys %{$primary_h}],
  0            
2865             $inner_h );
2866             }
2867              
2868             ###########################################################################
2869              
2870             sub rank {
2871 0     0 0   my ($topic, $name, $ord_func) = @_;
2872              
2873 0           my $topic_h = $topic->_heading();
2874              
2875 0           $topic->_assert_valid_atnm_arg( 'rank', '$name', $name );
2876             confess q{rank(): Bad $name arg; that name for a new attr to add}
2877             . q{ to the invocant, consisting of each tuple's numeric rank,}
2878             . q{ duplicates an existing attr of the invocant.}
2879 0 0         if exists $topic_h->{$name};
2880              
2881 0           $topic->_assert_valid_func_arg( 'rank', '$ord_func', $ord_func );
2882              
2883 0           my $result = $topic->_new();
2884              
2885 0           $result->_heading( {%{$topic_h}, $name => undef} );
  0            
2886              
2887 0 0         if ($topic->is_empty()) {
2888 0           return $result;
2889             }
2890              
2891 0 0         if (!$topic->_is_known_dup_free()) {
2892 0           $topic->_dup_free_want_index_over_all_attrs();
2893             }
2894              
2895 0           my $ext_topic_tuples = [];
2896 0           my $topic_tuples_by_ext_tt_ref = {};
2897              
2898 0           for my $topic_t (values %{$topic->_body()}) {
  0            
2899 0           my $ext_topic_t = $topic->_export_nfmt_tuple( $topic_t );
2900 0           push @{$ext_topic_tuples}, $ext_topic_t;
  0            
2901 0           $topic_tuples_by_ext_tt_ref->{refaddr $ext_topic_t} = $topic_t;
2902             }
2903              
2904             my $sorted_ext_topic_tuples = [sort {
2905 0           local $_ = { 'a' => $a, 'b' => $b };
2906 0           $ord_func->();
2907 0           } @{$ext_topic_tuples}];
  0            
2908              
2909 0           my $result_b = $result->_body();
2910              
2911 0           my $rank = -1;
2912 0           for my $ext_topic_t (@{$sorted_ext_topic_tuples}) {
  0            
2913 0           my $topic_t = $topic_tuples_by_ext_tt_ref->{refaddr $ext_topic_t};
2914 0           $rank ++;
2915 0           my $result_t = {$name => $rank, %{$topic_t}};
  0            
2916 0           $result_b->{refaddr $result_t} = $result_t;
2917             }
2918              
2919 0           return $result;
2920             }
2921              
2922             ###########################################################################
2923              
2924             sub rank_by_attr_names {
2925 0     0 0   my ($topic, $name, $order_by) = @_;
2926              
2927 0           my $topic_h = $topic->_heading();
2928              
2929 0           $topic->_assert_valid_atnm_arg( 'rank_by_attr_names', '$name', $name );
2930             confess q{rank_by_attr_names(): Bad $name arg; that name for a new}
2931             . q{ attr to add to the invocant, consisting of each tuple's}
2932             . q{ numeric rank, duplicates an existing attr of th invocant.}
2933 0 0         if exists $topic_h->{$name};
2934              
2935 0           $order_by = $topic->_normalize_order_by_arg(
2936             'rank_by_attr_names', '$order_by', $order_by );
2937              
2938 0           my $result = $topic->_new();
2939              
2940 0           $result->_heading( {%{$topic_h}, $name => undef} );
  0            
2941              
2942 0 0         if ($topic->is_empty()) {
2943 0           return $result;
2944             }
2945              
2946 0           my $sort_func = $topic->_sort_func_from_order_by( $order_by );
2947              
2948 0 0         if (!$topic->_is_known_dup_free()) {
2949 0           $topic->_dup_free_want_index_over_all_attrs();
2950             }
2951              
2952 0           my $result_b = $result->_body();
2953              
2954 0           my $rank = -1;
2955 0           for my $topic_t (@{$sort_func->( $topic )}) {
  0            
2956 0           $rank ++;
2957 0           my $result_t = {$name => $rank, %{$topic_t}};
  0            
2958 0           $result_b->{refaddr $result_t} = $result_t;
2959             }
2960              
2961 0           return $result;
2962             }
2963              
2964             ###########################################################################
2965              
2966             sub _normalize_order_by_arg {
2967 0     0     my ($topic, $rtn_nm, $arg_nm, $order_by) = @_;
2968              
2969 0 0 0       if (defined $order_by and !ref $order_by) {
2970 0           $order_by = [$order_by];
2971             }
2972 0 0         confess qq{$rtn_nm(): Bad $arg_nm arg;}
2973             . q{ it must be an array-ref or a defined non-ref.}
2974             if ref $order_by ne 'ARRAY';
2975              
2976             $order_by = [CORE::map {
2977             (ref $_ ne 'ARRAY') ? [$_, 0, 'cmp']
2978 0           : (@{$_} == 1) ? [@{$_}, 0, 'cmp']
  0            
2979 0 0         : (@{$_} == 2) ? [@{$_}, 'cmp']
  0 0          
  0 0          
2980             : $_
2981 0           } @{$order_by}];
  0            
2982             confess qq{$rtn_nm(): Bad $arg_nm arg elem;}
2983             . q{ it must be a 1-3 elem array-ref or a defined non-ref,}
2984             . q{ its first elem must be a valid attr name (defin non-ref),}
2985             . q{ and its third elem must be undef or one of 'cmp'|'<=>'.}
2986             if notall {
2987 0 0 0 0     ref $_ eq 'ARRAY' and @{$_} == 3
  0   0        
      0        
      0        
      0        
2988             and defined $_->[0] and !ref $_->[0]
2989             and (!defined $_->[2]
2990             or $_->[2] eq 'cmp' or $_->[2] eq '<=>')
2991 0 0         } @{$order_by};
  0            
2992              
2993 0           my $atnms = [CORE::map { $_->[0] } @{$order_by}];
  0            
  0            
2994             confess qq{$rtn_nm(): Bad $arg_nm arg;}
2995             . q{ it specifies a list of}
2996             . q{ attr names with at least one duplicated name.}
2997 0 0         if (uniqstr @{$atnms}) != @{$atnms};
  0            
  0            
2998              
2999 0           my $topic_h = $topic->_heading();
3000             confess qq{$rtn_nm(): Bad $arg_nm arg;}
3001             . q{ the list of attr names it specifies isn't a subset of the}
3002             . q{ heading of the relation defined by the $members arg.}
3003 0 0   0     if notall { exists $topic_h->{$_} } @{$atnms};
  0            
  0            
3004              
3005 0           return $order_by;
3006             }
3007              
3008             ###########################################################################
3009              
3010             sub _sort_func_from_order_by {
3011 0     0     my ($topic, $order_by) = @_;
3012             my $sort_func_perl
3013             = "sub {\n"
3014             . "my (\$topic) = \@_;\n"
3015             . "return [sort {\n"
3016             . (CORE::join ' || ', '0', CORE::map {
3017 0           my ($name, $is_reverse_order, $compare_op) = @{$_};
  0            
3018 0   0       $compare_op ||= 'cmp';
3019 0 0         ($is_reverse_order
3020             ? "\$b->{'$name'} $compare_op \$a->{'$name'}"
3021             : "\$a->{'$name'} $compare_op \$b->{'$name'}");
3022 0           } @{$order_by}) . "\n"
  0            
3023             . "} values \%{\$topic->_body()}];\n"
3024             . "}\n"
3025             ;
3026 0           my $sort_func = eval $sort_func_perl;
3027 0 0         if (my $err = $@) {
3028 0           confess qq{Oops, failed to compile Perl sort func from order by;\n}
3029             . qq{ error message is [[$err]];\n}
3030             . qq{ source code is [[$sort_func_perl]].}
3031             }
3032 0           return $sort_func;
3033             }
3034              
3035             ###########################################################################
3036              
3037             sub limit {
3038 0     0 0   my ($topic, $ord_func, $min_rank, $max_rank) = @_;
3039              
3040 0           $topic->_assert_valid_func_arg( 'limit', '$ord_func', $ord_func );
3041              
3042 0           $topic->_assert_valid_nnint_arg( 'limit', '$min_rank', $min_rank );
3043 0           $topic->_assert_valid_nnint_arg( 'limit', '$max_rank', $max_rank );
3044 0 0         confess q{limit(): The $max_rank arg can't be less than the $min_rank.}
3045             if $max_rank < $min_rank;
3046              
3047 0 0         if ($topic->is_empty()) {
3048 0           return $topic;
3049             }
3050              
3051 0 0         if (!$topic->_is_known_dup_free()) {
3052 0           $topic->_dup_free_want_index_over_all_attrs();
3053             }
3054              
3055 0           my $topic_b = $topic->_body();
3056              
3057 0           my $ext_topic_tuples = [];
3058 0           my $topic_tuples_by_ext_tt_ref = {};
3059              
3060 0           for my $topic_t_refaddr (CORE::keys %{$topic_b}) {
  0            
3061 0           my $topic_t = $topic_b->{$topic_t_refaddr};
3062 0           my $ext_topic_t = $topic->_export_nfmt_tuple( $topic_t );
3063 0           push @{$ext_topic_tuples}, $ext_topic_t;
  0            
3064 0           $topic_tuples_by_ext_tt_ref->{refaddr $ext_topic_t}
3065             = $topic_t_refaddr;
3066             }
3067              
3068             my $sorted_ext_topic_tuples = [sort {
3069 0           local $_ = { 'a' => $a, 'b' => $b };
3070 0           $ord_func->();
3071 0           } @{$ext_topic_tuples}];
  0            
3072              
3073 0           my $result = $topic->empty();
3074              
3075 0           my $result_b = $result->_body();
3076              
3077 0           for my $ext_topic_t
3078 0           (@{$sorted_ext_topic_tuples}[$min_rank..$max_rank]) {
3079             my $topic_t_refaddr
3080 0           = $topic_tuples_by_ext_tt_ref->{refaddr $ext_topic_t};
3081 0           $result_b->{$topic_t_refaddr} = $topic_b->{$topic_t_refaddr};
3082             }
3083              
3084 0           return $result;
3085             }
3086              
3087             ###########################################################################
3088              
3089             sub limit_by_attr_names {
3090 0     0 0   my ($topic, $order_by, $min_rank, $max_rank) = @_;
3091              
3092 0           $order_by = $topic->_normalize_order_by_arg(
3093             'limit_by_attr_names', '$order_by', $order_by );
3094              
3095 0           $topic->_assert_valid_nnint_arg(
3096             'limit_by_attr_names', '$min_rank', $min_rank );
3097 0           $topic->_assert_valid_nnint_arg(
3098             'limit_by_attr_names', '$max_rank', $max_rank );
3099 0 0         confess q{limit_by_attr_names():}
3100             . q{ The $max_rank arg can't be less than the $min_rank.}
3101             if $max_rank < $min_rank;
3102              
3103 0 0         if ($topic->is_empty()) {
3104 0           return $topic;
3105             }
3106              
3107 0           my $sort_func = $topic->_sort_func_from_order_by( $order_by );
3108              
3109 0 0         if (!$topic->_is_known_dup_free()) {
3110 0           $topic->_dup_free_want_index_over_all_attrs();
3111             }
3112              
3113 0           my $result = $topic->empty();
3114              
3115 0           my $result_b = $result->_body();
3116              
3117 0           for my $topic_t (@{$sort_func->( $topic )}[$min_rank..$max_rank]) {
  0            
3118 0           $result_b->{refaddr $topic_t} = $topic_t;
3119             }
3120              
3121 0           return $result;
3122             }
3123              
3124             ###########################################################################
3125              
3126             sub substitution {
3127 0     0 0   my ($topic, $attr_names, $func, $allow_dup_tuples) = @_;
3128 0           (my $subst_h, $attr_names)
3129             = $topic->_atnms_hr_from_assert_valid_subst_args(
3130             'substitution', '$attr_names', '$func', $attr_names, $func );
3131 0           return $topic->_substitution(
3132             'substitution', '$attr_names', '$func',
3133             $attr_names, $func, $subst_h, $allow_dup_tuples );
3134             }
3135              
3136             sub _atnms_hr_from_assert_valid_subst_args {
3137 0     0     my ($topic, $rtn_nm, $arg_nm_atnms, $arg_nm_func, $atnms, $func) = @_;
3138              
3139 0           (my $subst_h, $atnms) = $topic->_atnms_hr_from_assert_valid_atnms_arg(
3140             $rtn_nm, $arg_nm_atnms, $atnms );
3141 0           my (undef, undef, $subst_only)
3142             = $topic->_ptn_conj_and_disj( $topic->_heading(), $subst_h );
3143             confess qq{$rtn_nm(): Bad $arg_nm_atnms arg; that attr list}
3144             . q{ isn't a subset of the invocant's heading.}
3145 0 0         if @{$subst_only} > 0;
  0            
3146              
3147 0           $topic->_assert_valid_func_arg( $rtn_nm, $arg_nm_func, $func );
3148              
3149 0           return ($subst_h, $atnms);
3150             }
3151              
3152             sub _substitution {
3153 0     0     my ($topic, $rtn_nm, $arg_nm_attrs, $arg_nm_func, $attrs, $func,
3154             $subst_h, $allow_dup_tuples) = @_;
3155              
3156 0 0         if ($topic->is_empty()) {
3157 0           return $topic;
3158             }
3159 0 0         if (@{$attrs} == 0) {
  0            
3160             # Substitution in zero attrs of input yields the input.
3161 0           return $topic;
3162             }
3163              
3164 0 0 0       if (!$allow_dup_tuples and !$topic->_is_known_dup_free()) {
3165 0           $topic->_dup_free_want_index_over_all_attrs();
3166             }
3167              
3168 0           my $result = $topic->empty();
3169              
3170 0           my $result_b = $result->_body();
3171              
3172 0           for my $topic_t (values %{$topic->_body()}) {
  0            
3173 0           my $subst_t;
3174             {
3175 0           local $_ = $topic->_export_nfmt_tuple( $topic_t );
  0            
3176 0           $subst_t = $func->();
3177             }
3178             $topic->_assert_valid_tuple_result_of_func_arg(
3179 0           $rtn_nm, $arg_nm_func, $arg_nm_attrs, $subst_t, $subst_h );
3180 0           $subst_t = $topic->_import_nfmt_tuple( $subst_t );
3181 0           my $result_t = {%{$topic_t}, %{$subst_t}};
  0            
  0            
3182 0           $result_b->{refaddr $result_t} = $result_t;
3183             }
3184              
3185 0           return $result;
3186             }
3187              
3188             ###########################################################################
3189              
3190             sub static_subst {
3191 0     0 0   my ($topic, $attrs) = @_;
3192 0           $topic->_assert_valid_static_subst_args(
3193             'static_subst', '$attrs', $attrs );
3194 0           return $topic->_static_subst( $attrs );
3195             }
3196              
3197             sub _assert_valid_static_subst_args {
3198 0     0     my ($topic, $rtn_nm, $arg_nm_attrs, $attrs) = @_;
3199              
3200 0 0         confess qq{$rtn_nm(): Bad $arg_nm_attrs arg; it isn't a hash-ref.}
3201             if ref $attrs ne 'HASH';
3202              
3203 0           my (undef, undef, $subst_only)
3204             = $topic->_ptn_conj_and_disj( $topic->_heading(), $attrs );
3205             confess qq{$rtn_nm(): Bad $arg_nm_attrs arg; that attr list}
3206             . q{ isn't a subset of the invocant's heading.}
3207 0 0         if @{$subst_only} > 0;
  0            
3208              
3209 0 0         confess qq{$rtn_nm(): Bad $arg_nm_attrs arg;}
3210             . q{ it is a hash-ref, and there exist circular refs}
3211             . q{ between itself or its tuple-valued components.}
3212             if $topic->_tuple_arg_has_circular_refs( $attrs );
3213              
3214 0           return;
3215             }
3216              
3217             sub _static_subst {
3218 0     0     my ($topic, $attrs) = @_;
3219              
3220 0 0         if ($topic->is_empty()) {
3221 0           return $topic;
3222             }
3223 0 0         if ((scalar CORE::keys %{$attrs}) == 0) {
  0            
3224             # Substitution in zero attrs of input yields the input.
3225 0           return $topic;
3226             }
3227              
3228 0           $attrs = $topic->_import_nfmt_tuple( $attrs );
3229              
3230 0           my $result = $topic->empty();
3231              
3232 0           my $result_b = $result->_body();
3233              
3234 0           for my $topic_t (values %{$topic->_body()}) {
  0            
3235 0           my $result_t = {%{$topic_t}, %{$attrs}};
  0            
  0            
3236 0           $result_b->{refaddr $result_t} = $result_t;
3237             }
3238              
3239 0           return $result;
3240             }
3241              
3242             ###########################################################################
3243              
3244             sub subst_in_restr {
3245 0     0 0   my ($topic, $restr_func, $subst_attr_names, $subst_func,
3246             $allow_dup_tuples) = @_;
3247              
3248 0           $topic->_assert_valid_func_arg(
3249             'subst_in_restr', '$restr_func', $restr_func );
3250              
3251 0           (my $subst_h, $subst_attr_names) = $topic
3252             ->_atnms_hr_from_assert_valid_subst_args( 'subst_in_restr',
3253             '$subst_attr_names', '$subst_func',
3254             $subst_attr_names, $subst_func );
3255              
3256             my ($topic_to_subst, $topic_no_subst)
3257 0           = @{$topic->_restr_and_cmpl( $restr_func, $allow_dup_tuples )};
  0            
3258              
3259 0           return $topic_to_subst
3260             ->_substitution( 'subst_in_restr', '$subst_attr_names',
3261             '$subst_func', $subst_attr_names, $subst_func, $subst_h,
3262             $allow_dup_tuples )
3263             ->_union( [$topic_no_subst] );
3264             }
3265              
3266             ###########################################################################
3267              
3268             sub static_subst_in_restr {
3269 0     0 0   my ($topic, $restr_func, $subst, $allow_dup_tuples) = @_;
3270              
3271 0           $topic->_assert_valid_func_arg(
3272             'static_subst_in_restr', '$restr_func', $restr_func );
3273              
3274 0           $topic->_assert_valid_static_subst_args(
3275             'static_subst_in_restr', '$subst', $subst );
3276              
3277             my ($topic_to_subst, $topic_no_subst)
3278 0           = @{$topic->_restr_and_cmpl( $restr_func, $allow_dup_tuples )};
  0            
3279              
3280 0           return $topic_to_subst
3281             ->_static_subst( $subst )
3282             ->_union( [$topic_no_subst] );
3283             }
3284              
3285             ###########################################################################
3286              
3287             sub subst_in_semijoin {
3288 0     0 0   my ($topic, $restr, $subst_attr_names, $subst_func, $allow_dup_tuples)
3289             = @_;
3290              
3291 0           $restr = $topic->_normalize_relation_arg(
3292             'subst_in_semijoin', '$restr', $restr );
3293              
3294 0           (my $subst_h, $subst_attr_names) = $topic
3295             ->_atnms_hr_from_assert_valid_subst_args( 'subst_in_semijoin',
3296             '$subst_attr_names', '$subst_func',
3297             $subst_attr_names, $subst_func );
3298              
3299             my ($topic_to_subst, $topic_no_subst)
3300 0           = @{$topic->_semijoin_and_diff( $restr )};
  0            
3301              
3302 0           return $topic_to_subst
3303             ->_substitution( 'subst_in_semijoin', '$subst_attr_names',
3304             '$subst_func', $subst_attr_names, $subst_func, $subst_h,
3305             $allow_dup_tuples )
3306             ->_union( [$topic_no_subst] );
3307             }
3308              
3309             ###########################################################################
3310              
3311             sub static_subst_in_semijoin {
3312 0     0 0   my ($topic, $restr, $subst) = @_;
3313              
3314 0           $restr = $topic->_normalize_relation_arg(
3315             'static_subst_in_semijoin', '$restr', $restr );
3316              
3317 0           $topic->_assert_valid_static_subst_args(
3318             'static_subst_in_semijoin', '$subst', $subst );
3319              
3320             my ($topic_to_subst, $topic_no_subst)
3321 0           = @{$topic->_semijoin_and_diff( $restr )};
  0            
3322              
3323 0           return $topic_to_subst
3324             ->_static_subst( $subst )
3325             ->_union( [$topic_no_subst] );
3326             }
3327              
3328             ###########################################################################
3329              
3330             sub outer_join_with_group {
3331 0     0 0   my ($primary, $secondary, $group_attr) = @_;
3332              
3333 0           $secondary = $primary->_normalize_relation_arg(
3334             'outer_join_with_group', '$secondary', $secondary );
3335 0           $primary->_assert_valid_atnm_arg(
3336             'outer_join_with_group', '$group_attr', $group_attr );
3337              
3338 0           my $primary_h = $primary->_heading();
3339              
3340             confess q{outer_join_with_group(): Bad $group_attr arg;}
3341             . q{ that name for a new attr to add}
3342             . q{ to $primary, consisting of grouped $secondary-only attrs,}
3343             . q{ duplicates an attr of $primary (not being grouped).}
3344 0 0         if exists $primary_h->{$group_attr};
3345              
3346             # TODO: inline+merge what join/group/etc do for better performance.
3347              
3348 0           my ($both, $primary_only, $inner) = $primary->_ptn_conj_and_disj(
3349             $primary_h, $secondary->_heading() );
3350 0           my $inner_h = {CORE::map { $_ => undef } @{$inner}};
  0            
  0            
3351              
3352             my ($pri_matched, $pri_nonmatched)
3353 0           = @{$primary->_semijoin_and_diff( $secondary )};
  0            
3354              
3355             my $result_matched = $pri_matched
3356             ->_join( [$secondary] )
3357 0           ->_group( $group_attr, $inner, [CORE::keys %{$primary_h}],
  0            
3358             $inner_h );
3359              
3360 0           my $result_nonmatched = $pri_nonmatched
3361             ->_static_exten( {$group_attr => $primary->_new( $inner )} );
3362              
3363 0           my $result = $result_matched->_union( [$result_nonmatched] );
3364              
3365 0 0 0       if ($primary->_is_known_dup_free()
3366             and $secondary->_is_known_dup_free()) {
3367 0           $result->_is_known_dup_free( 1 );
3368             }
3369              
3370 0           return $result;
3371             }
3372              
3373             ###########################################################################
3374              
3375             sub outer_join_with_undefs {
3376 0     0 0   my ($primary, $secondary) = @_;
3377              
3378 0           $secondary = $primary->_normalize_relation_arg(
3379             'outer_join_with_undefs', '$secondary', $secondary );
3380              
3381 0           my (undef, undef, $exten_attrs) = $primary->_ptn_conj_and_disj(
3382             $primary->_heading(), $secondary->_heading() );
3383 0           my $filler = {CORE::map { $_ => undef } @{$exten_attrs}};
  0            
  0            
3384              
3385             my ($pri_matched, $pri_nonmatched)
3386 0           = @{$primary->_semijoin_and_diff( $secondary )};
  0            
3387              
3388 0           my $result_matched = $pri_matched->_join( [$secondary] );
3389              
3390 0           my $result_nonmatched = $pri_nonmatched->_static_exten( $filler );
3391              
3392 0           my $result = $result_matched->_union( [$result_nonmatched] );
3393              
3394 0 0 0       if ($primary->_is_known_dup_free()
3395             and $secondary->_is_known_dup_free()) {
3396 0           $result->_is_known_dup_free( 1 );
3397             }
3398              
3399 0           return $result;
3400             }
3401              
3402             ###########################################################################
3403              
3404             sub outer_join_with_static_exten {
3405 0     0 0   my ($primary, $secondary, $filler) = @_;
3406              
3407 0           $secondary = $primary->_normalize_relation_arg(
3408             'outer_join_with_static_exten', '$secondary', $secondary );
3409              
3410 0 0         confess q{outer_join_with_static_exten(): Bad $filler arg;}
3411             . q{ it isn't a hash-ref.}
3412             if ref $filler ne 'HASH';
3413 0 0         confess q{outer_join_with_static_exten(): Bad $filler arg;}
3414             . q{ it is a hash-ref, and there exist circular refs}
3415             . q{ between itself or its tuple-valued components.}
3416             if $primary->_tuple_arg_has_circular_refs( $filler );
3417              
3418 0           my (undef, undef, $exten_attrs) = $primary->_ptn_conj_and_disj(
3419             $primary->_heading(), $secondary->_heading() );
3420 0           my $exten_h = {CORE::map { $_ => undef } @{$exten_attrs}};
  0            
  0            
3421              
3422 0 0         confess q{outer_join_with_static_exten(): Bad $filler arg elem;}
3423             . q{ it doesn't have exactly the}
3424             . q{ same set of attr names as the sub-heading of $secondary}
3425             . q{ that doesn't overlap with the heading of $primary.}
3426             if !$primary->_is_identical_hkeys( $exten_h, $filler );
3427              
3428             my ($pri_matched, $pri_nonmatched)
3429 0           = @{$primary->_semijoin_and_diff( $secondary )};
  0            
3430              
3431 0           my $result_matched = $pri_matched->_join( [$secondary] );
3432              
3433 0           my $result_nonmatched = $pri_nonmatched->_static_exten( $filler );
3434              
3435 0           my $result = $result_matched->_union( [$result_nonmatched] );
3436              
3437 0 0 0       if ($primary->_is_known_dup_free()
3438             and $secondary->_is_known_dup_free()) {
3439 0           $result->_is_known_dup_free( 1 );
3440             }
3441              
3442 0           return $result;
3443             }
3444              
3445             ###########################################################################
3446              
3447             sub outer_join_with_exten {
3448 0     0 0   my ($primary, $secondary, $exten_func, $allow_dup_tuples) = @_;
3449              
3450 0           $secondary = $primary->_normalize_relation_arg(
3451             'outer_join_with_exten', '$secondary', $secondary );
3452 0           $primary->_assert_valid_func_arg(
3453             'outer_join_with_exten', '$exten_func', $exten_func );
3454              
3455 0           my (undef, undef, $exten_attrs) = $primary->_ptn_conj_and_disj(
3456             $primary->_heading(), $secondary->_heading() );
3457 0           my $exten_h = {CORE::map { $_ => undef } @{$exten_attrs}};
  0            
  0            
3458              
3459             my ($pri_matched, $pri_nonmatched)
3460 0           = @{$primary->_semijoin_and_diff( $secondary )};
  0            
3461              
3462 0           my $result_matched = $pri_matched->_join( [$secondary] );
3463              
3464             # Note: if '_extension' dies due to what $exten_func did it would
3465             # state the error is reported by 'extension' and with some wrong
3466             # details; todo fix later; on correct it won't affect users though.
3467 0           my $result_nonmatched = $pri_nonmatched->_extension(
3468             $exten_attrs, $exten_func, $exten_h, $allow_dup_tuples );
3469              
3470 0           my $result = $result_matched->_union( [$result_nonmatched] );
3471              
3472 0 0 0       if ($primary->_is_known_dup_free()
3473             and $secondary->_is_known_dup_free()) {
3474 0           $result->_is_known_dup_free( 1 );
3475             }
3476              
3477 0           return $result;
3478             }
3479              
3480             ###########################################################################
3481              
3482             } # class Set::Relation::V2
3483              
3484             ###########################################################################
3485             ###########################################################################
3486              
3487             1;