Skip to content

Commit 1c21eca

Browse files
authored
Added head/tail/last method to Rows (#1792)
1 parent cccb6f3 commit 1c21eca

2 files changed

Lines changed: 275 additions & 0 deletions

File tree

src/core/etl/src/Flow/ETL/Rows.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
namespace Flow\ETL;
66

77
use function Flow\ETL\DSL\{array_to_rows, row};
8+
use function Flow\Types\DSL\type_integer;
89
use Flow\ETL\Exception\{DuplicatedEntriesException, InvalidArgumentException, RuntimeException};
910
use Flow\ETL\Hash\{Algorithm, NativePHPHash};
1011
use Flow\ETL\Join\Expression;
1112
use Flow\ETL\Row\{CartesianProduct, EntryFactory};
1213
use Flow\ETL\Row\Comparator\NativeComparator;
1314
use Flow\ETL\Row\{Comparator, Entries, Reference, References, SortOrder};
1415
use Flow\Filesystem\{Partition, Partitions};
16+
use Flow\Types\Exception\InvalidTypeException;
1517

1618
/**
1719
* @implements \ArrayAccess<int, Row>
@@ -299,6 +301,26 @@ public function hash(Algorithm $algorithm = new NativePHPHash()) : string
299301
return $algorithm->hash($hash);
300302
}
301303

304+
/**
305+
* @param int $count - Count of rows to return. Must be >= 0.
306+
*
307+
* @throws InvalidArgumentException When count is negative
308+
* @throws InvalidTypeException When count is not an integer */
309+
public function head(int $count) : self
310+
{
311+
$count = type_integer()->assert($count);
312+
313+
if ($count < 0) {
314+
throw new InvalidArgumentException('Count must be greater than or equal to 0');
315+
}
316+
317+
if ($count === 0) {
318+
return self::partitioned([], $this->partitions);
319+
}
320+
321+
return self::partitioned(\array_slice($this->rows, 0, $count), $this->partitions);
322+
}
323+
302324
public function isPartitioned() : bool
303325
{
304326
return \count($this->partitions) > 0;
@@ -484,6 +506,15 @@ public function joinRight(self $right, Expression $expression) : self
484506
return new self(...$joined);
485507
}
486508

509+
public function last() : ?Row
510+
{
511+
if (empty($this->rows)) {
512+
return null;
513+
}
514+
515+
return $this->rows[\count($this->rows) - 1];
516+
}
517+
487518
/**
488519
* @param callable(Row) : Row $callable
489520
*/
@@ -739,6 +770,32 @@ public function sortEntries() : self
739770
return $this->map(fn (Row $row) : Row => $row->sortEntries());
740771
}
741772

773+
/**
774+
* @param int $count - Count of rows to return. Must be >= 0.
775+
*
776+
* @throws InvalidArgumentException When count is negative
777+
* @throws InvalidTypeException When count is not an integer */
778+
public function tail(int $count) : self
779+
{
780+
$count = type_integer()->assert($count);
781+
782+
if ($count < 0) {
783+
throw new InvalidArgumentException('Count must be greater than or equal to 0');
784+
}
785+
786+
if ($count === 0) {
787+
return self::partitioned([], $this->partitions);
788+
}
789+
790+
$rowsCount = \count($this->rows);
791+
792+
if ($count >= $rowsCount) {
793+
return self::partitioned($this->rows, $this->partitions);
794+
}
795+
796+
return self::partitioned(\array_slice($this->rows, -$count), $this->partitions);
797+
}
798+
742799
public function take(int $size) : self
743800
{
744801
return self::partitioned(\array_slice($this->rows, 0, $size), $this->partitions);

src/core/etl/tests/Flow/ETL/Tests/Unit/RowsTest.php

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,122 @@ public function test_hash_rows_with_different_order() : void
486486
);
487487
}
488488

489+
public function test_head() : void
490+
{
491+
$rows = rows(
492+
row(int_entry('id', 1)),
493+
row(int_entry('id', 2)),
494+
row(int_entry('id', 3)),
495+
row(int_entry('id', 4)),
496+
row(int_entry('id', 5)),
497+
);
498+
499+
$head = $rows->head(3);
500+
501+
self::assertCount(3, $head);
502+
self::assertSame(1, $head[0]->valueOf('id'));
503+
self::assertSame(2, $head[1]->valueOf('id'));
504+
self::assertSame(3, $head[2]->valueOf('id'));
505+
}
506+
507+
public function test_head_on_empty_rows() : void
508+
{
509+
$head = (rows())->head(5);
510+
511+
self::assertCount(0, $head);
512+
}
513+
514+
public function test_head_preserves_partitions() : void
515+
{
516+
$rows = rows_partitioned(
517+
[
518+
row(int_entry('id', 1), str_entry('group', 'a')),
519+
row(int_entry('id', 2), str_entry('group', 'a')),
520+
row(int_entry('id', 3), str_entry('group', 'a')),
521+
],
522+
[partition('group', 'a')]
523+
);
524+
525+
$head = $rows->head(2);
526+
527+
self::assertEquals(partitions(partition('group', 'a')), $head->partitions());
528+
self::assertCount(2, $head);
529+
}
530+
531+
public function test_head_with_count_larger_than_available() : void
532+
{
533+
$rows = rows(
534+
row(int_entry('id', 1)),
535+
row(int_entry('id', 2)),
536+
row(int_entry('id', 3)),
537+
);
538+
539+
$head = $rows->head(10);
540+
541+
self::assertCount(3, $head);
542+
self::assertSame(1, $head[0]->valueOf('id'));
543+
self::assertSame(2, $head[1]->valueOf('id'));
544+
self::assertSame(3, $head[2]->valueOf('id'));
545+
}
546+
547+
public function test_head_with_negative_count() : void
548+
{
549+
$this->expectException(InvalidArgumentException::class);
550+
$this->expectExceptionMessage('Count must be greater than or equal to 0');
551+
552+
$rows = rows(
553+
row(int_entry('id', 1)),
554+
row(int_entry('id', 2)),
555+
row(int_entry('id', 3)),
556+
);
557+
558+
$rows->head(-1);
559+
}
560+
561+
public function test_head_with_zero_count() : void
562+
{
563+
$rows = rows(
564+
row(int_entry('id', 1)),
565+
row(int_entry('id', 2)),
566+
row(int_entry('id', 3)),
567+
);
568+
569+
$head = $rows->head(0);
570+
571+
self::assertCount(0, $head);
572+
}
573+
574+
public function test_last() : void
575+
{
576+
$rows = rows(
577+
row(int_entry('id', 1)),
578+
row(int_entry('id', 2)),
579+
row(int_entry('id', 3)),
580+
);
581+
582+
$lastRow = $rows->last();
583+
584+
self::assertNotNull($lastRow);
585+
self::assertSame(3, $lastRow->valueOf('id'));
586+
}
587+
588+
public function test_last_on_empty_rows() : void
589+
{
590+
$lastRow = (rows())->last();
591+
592+
self::assertNull($lastRow);
593+
}
594+
595+
public function test_last_on_single_row() : void
596+
{
597+
$rows = rows(row(int_entry('id', 42)));
598+
599+
$lastRow = $rows->last();
600+
601+
self::assertNotNull($lastRow);
602+
self::assertSame(42, $lastRow->valueOf('id'));
603+
}
604+
489605
public function test_merge_empty_rows_with_partitioned_rows() : void
490606
{
491607
$rows1 = rows(row(int_entry('id', 1), str_entry('group', 'a')))->partitionBy(ref('group'))[0];
@@ -1006,6 +1122,108 @@ public function test_sorts_entries_in_all_rows() : void
10061122
);
10071123
}
10081124

1125+
public function test_tail() : void
1126+
{
1127+
$rows = rows(
1128+
row(int_entry('id', 1)),
1129+
row(int_entry('id', 2)),
1130+
row(int_entry('id', 3)),
1131+
row(int_entry('id', 4)),
1132+
row(int_entry('id', 5)),
1133+
);
1134+
1135+
$tail = $rows->tail(3);
1136+
1137+
self::assertCount(3, $tail);
1138+
self::assertSame(3, $tail[0]->valueOf('id'));
1139+
self::assertSame(4, $tail[1]->valueOf('id'));
1140+
self::assertSame(5, $tail[2]->valueOf('id'));
1141+
}
1142+
1143+
public function test_tail_maintains_correct_order() : void
1144+
{
1145+
$rows = rows(
1146+
row(int_entry('id', 1)),
1147+
row(int_entry('id', 2)),
1148+
row(int_entry('id', 3)),
1149+
row(int_entry('id', 4)),
1150+
row(int_entry('id', 5)),
1151+
);
1152+
1153+
$tail = $rows->tail(2);
1154+
1155+
self::assertCount(2, $tail);
1156+
self::assertSame(4, $tail[0]->valueOf('id'));
1157+
self::assertSame(5, $tail[1]->valueOf('id'));
1158+
}
1159+
1160+
public function test_tail_on_empty_rows() : void
1161+
{
1162+
$tail = (rows())->tail(5);
1163+
1164+
self::assertCount(0, $tail);
1165+
}
1166+
1167+
public function test_tail_preserves_partitions() : void
1168+
{
1169+
$rows = rows_partitioned(
1170+
[
1171+
row(int_entry('id', 1), str_entry('group', 'a')),
1172+
row(int_entry('id', 2), str_entry('group', 'a')),
1173+
row(int_entry('id', 3), str_entry('group', 'a')),
1174+
],
1175+
[partition('group', 'a')]
1176+
);
1177+
1178+
$tail = $rows->tail(2);
1179+
1180+
self::assertEquals(partitions(partition('group', 'a')), $tail->partitions());
1181+
self::assertCount(2, $tail);
1182+
}
1183+
1184+
public function test_tail_with_count_larger_than_available() : void
1185+
{
1186+
$rows = rows(
1187+
row(int_entry('id', 1)),
1188+
row(int_entry('id', 2)),
1189+
row(int_entry('id', 3)),
1190+
);
1191+
1192+
$tail = $rows->tail(10);
1193+
1194+
self::assertCount(3, $tail);
1195+
self::assertSame(1, $tail[0]->valueOf('id'));
1196+
self::assertSame(2, $tail[1]->valueOf('id'));
1197+
self::assertSame(3, $tail[2]->valueOf('id'));
1198+
}
1199+
1200+
public function test_tail_with_negative_count() : void
1201+
{
1202+
$this->expectException(InvalidArgumentException::class);
1203+
$this->expectExceptionMessage('Count must be greater than or equal to 0');
1204+
1205+
$rows = rows(
1206+
row(int_entry('id', 1)),
1207+
row(int_entry('id', 2)),
1208+
row(int_entry('id', 3)),
1209+
);
1210+
1211+
$rows->tail(-1);
1212+
}
1213+
1214+
public function test_tail_with_zero_count() : void
1215+
{
1216+
$rows = rows(
1217+
row(int_entry('id', 1)),
1218+
row(int_entry('id', 2)),
1219+
row(int_entry('id', 3)),
1220+
);
1221+
1222+
$tail = $rows->tail(0);
1223+
1224+
self::assertCount(0, $tail);
1225+
}
1226+
10091227
public function test_take() : void
10101228
{
10111229
$rows = rows(

0 commit comments

Comments
 (0)