Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/core/etl/src/Flow/ETL/DSL/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@
use Flow\ETL\Function\Count;
use Flow\ETL\Function\DateTimeFormat;
use Flow\ETL\Function\DenseRank;
use Flow\ETL\Function\EnumName;
use Flow\ETL\Function\EnumValue;
use Flow\ETL\Function\ExecutionMode;
use Flow\ETL\Function\Exists;
use Flow\ETL\Function\First;
Expand Down Expand Up @@ -1390,6 +1392,18 @@ function coalesce(ScalarFunction ...$values): Coalesce
return new Coalesce(...$values);
}

#[DocumentationDSL(module: Module::CORE, type: DSLType::SCALAR_FUNCTION)]
function enum_name(mixed $value): EnumName
{
return new EnumName($value);
}

#[DocumentationDSL(module: Module::CORE, type: DSLType::SCALAR_FUNCTION)]
function enum_value(mixed $value): EnumValue
{
return new EnumValue($value);
}

#[DocumentationDSL(module: Module::CORE, type: DSLType::AGGREGATING_FUNCTION)]
function count(?EntryReference $function = null): Count
{
Expand Down
33 changes: 33 additions & 0 deletions src/core/etl/src/Flow/ETL/Function/EnumName.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Flow\ETL\Function;

use Flow\ETL\Exception\InvalidArgumentException;
use Flow\ETL\FlowContext;
use Flow\ETL\Function\ScalarFunction\ScalarResult;
use Flow\ETL\Row;
use UnitEnum;

use function Flow\Types\DSL\type_string;

final class EnumName extends ScalarFunctionChain
{
public function __construct(
private readonly mixed $value,
) {}

public function eval(Row $row, FlowContext $context): ?ScalarResult
{
$enum = (new Parameter($this->value))->eval($row, $context);

if (!$enum instanceof UnitEnum) {
return $context
->functions()
->invalidResult(new InvalidArgumentException('EnumName function requires a UnitEnum value'));
}

return new ScalarResult($enum->name, type_string());
}
}
35 changes: 35 additions & 0 deletions src/core/etl/src/Flow/ETL/Function/EnumValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Flow\ETL\Function;

use BackedEnum;
use Flow\ETL\Exception\InvalidArgumentException;
use Flow\ETL\FlowContext;
use Flow\ETL\Function\ScalarFunction\ScalarResult;
use Flow\ETL\Row;

use function Flow\Types\DSL\type_integer;
use function Flow\Types\DSL\type_string;
use function is_int;

final class EnumValue extends ScalarFunctionChain
{
public function __construct(
private readonly mixed $value,
) {}

public function eval(Row $row, FlowContext $context): ?ScalarResult
{
$enum = (new Parameter($this->value))->eval($row, $context);

if (!$enum instanceof BackedEnum) {
return $context
->functions()
->invalidResult(new InvalidArgumentException('EnumValue function requires a BackedEnum value'));
}

return new ScalarResult($enum->value, is_int($enum->value) ? type_integer() : type_string());
}
}
10 changes: 10 additions & 0 deletions src/core/etl/src/Flow/ETL/Function/ScalarFunctionChain.php
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,16 @@ public function ensureStart(ScalarFunction|string $prefix): EnsureStart
return new EnsureStart($this, $prefix);
}

public function enumName(): EnumName
{
return new EnumName($this);
}

public function enumValue(): EnumValue
{
return new EnumValue($this);
}

public function equals(mixed $ref): Equals
{
return new Equals($this, $ref);
Expand Down
19 changes: 12 additions & 7 deletions src/core/etl/src/Flow/ETL/Schema/Definition/EnumDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,18 @@ public function merge(Definition $definition): Definition
return $this->makeNullable()->setMetadata($this->metadata->merge($definition->metadata()));
}

if ($definition instanceof self && $definition->enumClass === $this->enumClass) {
return new self(
$this->ref,
$this->enumClass,
$this->nullable || $definition->nullable,
$this->metadata->merge($definition->metadata),
);
if ($definition instanceof self) {
// A null enum value carries no class, EnumEntry falls back to UnitEnum, so the concrete side wins.
$enumClass = $this->enumClass === UnitEnum::class ? $definition->enumClass : $this->enumClass;

if ($enumClass === $definition->enumClass || $definition->enumClass === UnitEnum::class) {
return new self(
$this->ref,
$enumClass,
$this->nullable || $definition->nullable,
$this->metadata->merge($definition->metadata),
);
}
}

if ($definition instanceof StringDefinition) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Flow\ETL\Pipeline;
use Flow\ETL\Schema;
use Flow\ETL\Tests\Fixtures\Enum\BackedStringEnum;
use Flow\ETL\Tests\FlowIntegrationTestCase;

use function array_map;
Expand All @@ -14,6 +15,8 @@
use function Flow\ETL\DSL\bool_schema;
use function Flow\ETL\DSL\config;
use function Flow\ETL\DSL\df;
use function Flow\ETL\DSL\enum_entry;
use function Flow\ETL\DSL\enum_schema;
use function Flow\ETL\DSL\float_entry;
use function Flow\ETL\DSL\float_schema;
use function Flow\ETL\DSL\flow_context;
Expand Down Expand Up @@ -79,6 +82,19 @@ public function test_extraction_without_to_schema(): void
static::assertEquals(schema(int_schema('id'), str_schema('name'), null_schema('active')), $rows->schema());
}

public function test_getting_schema_of_enum_column_with_null_values(): void
{
static::assertEquals(
schema(enum_schema('status', BackedStringEnum::class, nullable: true)),
df()
->read(from_rows(rows(
row(enum_entry('status', BackedStringEnum::one)),
row(enum_entry('status', null)),
)))
->schema(),
);
}

public function test_getting_schema(): void
{
$rows = array_to_rows(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace Flow\ETL\Tests\Integration\Function;

use Flow\ETL\Memory\ArrayMemory;
use Flow\ETL\Tests\Fixtures\Enum\BackedIntEnum;
use Flow\ETL\Tests\FlowTestCase;

use function Flow\ETL\DSL\data_frame;
use function Flow\ETL\DSL\enum_entry;
use function Flow\ETL\DSL\enum_name;
use function Flow\ETL\DSL\from_rows;
use function Flow\ETL\DSL\ref;
use function Flow\ETL\DSL\row;
use function Flow\ETL\DSL\rows;
use function Flow\ETL\DSL\to_memory;
use function Flow\Types\DSL\type_equals;
use function Flow\Types\DSL\type_string;

final class EnumNameTest extends FlowTestCase
{
public function test_enum_name_produces_string_entry_for_int_backed_enum(): void
{
static::assertTrue(type_equals(
type_string(),
data_frame()
->read(from_rows(rows(row(enum_entry('e', BackedIntEnum::one)))))
->withEntry('code', enum_name(ref('e')))
->schema()
->get('code')
->type(),
));
}

public function test_enum_name_writes_null_for_null_enum_in_permissive_mode(): void
{
data_frame()
->read(from_rows(rows(row(enum_entry('e', BackedIntEnum::one)), row(enum_entry('e', null)))))
->withEntry('code', enum_name(ref('e')))
->select('code')
->write(to_memory($memory = new ArrayMemory()))
->run();

static::assertSame([['code' => 'one'], ['code' => null]], $memory->dump());
}

public function test_enum_name_writes_case_names(): void
{
data_frame()
->read(from_rows(rows(row(enum_entry('e', BackedIntEnum::one)), row(enum_entry('e', BackedIntEnum::two)))))
->withEntry('code', enum_name(ref('e')))
->select('code')
->write(to_memory($memory = new ArrayMemory()))
->run();

static::assertSame([['code' => 'one'], ['code' => 'two']], $memory->dump());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

namespace Flow\ETL\Tests\Integration\Function;

use Flow\ETL\Memory\ArrayMemory;
use Flow\ETL\Tests\Fixtures\Enum\BackedIntEnum;
use Flow\ETL\Tests\Fixtures\Enum\BackedStringEnum;
use Flow\ETL\Tests\FlowTestCase;

use function Flow\ETL\DSL\data_frame;
use function Flow\ETL\DSL\enum_entry;
use function Flow\ETL\DSL\enum_value;
use function Flow\ETL\DSL\from_rows;
use function Flow\ETL\DSL\ref;
use function Flow\ETL\DSL\row;
use function Flow\ETL\DSL\rows;
use function Flow\ETL\DSL\to_memory;
use function Flow\Types\DSL\type_equals;
use function Flow\Types\DSL\type_integer;
use function Flow\Types\DSL\type_string;

final class EnumValueTest extends FlowTestCase
{
public function test_enum_value_produces_integer_entry_for_int_backed_enum(): void
{
static::assertTrue(type_equals(
type_integer(),
data_frame()
->read(from_rows(rows(row(enum_entry('e', BackedIntEnum::one)))))
->withEntry('code', enum_value(ref('e')))
->schema()
->get('code')
->type(),
));
}

public function test_enum_value_produces_string_entry_for_string_backed_enum(): void
{
static::assertTrue(type_equals(
type_string(),
data_frame()
->read(from_rows(rows(row(enum_entry('e', BackedStringEnum::one)))))
->withEntry('code', enum_value(ref('e')))
->schema()
->get('code')
->type(),
));
}

public function test_enum_value_writes_null_for_null_enum_in_permissive_mode(): void
{
data_frame()
->read(from_rows(rows(row(enum_entry('e', BackedIntEnum::one)), row(enum_entry('e', null)))))
->withEntry('code', enum_value(ref('e')))
->select('code')
->write(to_memory($memory = new ArrayMemory()))
->run();

static::assertSame([['code' => 1], ['code' => null]], $memory->dump());
}

public function test_enum_value_writes_backing_values(): void
{
data_frame()
->read(from_rows(rows(row(enum_entry('e', BackedIntEnum::one)), row(enum_entry('e', BackedIntEnum::two)))))
->withEntry('code', enum_value(ref('e')))
->select('code')
->write(to_memory($memory = new ArrayMemory()))
->run();

static::assertSame([['code' => 1], ['code' => 2]], $memory->dump());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

namespace Flow\ETL\Tests\Unit\Function;

use Flow\ETL\Exception\InvalidArgumentException;
use Flow\ETL\Function\ExecutionMode;
use Flow\ETL\Tests\Fixtures\Enum\BackedIntEnum;
use Flow\ETL\Tests\Fixtures\Enum\BackedStringEnum;
use Flow\ETL\Tests\Fixtures\Enum\BasicEnum;
use Flow\ETL\Tests\FlowTestCase;
use PHPUnit\Framework\Attributes\TestWith;
use UnitEnum;

use function Flow\ETL\DSL\config;
use function Flow\ETL\DSL\enum_entry;
use function Flow\ETL\DSL\enum_name;
use function Flow\ETL\DSL\flow_context;
use function Flow\ETL\DSL\ref;
use function Flow\ETL\DSL\row;
use function Flow\Types\DSL\type_equals;
use function Flow\Types\DSL\type_string;

final class EnumNameTest extends FlowTestCase
{
public function test_enum_name_accepts_literal_enum(): void
{
static::assertSame('one', enum_name(BackedStringEnum::one)->eval(row(), flow_context())?->value);
}

public function test_enum_name_carries_string_type(): void
{
$result = enum_name(ref('e'))->eval(row(enum_entry('e', BackedIntEnum::one)), flow_context());

static::assertNotNull($result);
static::assertTrue(type_equals(type_string(), $result->type));
}

public function test_enum_name_from_scalar_function_chain(): void
{
static::assertSame(
'one',
ref('e')->enumName()->eval(row(enum_entry('e', BackedIntEnum::one)), flow_context())?->value,
);
}

#[TestWith([BackedStringEnum::one])]
#[TestWith([BackedIntEnum::one])]
#[TestWith([BasicEnum::one])]
public function test_enum_name_returns_case_name(UnitEnum $enum): void
{
static::assertSame('one', enum_name(ref('e'))->eval(row(enum_entry('e', $enum)), flow_context())?->value);
}

#[TestWith([null])]
#[TestWith(['foo'])]
#[TestWith([42])]
public function test_enum_name_returns_null_in_permissive_mode(mixed $input): void
{
static::assertNull(enum_name($input)->eval(row(), flow_context()));
}

#[TestWith([null])]
#[TestWith(['foo'])]
#[TestWith([42])]
public function test_enum_name_throws_in_strict_mode(mixed $input): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('EnumName function requires a UnitEnum value');

$context = flow_context(config());
$context->functions()->setMode(ExecutionMode::STRICT);

enum_name($input)->eval(row(), $context);
}
}
Loading
Loading