Skip to content

Commit fea62a4

Browse files
authored
Merge pull request #2587 from flow-php/enum-scalar-functions
feat(flow-php/etl): enum_value and enum_name scalar function
2 parents d1dec40 + bc5d672 commit fea62a4

13 files changed

Lines changed: 448 additions & 9 deletions

File tree

src/core/etl/src/Flow/ETL/DSL/functions.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@
9191
use Flow\ETL\Function\Count;
9292
use Flow\ETL\Function\DateTimeFormat;
9393
use Flow\ETL\Function\DenseRank;
94+
use Flow\ETL\Function\EnumName;
95+
use Flow\ETL\Function\EnumValue;
9496
use Flow\ETL\Function\ExecutionMode;
9597
use Flow\ETL\Function\Exists;
9698
use Flow\ETL\Function\First;
@@ -1390,6 +1392,18 @@ function coalesce(ScalarFunction ...$values): Coalesce
13901392
return new Coalesce(...$values);
13911393
}
13921394

1395+
#[DocumentationDSL(module: Module::CORE, type: DSLType::SCALAR_FUNCTION)]
1396+
function enum_name(mixed $value): EnumName
1397+
{
1398+
return new EnumName($value);
1399+
}
1400+
1401+
#[DocumentationDSL(module: Module::CORE, type: DSLType::SCALAR_FUNCTION)]
1402+
function enum_value(mixed $value): EnumValue
1403+
{
1404+
return new EnumValue($value);
1405+
}
1406+
13931407
#[DocumentationDSL(module: Module::CORE, type: DSLType::AGGREGATING_FUNCTION)]
13941408
function count(?EntryReference $function = null): Count
13951409
{
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\ETL\Function;
6+
7+
use Flow\ETL\Exception\InvalidArgumentException;
8+
use Flow\ETL\FlowContext;
9+
use Flow\ETL\Function\ScalarFunction\ScalarResult;
10+
use Flow\ETL\Row;
11+
use UnitEnum;
12+
13+
use function Flow\Types\DSL\type_string;
14+
15+
final class EnumName extends ScalarFunctionChain
16+
{
17+
public function __construct(
18+
private readonly mixed $value,
19+
) {}
20+
21+
public function eval(Row $row, FlowContext $context): ?ScalarResult
22+
{
23+
$enum = (new Parameter($this->value))->eval($row, $context);
24+
25+
if (!$enum instanceof UnitEnum) {
26+
return $context
27+
->functions()
28+
->invalidResult(new InvalidArgumentException('EnumName function requires a UnitEnum value'));
29+
}
30+
31+
return new ScalarResult($enum->name, type_string());
32+
}
33+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\ETL\Function;
6+
7+
use BackedEnum;
8+
use Flow\ETL\Exception\InvalidArgumentException;
9+
use Flow\ETL\FlowContext;
10+
use Flow\ETL\Function\ScalarFunction\ScalarResult;
11+
use Flow\ETL\Row;
12+
13+
use function Flow\Types\DSL\type_integer;
14+
use function Flow\Types\DSL\type_string;
15+
use function is_int;
16+
17+
final class EnumValue extends ScalarFunctionChain
18+
{
19+
public function __construct(
20+
private readonly mixed $value,
21+
) {}
22+
23+
public function eval(Row $row, FlowContext $context): ?ScalarResult
24+
{
25+
$enum = (new Parameter($this->value))->eval($row, $context);
26+
27+
if (!$enum instanceof BackedEnum) {
28+
return $context
29+
->functions()
30+
->invalidResult(new InvalidArgumentException('EnumValue function requires a BackedEnum value'));
31+
}
32+
33+
return new ScalarResult($enum->value, is_int($enum->value) ? type_integer() : type_string());
34+
}
35+
}

src/core/etl/src/Flow/ETL/Function/ScalarFunctionChain.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,16 @@ public function ensureStart(ScalarFunction|string $prefix): EnsureStart
292292
return new EnsureStart($this, $prefix);
293293
}
294294

295+
public function enumName(): EnumName
296+
{
297+
return new EnumName($this);
298+
}
299+
300+
public function enumValue(): EnumValue
301+
{
302+
return new EnumValue($this);
303+
}
304+
295305
public function equals(mixed $ref): Equals
296306
{
297307
return new Equals($this, $ref);

src/core/etl/src/Flow/ETL/Schema/Definition/EnumDefinition.php

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -137,13 +137,18 @@ public function merge(Definition $definition): Definition
137137
return $this->makeNullable()->setMetadata($this->metadata->merge($definition->metadata()));
138138
}
139139

140-
if ($definition instanceof self && $definition->enumClass === $this->enumClass) {
141-
return new self(
142-
$this->ref,
143-
$this->enumClass,
144-
$this->nullable || $definition->nullable,
145-
$this->metadata->merge($definition->metadata),
146-
);
140+
if ($definition instanceof self) {
141+
// A null enum value carries no class, EnumEntry falls back to UnitEnum, so the concrete side wins.
142+
$enumClass = $this->enumClass === UnitEnum::class ? $definition->enumClass : $this->enumClass;
143+
144+
if ($enumClass === $definition->enumClass || $definition->enumClass === UnitEnum::class) {
145+
return new self(
146+
$this->ref,
147+
$enumClass,
148+
$this->nullable || $definition->nullable,
149+
$this->metadata->merge($definition->metadata),
150+
);
151+
}
147152
}
148153

149154
if ($definition instanceof StringDefinition) {

src/core/etl/tests/Flow/ETL/Tests/Integration/DataFrame/SchemaTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Flow\ETL\Pipeline;
88
use Flow\ETL\Schema;
9+
use Flow\ETL\Tests\Fixtures\Enum\BackedStringEnum;
910
use Flow\ETL\Tests\FlowIntegrationTestCase;
1011

1112
use function array_map;
@@ -14,6 +15,8 @@
1415
use function Flow\ETL\DSL\bool_schema;
1516
use function Flow\ETL\DSL\config;
1617
use function Flow\ETL\DSL\df;
18+
use function Flow\ETL\DSL\enum_entry;
19+
use function Flow\ETL\DSL\enum_schema;
1720
use function Flow\ETL\DSL\float_entry;
1821
use function Flow\ETL\DSL\float_schema;
1922
use function Flow\ETL\DSL\flow_context;
@@ -79,6 +82,19 @@ public function test_extraction_without_to_schema(): void
7982
static::assertEquals(schema(int_schema('id'), str_schema('name'), null_schema('active')), $rows->schema());
8083
}
8184

85+
public function test_getting_schema_of_enum_column_with_null_values(): void
86+
{
87+
static::assertEquals(
88+
schema(enum_schema('status', BackedStringEnum::class, nullable: true)),
89+
df()
90+
->read(from_rows(rows(
91+
row(enum_entry('status', BackedStringEnum::one)),
92+
row(enum_entry('status', null)),
93+
)))
94+
->schema(),
95+
);
96+
}
97+
8298
public function test_getting_schema(): void
8399
{
84100
$rows = array_to_rows(
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\ETL\Tests\Integration\Function;
6+
7+
use Flow\ETL\Memory\ArrayMemory;
8+
use Flow\ETL\Tests\Fixtures\Enum\BackedIntEnum;
9+
use Flow\ETL\Tests\FlowTestCase;
10+
11+
use function Flow\ETL\DSL\data_frame;
12+
use function Flow\ETL\DSL\enum_entry;
13+
use function Flow\ETL\DSL\enum_name;
14+
use function Flow\ETL\DSL\from_rows;
15+
use function Flow\ETL\DSL\ref;
16+
use function Flow\ETL\DSL\row;
17+
use function Flow\ETL\DSL\rows;
18+
use function Flow\ETL\DSL\to_memory;
19+
use function Flow\Types\DSL\type_equals;
20+
use function Flow\Types\DSL\type_string;
21+
22+
final class EnumNameTest extends FlowTestCase
23+
{
24+
public function test_enum_name_produces_string_entry_for_int_backed_enum(): void
25+
{
26+
static::assertTrue(type_equals(
27+
type_string(),
28+
data_frame()
29+
->read(from_rows(rows(row(enum_entry('e', BackedIntEnum::one)))))
30+
->withEntry('code', enum_name(ref('e')))
31+
->schema()
32+
->get('code')
33+
->type(),
34+
));
35+
}
36+
37+
public function test_enum_name_writes_null_for_null_enum_in_permissive_mode(): void
38+
{
39+
data_frame()
40+
->read(from_rows(rows(row(enum_entry('e', BackedIntEnum::one)), row(enum_entry('e', null)))))
41+
->withEntry('code', enum_name(ref('e')))
42+
->select('code')
43+
->write(to_memory($memory = new ArrayMemory()))
44+
->run();
45+
46+
static::assertSame([['code' => 'one'], ['code' => null]], $memory->dump());
47+
}
48+
49+
public function test_enum_name_writes_case_names(): void
50+
{
51+
data_frame()
52+
->read(from_rows(rows(row(enum_entry('e', BackedIntEnum::one)), row(enum_entry('e', BackedIntEnum::two)))))
53+
->withEntry('code', enum_name(ref('e')))
54+
->select('code')
55+
->write(to_memory($memory = new ArrayMemory()))
56+
->run();
57+
58+
static::assertSame([['code' => 'one'], ['code' => 'two']], $memory->dump());
59+
}
60+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\ETL\Tests\Integration\Function;
6+
7+
use Flow\ETL\Memory\ArrayMemory;
8+
use Flow\ETL\Tests\Fixtures\Enum\BackedIntEnum;
9+
use Flow\ETL\Tests\Fixtures\Enum\BackedStringEnum;
10+
use Flow\ETL\Tests\FlowTestCase;
11+
12+
use function Flow\ETL\DSL\data_frame;
13+
use function Flow\ETL\DSL\enum_entry;
14+
use function Flow\ETL\DSL\enum_value;
15+
use function Flow\ETL\DSL\from_rows;
16+
use function Flow\ETL\DSL\ref;
17+
use function Flow\ETL\DSL\row;
18+
use function Flow\ETL\DSL\rows;
19+
use function Flow\ETL\DSL\to_memory;
20+
use function Flow\Types\DSL\type_equals;
21+
use function Flow\Types\DSL\type_integer;
22+
use function Flow\Types\DSL\type_string;
23+
24+
final class EnumValueTest extends FlowTestCase
25+
{
26+
public function test_enum_value_produces_integer_entry_for_int_backed_enum(): void
27+
{
28+
static::assertTrue(type_equals(
29+
type_integer(),
30+
data_frame()
31+
->read(from_rows(rows(row(enum_entry('e', BackedIntEnum::one)))))
32+
->withEntry('code', enum_value(ref('e')))
33+
->schema()
34+
->get('code')
35+
->type(),
36+
));
37+
}
38+
39+
public function test_enum_value_produces_string_entry_for_string_backed_enum(): void
40+
{
41+
static::assertTrue(type_equals(
42+
type_string(),
43+
data_frame()
44+
->read(from_rows(rows(row(enum_entry('e', BackedStringEnum::one)))))
45+
->withEntry('code', enum_value(ref('e')))
46+
->schema()
47+
->get('code')
48+
->type(),
49+
));
50+
}
51+
52+
public function test_enum_value_writes_null_for_null_enum_in_permissive_mode(): void
53+
{
54+
data_frame()
55+
->read(from_rows(rows(row(enum_entry('e', BackedIntEnum::one)), row(enum_entry('e', null)))))
56+
->withEntry('code', enum_value(ref('e')))
57+
->select('code')
58+
->write(to_memory($memory = new ArrayMemory()))
59+
->run();
60+
61+
static::assertSame([['code' => 1], ['code' => null]], $memory->dump());
62+
}
63+
64+
public function test_enum_value_writes_backing_values(): void
65+
{
66+
data_frame()
67+
->read(from_rows(rows(row(enum_entry('e', BackedIntEnum::one)), row(enum_entry('e', BackedIntEnum::two)))))
68+
->withEntry('code', enum_value(ref('e')))
69+
->select('code')
70+
->write(to_memory($memory = new ArrayMemory()))
71+
->run();
72+
73+
static::assertSame([['code' => 1], ['code' => 2]], $memory->dump());
74+
}
75+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\ETL\Tests\Unit\Function;
6+
7+
use Flow\ETL\Exception\InvalidArgumentException;
8+
use Flow\ETL\Function\ExecutionMode;
9+
use Flow\ETL\Tests\Fixtures\Enum\BackedIntEnum;
10+
use Flow\ETL\Tests\Fixtures\Enum\BackedStringEnum;
11+
use Flow\ETL\Tests\Fixtures\Enum\BasicEnum;
12+
use Flow\ETL\Tests\FlowTestCase;
13+
use PHPUnit\Framework\Attributes\TestWith;
14+
use UnitEnum;
15+
16+
use function Flow\ETL\DSL\config;
17+
use function Flow\ETL\DSL\enum_entry;
18+
use function Flow\ETL\DSL\enum_name;
19+
use function Flow\ETL\DSL\flow_context;
20+
use function Flow\ETL\DSL\ref;
21+
use function Flow\ETL\DSL\row;
22+
use function Flow\Types\DSL\type_equals;
23+
use function Flow\Types\DSL\type_string;
24+
25+
final class EnumNameTest extends FlowTestCase
26+
{
27+
public function test_enum_name_accepts_literal_enum(): void
28+
{
29+
static::assertSame('one', enum_name(BackedStringEnum::one)->eval(row(), flow_context())?->value);
30+
}
31+
32+
public function test_enum_name_carries_string_type(): void
33+
{
34+
$result = enum_name(ref('e'))->eval(row(enum_entry('e', BackedIntEnum::one)), flow_context());
35+
36+
static::assertNotNull($result);
37+
static::assertTrue(type_equals(type_string(), $result->type));
38+
}
39+
40+
public function test_enum_name_from_scalar_function_chain(): void
41+
{
42+
static::assertSame(
43+
'one',
44+
ref('e')->enumName()->eval(row(enum_entry('e', BackedIntEnum::one)), flow_context())?->value,
45+
);
46+
}
47+
48+
#[TestWith([BackedStringEnum::one])]
49+
#[TestWith([BackedIntEnum::one])]
50+
#[TestWith([BasicEnum::one])]
51+
public function test_enum_name_returns_case_name(UnitEnum $enum): void
52+
{
53+
static::assertSame('one', enum_name(ref('e'))->eval(row(enum_entry('e', $enum)), flow_context())?->value);
54+
}
55+
56+
#[TestWith([null])]
57+
#[TestWith(['foo'])]
58+
#[TestWith([42])]
59+
public function test_enum_name_returns_null_in_permissive_mode(mixed $input): void
60+
{
61+
static::assertNull(enum_name($input)->eval(row(), flow_context()));
62+
}
63+
64+
#[TestWith([null])]
65+
#[TestWith(['foo'])]
66+
#[TestWith([42])]
67+
public function test_enum_name_throws_in_strict_mode(mixed $input): void
68+
{
69+
$this->expectException(InvalidArgumentException::class);
70+
$this->expectExceptionMessage('EnumName function requires a UnitEnum value');
71+
72+
$context = flow_context(config());
73+
$context->functions()->setMode(ExecutionMode::STRICT);
74+
75+
enum_name($input)->eval(row(), $context);
76+
}
77+
}

0 commit comments

Comments
 (0)