From b4087a524a33fe34c70eb11d72b3a4e38d141a09 Mon Sep 17 00:00:00 2001 From: Erik Zigo Date: Sat, 25 Jan 2025 12:01:03 +0100 Subject: [PATCH 01/10] [output-mapping] MappingFromProcessedConfiguration::getDeleteWhere() --- .../MappingFromProcessedConfiguration.php | 17 +++++++++++ .../MappingFromProcessedConfigurationTest.php | 28 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/Mapping/MappingFromProcessedConfiguration.php b/src/Mapping/MappingFromProcessedConfiguration.php index 8839eb7..0e43617 100644 --- a/src/Mapping/MappingFromProcessedConfiguration.php +++ b/src/Mapping/MappingFromProcessedConfiguration.php @@ -208,4 +208,21 @@ public function getItemSourceType(): SourceType { return $this->source->getSourceType(); } + + /** + * @return MappingFromConfigurationDeleteWhere[]|null + */ + public function getDeleteWhere(): ?array + { + if (!isset($this->mapping['delete_where'])) { + return null; + } + + return array_map( + function (array $deleteWhere): MappingFromConfigurationDeleteWhere { + return new MappingFromConfigurationDeleteWhere($deleteWhere); + }, + $this->mapping['delete_where'], + ); + } } diff --git a/tests/Mapping/MappingFromProcessedConfigurationTest.php b/tests/Mapping/MappingFromProcessedConfigurationTest.php index b6676ff..01dc2c1 100644 --- a/tests/Mapping/MappingFromProcessedConfigurationTest.php +++ b/tests/Mapping/MappingFromProcessedConfigurationTest.php @@ -69,6 +69,7 @@ public function testBasic(): void self::assertFalse($mapping->isIncremental()); self::assertEquals(SourceType::WORKSPACE, $mapping->getItemSourceType()); self::assertInstanceOf(MappingDestination::class, $mapping->getDestination()); + self::assertNull($mapping->getDeleteWhere()); } public function testTableMetadata(): void @@ -277,4 +278,31 @@ public function testPrimaryKeyAndColumsAreConvertedToStrings(): void self::assertSame(['', '123', 'col1', 'col2'], $mapping->getColumns()); self::assertSame(['', '123', 'col1'], $mapping->getPrimaryKey()); } + + public function testGetDeleteWhere(): void + { + $mapping = [ + 'destination' => 'in.c-main.table', + 'delete_where' => [ + [ + 'changed_since' => '-7 days', + ], + [ + 'changed_until' => '-2 days', + ], + ], + ]; + + $mapping = new MappingFromProcessedConfiguration( + $mapping, + $this->createMock(MappingFromRawConfigurationAndPhysicalDataWithManifest::class), + ); + + $deleteWhere = $mapping->getDeleteWhere(); + self::assertNotNull($deleteWhere); + self::assertCount(2, $deleteWhere); + + self::assertSame('-7 days', $deleteWhere[0]->getChangedSince()); + self::assertSame('-2 days', $deleteWhere[1]->getChangedUntil()); + } } From 7fff2cc4462bc3b4af073d22f56c26f9b0370821 Mon Sep 17 00:00:00 2001 From: Erik Zigo Date: Sat, 25 Jan 2025 12:10:56 +0100 Subject: [PATCH 02/10] [output-mapping] DeleteTableRowsOptionsFactory --- src/Storage/DeleteTableRowsOptionsFactory.php | 20 ++++++++++++++ src/Storage/TableDataModifier.php | 23 +++++++++++----- .../DeleteTableRowsOptionsFactoryTest.php | 27 +++++++++++++++++++ 3 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 src/Storage/DeleteTableRowsOptionsFactory.php create mode 100644 tests/Storage/DeleteTableRowsOptionsFactoryTest.php diff --git a/src/Storage/DeleteTableRowsOptionsFactory.php b/src/Storage/DeleteTableRowsOptionsFactory.php new file mode 100644 index 0000000..af6bee5 --- /dev/null +++ b/src/Storage/DeleteTableRowsOptionsFactory.php @@ -0,0 +1,20 @@ + $column, + 'whereOperator' => $operator, + 'whereValues' => $values, + ]; + } +} diff --git a/src/Storage/TableDataModifier.php b/src/Storage/TableDataModifier.php index 18d2ac6..3fa96aa 100644 --- a/src/Storage/TableDataModifier.php +++ b/src/Storage/TableDataModifier.php @@ -19,13 +19,7 @@ public function __construct( public function updateTableData(MappingFromProcessedConfiguration $source, MappingDestination $destination): void { - if (!is_null($source->getDeleteWhereColumn())) { - // Delete rows - $deleteOptions = [ - 'whereColumn' => $source->getDeleteWhereColumn(), - 'whereOperator' => $source->getDeleteWhereOperator(), - 'whereValues' => $source->getDeleteWhereValues(), - ]; + foreach ($this->prepareDeleteOptionsList($source) as $deleteOptions) { try { $this->clientWrapper->getTableAndFileStorageClient()->deleteTableRows( $destination->getTableId(), @@ -44,4 +38,19 @@ public function updateTableData(MappingFromProcessedConfiguration $source, Mappi } } } + + private function prepareDeleteOptionsList(MappingFromProcessedConfiguration $source): array + { + if ($source->getDeleteWhereColumn() !== null) { + return [ + DeleteTableRowsOptionsFactory::createFromLegacyDeleteWhereColumn( + $source->getDeleteWhereColumn(), + $source->getDeleteWhereOperator(), + $source->getDeleteWhereValues(), + ), + ]; + } + + return []; + } } diff --git a/tests/Storage/DeleteTableRowsOptionsFactoryTest.php b/tests/Storage/DeleteTableRowsOptionsFactoryTest.php new file mode 100644 index 0000000..c1d05fa --- /dev/null +++ b/tests/Storage/DeleteTableRowsOptionsFactoryTest.php @@ -0,0 +1,27 @@ + 'test_column', + 'whereOperator' => 'eq', + 'whereValues' => ['value1', 'value2'], + ], + DeleteTableRowsOptionsFactory::createFromLegacyDeleteWhereColumn( + 'test_column', + 'eq', + ['value1', 'value2'], + ), + ); + } +} From 10a363f15e136b075a8ed7a539163ecec7717f77 Mon Sep 17 00:00:00 2001 From: Erik Zigo Date: Sat, 25 Jan 2025 12:15:23 +0100 Subject: [PATCH 03/10] DeleteTableRowsOptionsFactory::createFromDeleteWhere() --- src/Storage/DeleteTableRowsOptionsFactory.php | 55 +++++++ .../DeleteTableRowsOptionsFactoryTest.php | 134 ++++++++++++++++++ 2 files changed, 189 insertions(+) diff --git a/src/Storage/DeleteTableRowsOptionsFactory.php b/src/Storage/DeleteTableRowsOptionsFactory.php index af6bee5..385a95b 100644 --- a/src/Storage/DeleteTableRowsOptionsFactory.php +++ b/src/Storage/DeleteTableRowsOptionsFactory.php @@ -4,6 +4,10 @@ namespace Keboola\OutputMapping\Storage; +use Keboola\OutputMapping\Mapping\MappingFromConfigurationDeleteWhere; +use Keboola\OutputMapping\Mapping\MappingFromConfigurationDeleteWhereFilterFromSet; +use Keboola\OutputMapping\Mapping\MappingFromConfigurationDeleteWhereFilterFromWorkspace; + class DeleteTableRowsOptionsFactory { public static function createFromLegacyDeleteWhereColumn( @@ -17,4 +21,55 @@ public static function createFromLegacyDeleteWhereColumn( 'whereValues' => $values, ]; } + + public static function createFromDeleteWhere( + MappingFromConfigurationDeleteWhere $deleteWhere, + ): ?array { + $options = []; + + if ($deleteWhere->getChangedSince()) { + $options['changedSince'] = $deleteWhere->getChangedSince(); + } + + if ($deleteWhere->getChangedUntil()) { + $options['changedUntil'] = $deleteWhere->getChangedUntil(); + } + + $whereFilters = self::createWhereFilters($deleteWhere); + if ($whereFilters !== []) { + $options['whereFilters'] = $whereFilters; + } + + return $options === [] ? null : $options; + } + + private static function createWhereFilters(MappingFromConfigurationDeleteWhere $deleteWhere): array + { + if (!$deleteWhere->getWhereFilters()) { + return []; + } + + $whereFilters = []; + foreach ($deleteWhere->getWhereFilters() as $deleteFilter) { + $whereFilter = [ + 'column' => $deleteFilter->getColumn(), + 'operator' => $deleteFilter->getOperator(), + ]; + + if ($deleteFilter instanceof MappingFromConfigurationDeleteWhereFilterFromSet) { + $whereFilter['values'] = $deleteFilter->getValues(); + $whereFilters[] = $whereFilter; + } + if ($deleteFilter instanceof MappingFromConfigurationDeleteWhereFilterFromWorkspace) { + $whereFilter['valuesByTableInWorkspace'] = [ + 'workspaceId' => $deleteFilter->getWorkspaceId(), + 'table' => $deleteFilter->getWorkspaceTable(), + 'column' => $deleteFilter->getWorkspaceColumn(), + ]; + $whereFilters[] = $whereFilter; + } + } + + return $whereFilters; + } } diff --git a/tests/Storage/DeleteTableRowsOptionsFactoryTest.php b/tests/Storage/DeleteTableRowsOptionsFactoryTest.php index c1d05fa..718fa91 100644 --- a/tests/Storage/DeleteTableRowsOptionsFactoryTest.php +++ b/tests/Storage/DeleteTableRowsOptionsFactoryTest.php @@ -4,6 +4,8 @@ namespace Keboola\OutputMapping\Tests\Storage; +use Generator; +use Keboola\OutputMapping\Mapping\MappingFromConfigurationDeleteWhere; use Keboola\OutputMapping\Storage\DeleteTableRowsOptionsFactory; use PHPUnit\Framework\TestCase; @@ -24,4 +26,136 @@ public function testCreateFromLegacyDeleteWhereColumn(): void ), ); } + + /** + * @dataProvider provideCreateFromDeleteWhereData + */ + public function testCreateFromDeleteWhere( + array $mapping, + ?array $expectedResult, + ): void { + $deleteWhere = new MappingFromConfigurationDeleteWhere($mapping); + $result = DeleteTableRowsOptionsFactory::createFromDeleteWhere($deleteWhere); + + $this->assertEquals($expectedResult, $result); + } + + public static function provideCreateFromDeleteWhereData(): Generator + { + yield 'empty mapping returns null' => [ + 'mapping' => [], + 'expectedResult' => null, + ]; + + yield 'mapping with changed_since only' => [ + 'mapping' => [ + 'changed_since' => '2024-01-01', + ], + 'expectedResult' => [ + 'changedSince' => '2024-01-01', + ], + ]; + + yield 'mapping with changed_until only' => [ + 'mapping' => [ + 'changed_until' => '2024-01-02', + ], + 'expectedResult' => [ + 'changedUntil' => '2024-01-02', + ], + ]; + + yield 'mapping with values_from_set filter' => [ + 'mapping' => [ + 'where_filters' => [ + [ + 'column' => 'test_column', + 'operator' => 'eq', + 'values_from_set' => ['value1', 'value2'], + ], + ], + ], + 'expectedResult' => [ + 'whereFilters' => [ + [ + 'column' => 'test_column', + 'operator' => 'eq', + 'values' => ['value1', 'value2'], + ], + ], + ], + ]; + + yield 'mapping with values_from_workspace filter' => [ + 'mapping' => [ + 'where_filters' => [ + [ + 'column' => 'test_column', + 'operator' => 'ne', + 'values_from_workspace' => [ + 'workspace_id' => 'workspace123', + 'table' => 'table1', + 'column' => 'column1', + ], + ], + ], + ], + 'expectedResult' => [ + 'whereFilters' => [ + [ + 'column' => 'test_column', + 'operator' => 'ne', + 'valuesByTableInWorkspace' => [ + 'workspaceId' => 'workspace123', + 'table' => 'table1', + 'column' => 'column1', + ], + ], + ], + ], + ]; + + yield 'mapping with multiple filters and dates' => [ + 'mapping' => [ + 'changed_since' => '2024-01-01', + 'changed_until' => '2024-01-02', + 'where_filters' => [ + [ + 'column' => 'test_column', + 'operator' => 'eq', + 'values_from_set' => ['value1', 'value2'], + ], + [ + 'column' => 'another_column', + 'operator' => 'ne', + 'values_from_workspace' => [ + 'workspace_id' => 'workspace123', + 'table' => 'table1', + 'column' => 'column1', + ], + ], + ], + ], + 'expectedResult' => [ + 'changedSince' => '2024-01-01', + 'changedUntil' => '2024-01-02', + 'whereFilters' => [ + [ + 'column' => 'test_column', + 'operator' => 'eq', + 'values' => ['value1', 'value2'], + ], + [ + 'column' => 'another_column', + 'operator' => 'ne', + 'valuesByTableInWorkspace' => [ + 'workspaceId' => 'workspace123', + 'table' => 'table1', + 'column' => 'column1', + ], + ], + ], + ], + ]; + } } From 66b80ae17cd27b3a2431954c85abe7bb3bfbe7cb Mon Sep 17 00:00:00 2001 From: Erik Zigo Date: Sat, 25 Jan 2025 12:25:24 +0100 Subject: [PATCH 04/10] [output-mapping] TableConfigurationResolver backfills workspaceId if needed --- src/Writer/Helper/DeleteWhereHelper.php | 36 ++++ .../Table/TableConfigurationResolver.php | 17 ++ tests/Writer/Helper/DeleteWhereHelperTest.php | 173 ++++++++++++++++++ .../Table/TableConfigurationResolverTest.php | 115 ++++++++++++ 4 files changed, 341 insertions(+) create mode 100644 src/Writer/Helper/DeleteWhereHelper.php create mode 100644 tests/Writer/Helper/DeleteWhereHelperTest.php diff --git a/src/Writer/Helper/DeleteWhereHelper.php b/src/Writer/Helper/DeleteWhereHelper.php new file mode 100644 index 0000000..e8f63d1 --- /dev/null +++ b/src/Writer/Helper/DeleteWhereHelper.php @@ -0,0 +1,36 @@ +logger, $config['primary_key']); } @@ -46,6 +49,20 @@ public function resolveTableConfiguration( ); } + if (isset($config['delete_where'])) { + if ($source->getSourceType() === SourceType::WORKSPACE) { + $config['delete_where'] = array_map( + function (array $deleteWhere) use ($source): array { + return DeleteWhereHelper::addWorkspaceIdToValuesFromWorkspaceIfMissing( + $deleteWhere, + $source->getWorkspaceId(), + ); + }, + $config['delete_where'], + ); + } + } + if ($configuration->hasTagStagingFilesFeature()) { $config = TagsHelper::addSystemTags($config, $systemMetadata); } diff --git a/tests/Writer/Helper/DeleteWhereHelperTest.php b/tests/Writer/Helper/DeleteWhereHelperTest.php new file mode 100644 index 0000000..66b39f7 --- /dev/null +++ b/tests/Writer/Helper/DeleteWhereHelperTest.php @@ -0,0 +1,173 @@ + [ + 'deleteWhere' => [], + 'expectedResult' => [], + ]; + + yield 'no where_filters' => [ + 'deleteWhere' => [ + 'changed_since' => '2021-01-01', + ], + 'expectedResult' => [ + 'changed_since' => '2021-01-01', + ], + ]; + + yield 'invalid where_filters' => [ + 'deleteWhere' => [ + 'where_filters' => 'invalid', + ], + 'expectedResult' => [ + 'where_filters' => 'invalid', + ], + ]; + + yield 'empty where_filters' => [ + 'deleteWhere' => [ + 'where_filters' => [], + ], + 'expectedResult' => [ + 'where_filters' => [], + ], + ]; + + yield 'where_filters with values_from_set' => [ + 'deleteWhere' => [ + 'where_filters' => [ + [ + 'column' => 'id', + 'operator' => 'eq', + 'values_from_set' => ['123'], + ], + ], + ], + 'expectedResult' => [ + 'where_filters' => [ + [ + 'column' => 'id', + 'operator' => 'eq', + 'values_from_set' => ['123'], + ], + ], + ], + ]; + + yield 'values_from_workspace with existing workspace' => [ + 'deleteWhere' => [ + 'where_filters' => [ + [ + 'column' => 'id', + 'operator' => 'eq', + 'values_from_workspace' => [ + 'workspace_id' => 'existing-workspace', + 'table' => 'tableName', + ], + ], + ], + ], + 'expectedResult' => [ + 'where_filters' => [ + [ + 'column' => 'id', + 'operator' => 'eq', + 'values_from_workspace' => [ + 'workspace_id' => 'existing-workspace', + 'table' => 'tableName', + ], + ], + ], + ], + ]; + + yield 'values_from_workspace without workspace_id' => [ + 'deleteWhere' => [ + 'where_filters' => [ + [ + 'column' => 'id', + 'operator' => 'eq', + 'values_from_workspace' => [ + 'table' => 'tableName', + ], + ], + ], + ], + 'expectedResult' => [ + 'where_filters' => [ + [ + 'column' => 'id', + 'operator' => 'eq', + 'values_from_workspace' => [ + 'table' => 'tableName', + 'workspace_id' => '123', + ], + ], + ], + ], + ]; + + yield 'mixed where_filters' => [ + 'deleteWhere' => [ + 'where_filters' => [ + [ + 'column' => 'id', + 'operator' => 'eq', + 'values_from_workspace' => [ + 'workspace_id' => 'existing-workspace', + 'table' => 'tableName', + ], + ], + [ + 'column' => 'id', + 'operator' => 'eq', + 'values_from_workspace' => [ + 'table' => 'tableName', + ], + ], + ], + ], + 'expectedResult' => [ + 'where_filters' => [ + [ + 'column' => 'id', + 'operator' => 'eq', + 'values_from_workspace' => [ + 'workspace_id' => 'existing-workspace', + 'table' => 'tableName', + ], + ], + [ + 'column' => 'id', + 'operator' => 'eq', + 'values_from_workspace' => [ + 'table' => 'tableName', + 'workspace_id' => '123', + ], + ], + ], + ], + ]; + } + + /** + * @dataProvider provideDeleteWhereMappings + */ + public function testAddWorkspaceIdToValuesFromWorkspaceIfMissing(array $deleteWhere, array $expectedResult): void + { + $result = DeleteWhereHelper::addWorkspaceIdToValuesFromWorkspaceIfMissing($deleteWhere, '123'); + + self::assertSame($expectedResult, $result); + } +} diff --git a/tests/Writer/Table/TableConfigurationResolverTest.php b/tests/Writer/Table/TableConfigurationResolverTest.php index 99e9ada..bac99d7 100644 --- a/tests/Writer/Table/TableConfigurationResolverTest.php +++ b/tests/Writer/Table/TableConfigurationResolverTest.php @@ -8,6 +8,7 @@ use Keboola\OutputMapping\Mapping\MappingFromRawConfigurationAndPhysicalDataWithManifest; use Keboola\OutputMapping\OutputMappingSettings; use Keboola\OutputMapping\SystemMetadata; +use Keboola\OutputMapping\Writer\Table\Source\SourceType; use Keboola\OutputMapping\Writer\Table\TableConfigurationResolver; use Monolog\Handler\TestHandler; use Monolog\Logger; @@ -430,4 +431,118 @@ public function testErrorDestinationInManifestWithoutBucketAndWithoutDefaultBuck $systemMetadata, ); } + + public function testConfigurationAddWorkspaceIdIfSourceIsWorkspaceType(): void + { + + $configuration = $this->createMock(OutputMappingSettings::class); + $configuration->expects(self::once()) + ->method('getDefaultBucket') + ->willReturn('in.c-main') + ; + $configuration->expects(self::once()) + ->method('hasTagStagingFilesFeature') + ->willReturn(false) + ; + + $source = $this->createMock(MappingFromRawConfigurationAndPhysicalDataWithManifest::class); + $source->expects(self::once()) + ->method('getSourceType') + ->willReturn(SourceType::WORKSPACE) + ; + $source->expects(self::once()) + ->method('getWorkspaceId') + ->willReturn('123') + ; + + $systemMetadata = new SystemMetadata([ + 'componentId' => 'keboola.ex-db-snowflake', + 'configurationId' => '123', + 'configurationRowId' => '456', + ]); + + $mappingFromManifest = [ + 'destination' => 'in.c-main.table1', + 'source' => 'in.c-main.table1', + ]; + + $mappingFromConfiguration = [ + 'delete_where' => [ + [ + 'where_filters' => [ + [ + 'column' => 'id', + 'operator' => 'eq', + 'values_from_workspace' => [ + 'table' => 'table1', + 'column' => 'column1', + ], + ], + [ + 'column' => 'city', + 'operator' => 'eq', + 'values_from_workspace' => [ + 'workspace_id' => '456', + 'table' => 'table2', + 'column' => 'column1', + ], + ], + ], + ], + ], + ]; + + $resolver = new TableConfigurationResolver($this->logger); + $config = $resolver->resolveTableConfiguration( + $configuration, + $source, + $mappingFromManifest, + $mappingFromConfiguration, + $systemMetadata, + ); + + $expectedConfig = [ + 'destination' => 'in.c-main.table1', + 'source' => 'in.c-main.table1', + 'incremental' => false, + 'primary_key' => [], + 'columns' => [], + 'distribution_key' => [], + 'delete_where_values' => [], + 'delete_where_operator' => 'eq', + 'delimiter' => ',', + 'enclosure' => '"', + 'metadata' => [], + 'column_metadata' => [], + 'write_always' => false, + 'tags' => [], + 'schema' => [], + 'delete_where' => [ + [ + 'where_filters' => [ + [ + 'column' => 'id', + 'operator' => 'eq', + 'values_from_workspace' => [ + 'table' => 'table1', + 'column' => 'column1', + 'workspace_id' => '123', + ], + ], + [ + 'column' => 'city', + 'operator' => 'eq', + 'values_from_workspace' => [ + 'workspace_id' => '456', + 'table' => 'table2', + 'column' => 'column1', + ], + ], + ], + ], + ], + ]; + + self::assertEquals($expectedConfig, $config); + } } From 5a698d7ed92c6e3bb7cd331734b1214d41e6715f Mon Sep 17 00:00:00 2001 From: Erik Zigo Date: Sat, 25 Jan 2025 12:33:46 +0100 Subject: [PATCH 05/10] [output-mapping] TableDataModifier supports row delete by delete_where mapping --- src/Storage/TableDataModifier.php | 11 +++ tests/Storage/TableDataModifierTest.php | 95 +++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/src/Storage/TableDataModifier.php b/src/Storage/TableDataModifier.php index 3fa96aa..2c0d1e8 100644 --- a/src/Storage/TableDataModifier.php +++ b/src/Storage/TableDataModifier.php @@ -5,6 +5,7 @@ namespace Keboola\OutputMapping\Storage; use Keboola\OutputMapping\Exception\InvalidOutputException; +use Keboola\OutputMapping\Mapping\MappingFromConfigurationDeleteWhere; use Keboola\OutputMapping\Mapping\MappingFromProcessedConfiguration; use Keboola\OutputMapping\Writer\Table\MappingDestination; use Keboola\StorageApi\ClientException; @@ -41,6 +42,16 @@ public function updateTableData(MappingFromProcessedConfiguration $source, Mappi private function prepareDeleteOptionsList(MappingFromProcessedConfiguration $source): array { + if ($source->getDeleteWhere() !== null) { + return array_filter( + array_map( + function (MappingFromConfigurationDeleteWhere $deleteWhere) { + return DeleteTableRowsOptionsFactory::createFromDeleteWhere($deleteWhere); + }, + $source->getDeleteWhere(), + ), + ); + } if ($source->getDeleteWhereColumn() !== null) { return [ DeleteTableRowsOptionsFactory::createFromLegacyDeleteWhereColumn( diff --git a/tests/Storage/TableDataModifierTest.php b/tests/Storage/TableDataModifierTest.php index f511e11..00d391d 100644 --- a/tests/Storage/TableDataModifierTest.php +++ b/tests/Storage/TableDataModifierTest.php @@ -4,7 +4,9 @@ namespace Keboola\OutputMapping\Tests\Storage; +use Generator; use Keboola\OutputMapping\Exception\InvalidOutputException; +use Keboola\OutputMapping\Mapping\MappingFromConfigurationDeleteWhere; use Keboola\OutputMapping\Mapping\MappingFromProcessedConfiguration; use Keboola\OutputMapping\Storage\TableDataModifier; use Keboola\OutputMapping\Tests\AbstractTestCase; @@ -31,6 +33,94 @@ public function testDeleteTableRows(): void $this->assertEquals(1, $newTable['rowsCount']); } + + public static function provideDeleteTableRowsFromDeleteWhereConfig(): Generator + { + yield 'single delete where' => [ + 'deleteWhere' => [ + // single tableRowsDelete job + new MappingFromConfigurationDeleteWhere([ + 'where_filters' => [ + [ + 'column' => 'Id', + 'operator' => 'eq', + 'values_from_set' => ['id1', 'id2'], + ], + ], + ]), + ], + 'expectedRowsCount' => 1, + ]; + + yield 'multiple delete where' => [ + 'deleteWhere' => [ + // multiple tableRowsDelete jobs + new MappingFromConfigurationDeleteWhere([ + 'where_filters' => [ + [ + 'column' => 'Id', + 'operator' => 'eq', + 'values_from_set' => ['id1'], + ], + ], + ]), + new MappingFromConfigurationDeleteWhere([ + 'where_filters' => [ + [ + 'column' => 'Id', + 'operator' => 'eq', + 'values_from_set' => ['id2'], + ], + ], + ]), + ], + 'expectedRowsCount' => 1, + ]; + + yield 'multiple where_filters' => [ + 'deleteWhere' => [ + // single tableRowsDelete - multiple conditions + new MappingFromConfigurationDeleteWhere([ + 'where_filters' => [ + [ + 'column' => 'Id', + 'operator' => 'eq', + 'values_from_set' => ['id1'], + ], + [ + 'column' => 'Id', + 'operator' => 'eq', + 'values_from_set' => ['id2'], + ], + ], + ]), + ], + 'expectedRowsCount' => 3, // Condition Id IN('id1') AND Id IN('id2') will never be true + ]; + } + + /** + * @dataProvider provideDeleteTableRowsFromDeleteWhereConfig + */ + #[NeedsTestTables(count: 1)] + public function testDeleteTableRowsFromDeleteWhereConfig(array $deleteWhere, int $expectedRowsCount): void + { + $tableDataModifier = new TableDataModifier($this->clientWrapper); + + $destination = new MappingDestination($this->firstTableId); + + $source = $this->createMock(MappingFromProcessedConfiguration::class); + $source->method('getDeleteWhere')->willReturn($deleteWhere); + + $tableDataModifier->updateTableData($source, $destination); + + $newTable = $this->clientWrapper->getTableAndFileStorageClient()->getTable($this->firstTableId); + + $this->assertEquals($expectedRowsCount, $newTable['rowsCount']); + + //@TODO: Validate also Storage job params and results + } + #[NeedsTestTables(count: 1)] public function testDeleteTableRowsWithUnexistColumn(): void { @@ -70,4 +160,9 @@ public function testWhereColumnNotSet(): void $this->assertEquals(3, $newTable['rowsCount']); } + + public function testaDeleteTableRowsFromDeleteWhereConfigWithWorkspace(): void + { + $this->markTestIncomplete('Not implemented yet on Storage API side.'); + } } From 49a78be26ba88179f3f21624deaf387010353ca2 Mon Sep 17 00:00:00 2001 From: Erik Zigo Date: Sat, 25 Jan 2025 13:55:55 +0100 Subject: [PATCH 06/10] [output-mapping] DeleteTableRowsOptionsFactory uses column from filter if needed --- ...gurationDeleteWhereFilterFromWorkspace.php | 4 +- src/Storage/DeleteTableRowsOptionsFactory.php | 2 +- ...tionDeleteWhereFilterFromWorkspaceTest.php | 50 ++++++++++++++++--- .../DeleteTableRowsOptionsFactoryTest.php | 28 +++++++++++ 4 files changed, 73 insertions(+), 11 deletions(-) diff --git a/src/Mapping/MappingFromConfigurationDeleteWhereFilterFromWorkspace.php b/src/Mapping/MappingFromConfigurationDeleteWhereFilterFromWorkspace.php index a8c5526..4cd6036 100644 --- a/src/Mapping/MappingFromConfigurationDeleteWhereFilterFromWorkspace.php +++ b/src/Mapping/MappingFromConfigurationDeleteWhereFilterFromWorkspace.php @@ -16,8 +16,8 @@ public function getWorkspaceTable(): string return $this->mapping['values_from_workspace']['table']; } - public function getWorkspaceColumn(): string + public function getWorkspaceColumn(): ?string { - return $this->mapping['values_from_workspace']['column']; + return $this->mapping['values_from_workspace']['column'] ?? null; } } diff --git a/src/Storage/DeleteTableRowsOptionsFactory.php b/src/Storage/DeleteTableRowsOptionsFactory.php index 385a95b..1b06523 100644 --- a/src/Storage/DeleteTableRowsOptionsFactory.php +++ b/src/Storage/DeleteTableRowsOptionsFactory.php @@ -64,7 +64,7 @@ private static function createWhereFilters(MappingFromConfigurationDeleteWhere $ $whereFilter['valuesByTableInWorkspace'] = [ 'workspaceId' => $deleteFilter->getWorkspaceId(), 'table' => $deleteFilter->getWorkspaceTable(), - 'column' => $deleteFilter->getWorkspaceColumn(), + 'column' => $deleteFilter->getWorkspaceColumn() ?: $deleteFilter->getColumn(), ]; $whereFilters[] = $whereFilter; } diff --git a/tests/Mapping/MappingFromConfigurationDeleteWhereFilterFromWorkspaceTest.php b/tests/Mapping/MappingFromConfigurationDeleteWhereFilterFromWorkspaceTest.php index 6f34a77..d134b5f 100644 --- a/tests/Mapping/MappingFromConfigurationDeleteWhereFilterFromWorkspaceTest.php +++ b/tests/Mapping/MappingFromConfigurationDeleteWhereFilterFromWorkspaceTest.php @@ -4,14 +4,33 @@ namespace Keboola\OutputMapping\Tests\Mapping; +use Generator; use Keboola\OutputMapping\Mapping\MappingFromConfigurationDeleteWhereFilterFromWorkspace; use PHPUnit\Framework\TestCase; class MappingFromConfigurationDeleteWhereFilterFromWorkspaceTest extends TestCase { - public function testGetters(): void + public static function configurationProvider(): Generator { - $whereFilterFromSet = new MappingFromConfigurationDeleteWhereFilterFromWorkspace( + yield 'minimal configuration' => [ + [ + 'column' => 'columnName', + 'operator' => 'ne', + 'values_from_workspace' => [ + 'workspace_id' => '123', + 'table' => 'workspaceTable', + ], + ], + [ + 'column' => 'columnName', + 'operator' => 'ne', + 'workspace_id' => '123', + 'table' => 'workspaceTable', + 'workspace_column' => null, + ], + ]; + + yield 'full configuration' => [ [ 'column' => 'columnName', 'operator' => 'ne', @@ -21,12 +40,27 @@ public function testGetters(): void 'column' => 'workspaceColumn', ], ], - ); + [ + 'column' => 'columnName', + 'operator' => 'ne', + 'workspace_id' => '123', + 'table' => 'workspaceTable', + 'workspace_column' => 'workspaceColumn', + ], + ]; + } + + /** + * @dataProvider configurationProvider + */ + public function testGetters(array $config, array $expected): void + { + $whereFilterFromSet = new MappingFromConfigurationDeleteWhereFilterFromWorkspace($config); - self::assertSame('columnName', $whereFilterFromSet->getColumn()); - self::assertSame('ne', $whereFilterFromSet->getOperator()); - self::assertSame('123', $whereFilterFromSet->getWorkspaceId()); - self::assertSame('workspaceTable', $whereFilterFromSet->getWorkspaceTable()); - self::assertSame('workspaceColumn', $whereFilterFromSet->getWorkspaceColumn()); + self::assertSame($expected['column'], $whereFilterFromSet->getColumn()); + self::assertSame($expected['operator'], $whereFilterFromSet->getOperator()); + self::assertSame($expected['workspace_id'], $whereFilterFromSet->getWorkspaceId()); + self::assertSame($expected['table'], $whereFilterFromSet->getWorkspaceTable()); + self::assertSame($expected['workspace_column'], $whereFilterFromSet->getWorkspaceColumn()); } } diff --git a/tests/Storage/DeleteTableRowsOptionsFactoryTest.php b/tests/Storage/DeleteTableRowsOptionsFactoryTest.php index 718fa91..b3a854d 100644 --- a/tests/Storage/DeleteTableRowsOptionsFactoryTest.php +++ b/tests/Storage/DeleteTableRowsOptionsFactoryTest.php @@ -115,6 +115,34 @@ public static function provideCreateFromDeleteWhereData(): Generator ], ]; + yield 'mapping with values_from_workspace filter - use column from filter' => [ + 'mapping' => [ + 'where_filters' => [ + [ + 'column' => 'test_column', + 'operator' => 'ne', + 'values_from_workspace' => [ + 'workspace_id' => 'workspace123', + 'table' => 'table1', + ], + ], + ], + ], + 'expectedResult' => [ + 'whereFilters' => [ + [ + 'column' => 'test_column', + 'operator' => 'ne', + 'valuesByTableInWorkspace' => [ + 'workspaceId' => 'workspace123', + 'table' => 'table1', + 'column' => 'test_column', + ], + ], + ], + ], + ]; + yield 'mapping with multiple filters and dates' => [ 'mapping' => [ 'changed_since' => '2024-01-01', From 47d797d11915ec49ed684fb4640f0e5d498afe1b Mon Sep 17 00:00:00 2001 From: Erik Zigo Date: Tue, 28 Jan 2025 14:33:55 +0100 Subject: [PATCH 07/10] [output-mapping] testDeleteTableRowsFromDeleteWhereConfig validates jobs params and results --- tests/Storage/TableDataModifierTest.php | 124 +++++++++++++++++++++++- 1 file changed, 121 insertions(+), 3 deletions(-) diff --git a/tests/Storage/TableDataModifierTest.php b/tests/Storage/TableDataModifierTest.php index 00d391d..4a82e72 100644 --- a/tests/Storage/TableDataModifierTest.php +++ b/tests/Storage/TableDataModifierTest.php @@ -50,6 +50,28 @@ public static function provideDeleteTableRowsFromDeleteWhereConfig(): Generator ]), ], 'expectedRowsCount' => 1, + 'expectedJobsProperties' => [ + 'job1' => [ + 'operationParams' => [ + 'queue' => 'main', + 'request' => [ + 'changedSince' => null, + 'changedUntil' => null, + 'whereFilters' => [ + [ + 'column' => 'Id', + 'values' => ['id1', 'id2'], + 'operator' => 'eq', + ], + ], + ], + 'backendConfiguration' => [], + ], + 'results' => [ + 'deletedRows' => null, //2, (bug in Storage API) + ], + ], + ], ]; yield 'multiple delete where' => [ @@ -75,6 +97,48 @@ public static function provideDeleteTableRowsFromDeleteWhereConfig(): Generator ]), ], 'expectedRowsCount' => 1, + 'expectedJobsProperties' => [ + 'job2' => [ + 'operationParams' => [ + 'queue' => 'main', + 'request' => [ + 'changedSince' => null, + 'changedUntil' => null, + 'whereFilters' => [ + [ + 'column' => 'Id', + 'values' => ['id2'], + 'operator' => 'eq', + ], + ], + ], + 'backendConfiguration' => [], + ], + 'results' => [ + 'deletedRows' => null, //1, (bug in Storage API) + ], + ], + 'job1' => [ + 'operationParams' => [ + 'queue' => 'main', + 'request' => [ + 'changedSince' => null, + 'changedUntil' => null, + 'whereFilters' => [ + [ + 'column' => 'Id', + 'values' => ['id1'], + 'operator' => 'eq', + ], + ], + ], + 'backendConfiguration' => [], + ], + 'results' => [ + 'deletedRows' => null, //1, (bug in Storage API) + ], + ], + ], ]; yield 'multiple where_filters' => [ @@ -96,6 +160,33 @@ public static function provideDeleteTableRowsFromDeleteWhereConfig(): Generator ]), ], 'expectedRowsCount' => 3, // Condition Id IN('id1') AND Id IN('id2') will never be true + 'expectedJobsProperties' => [ + 'job1' => [ + 'operationParams' => [ + 'queue' => 'main', + 'request' => [ + 'changedSince' => null, + 'changedUntil' => null, + 'whereFilters' => [ + [ + 'column' => 'Id', + 'values' => ['id1'], + 'operator' => 'eq', + ], + [ + 'column' => 'Id', + 'values' => ['id2'], + 'operator' => 'eq', + ], + ], + ], + 'backendConfiguration' => [], + ], + 'results' => [ + 'deletedRows' => null, //0, (bug in Storage API) + ], + ], + ], ]; } @@ -103,8 +194,11 @@ public static function provideDeleteTableRowsFromDeleteWhereConfig(): Generator * @dataProvider provideDeleteTableRowsFromDeleteWhereConfig */ #[NeedsTestTables(count: 1)] - public function testDeleteTableRowsFromDeleteWhereConfig(array $deleteWhere, int $expectedRowsCount): void - { + public function testDeleteTableRowsFromDeleteWhereConfig( + array $deleteWhere, + int $expectedRowsCount, + array $expectedJobsProperties, + ): void { $tableDataModifier = new TableDataModifier($this->clientWrapper); $destination = new MappingDestination($this->firstTableId); @@ -112,13 +206,29 @@ public function testDeleteTableRowsFromDeleteWhereConfig(array $deleteWhere, int $source = $this->createMock(MappingFromProcessedConfiguration::class); $source->method('getDeleteWhere')->willReturn($deleteWhere); + $runId = $this->clientWrapper->getBasicClient()->generateRunId(); + $this->clientWrapper->getTableAndFileStorageClient()->setRunId($runId); + $tableDataModifier->updateTableData($source, $destination); $newTable = $this->clientWrapper->getTableAndFileStorageClient()->getTable($this->firstTableId); $this->assertEquals($expectedRowsCount, $newTable['rowsCount']); - //@TODO: Validate also Storage job params and results + $jobs = array_values( + array_filter( + $this->clientWrapper->getBasicClient()->listJobs(), + function (array $job) use ($runId): bool { + return $job['runId'] === $runId; + }, + ), + ); + + self::assertCount(count($expectedJobsProperties), $jobs); + foreach (array_values($expectedJobsProperties) as $key => $expectedJobProperties) { + self::assertArrayHasKey($key, $jobs); + self::assertJobParams($expectedJobProperties, $jobs[$key]); + } } #[NeedsTestTables(count: 1)] @@ -165,4 +275,12 @@ public function testaDeleteTableRowsFromDeleteWhereConfigWithWorkspace(): void { $this->markTestIncomplete('Not implemented yet on Storage API side.'); } + + private static function assertJobParams(array $expectedJobParams, array $actualJobParams): void + { + foreach ($expectedJobParams as $jobParam => $jobParamValues) { + self::assertArrayHasKey($jobParam, $actualJobParams); + self::assertSame($jobParamValues, $actualJobParams[$jobParam]); + } + } } From 3abd87d82c43eccee1dc8439742e14d4ac87c821 Mon Sep 17 00:00:00 2001 From: Erik Zigo Date: Tue, 28 Jan 2025 16:51:27 +0100 Subject: [PATCH 08/10] [output-mapping] TestSatisfyer fixup: reduce table default count created by NeedsTestTables --- tests/Needs/TestSatisfyer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Needs/TestSatisfyer.php b/tests/Needs/TestSatisfyer.php index 1302773..d7530da 100644 --- a/tests/Needs/TestSatisfyer.php +++ b/tests/Needs/TestSatisfyer.php @@ -238,7 +238,7 @@ public static function satisfyTestNeeds( // Create table $propNames = ['firstTableId', 'secondTableId', 'thirdTableId']; - for ($i = 0; $i < max($tableCount, count($propNames)); $i++) { + for ($i = 0; $i < $tableCount; $i++) { $tableIds[$i] = $clientWrapper->getTableAndFileStorageClient()->createTableAsync( $testBucketId, 'test' . ($i + 1), @@ -254,7 +254,7 @@ public static function satisfyTestNeeds( // Create table $propNames = ['firstTableId', 'secondTableId', 'thirdTableId']; - for ($i = 0; $i < max($tableCount, count($propNames)); $i++) { + for ($i = 0; $i < $tableCount; $i++) { $tableName = 'test' . ($i + 1); $tableId = $clientWrapper->getTableAndFileStorageClient()->createTableDefinition( $testBucketId, From d2dedc1cd18dc7de43294b98cd93dc3ef600dfec Mon Sep 17 00:00:00 2001 From: Erik Zigo Date: Tue, 28 Jan 2025 16:58:58 +0100 Subject: [PATCH 09/10] [output-mapping] TableDataModifierTest fixups after Storage API release --- tests/Storage/TableDataModifierTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Storage/TableDataModifierTest.php b/tests/Storage/TableDataModifierTest.php index 4a82e72..3c3de69 100644 --- a/tests/Storage/TableDataModifierTest.php +++ b/tests/Storage/TableDataModifierTest.php @@ -68,7 +68,7 @@ public static function provideDeleteTableRowsFromDeleteWhereConfig(): Generator 'backendConfiguration' => [], ], 'results' => [ - 'deletedRows' => null, //2, (bug in Storage API) + 'deletedRows' => 2, ], ], ], @@ -115,7 +115,7 @@ public static function provideDeleteTableRowsFromDeleteWhereConfig(): Generator 'backendConfiguration' => [], ], 'results' => [ - 'deletedRows' => null, //1, (bug in Storage API) + 'deletedRows' => 1, ], ], 'job1' => [ @@ -135,7 +135,7 @@ public static function provideDeleteTableRowsFromDeleteWhereConfig(): Generator 'backendConfiguration' => [], ], 'results' => [ - 'deletedRows' => null, //1, (bug in Storage API) + 'deletedRows' => 1, ], ], ], @@ -183,7 +183,7 @@ public static function provideDeleteTableRowsFromDeleteWhereConfig(): Generator 'backendConfiguration' => [], ], 'results' => [ - 'deletedRows' => null, //0, (bug in Storage API) + 'deletedRows' => 0, ], ], ], @@ -245,7 +245,7 @@ public function testDeleteTableRowsWithUnexistColumn(): void $expectedMessage = 'Cannot delete rows ' . 'from table "in.c-TableDataModifierTest_testDeleteTableRowsWithUnexistColumn.test1" ' . - 'in Storage: exceptions.storage.tables.columnNotExists'; + 'in Storage: Cannot filter by column "UnexistColumn", column does not exist'; $this->expectException(InvalidOutputException::class); $this->expectExceptionMessage($expectedMessage); From 328438bb6c2b6ca8f0dcb862f30096d0cc43ecc4 Mon Sep 17 00:00:00 2001 From: Erik Zigo Date: Tue, 28 Jan 2025 17:01:23 +0100 Subject: [PATCH 10/10] [output-mapping] testaDeleteTableRowsFromDeleteWhereConfigWithWorkspace --- tests/Storage/TableDataModifierTest.php | 169 +++++++++++++++++++++++- 1 file changed, 167 insertions(+), 2 deletions(-) diff --git a/tests/Storage/TableDataModifierTest.php b/tests/Storage/TableDataModifierTest.php index 3c3de69..ca2f9d7 100644 --- a/tests/Storage/TableDataModifierTest.php +++ b/tests/Storage/TableDataModifierTest.php @@ -5,6 +5,7 @@ namespace Keboola\OutputMapping\Tests\Storage; use Generator; +use Keboola\InputMapping\Staging\AbstractStrategyFactory; use Keboola\OutputMapping\Exception\InvalidOutputException; use Keboola\OutputMapping\Mapping\MappingFromConfigurationDeleteWhere; use Keboola\OutputMapping\Mapping\MappingFromProcessedConfiguration; @@ -12,6 +13,8 @@ use Keboola\OutputMapping\Tests\AbstractTestCase; use Keboola\OutputMapping\Tests\Needs\NeedsTestTables; use Keboola\OutputMapping\Writer\Table\MappingDestination; +use Keboola\OutputMapping\Writer\Table\Strategy\SqlWorkspaceTableStrategy; +use Keboola\StorageApi\Workspaces; class TableDataModifierTest extends AbstractTestCase { @@ -27,11 +30,55 @@ public function testDeleteTableRows(): void $source->method('getDeleteWhereOperator')->willReturn('eq'); $source->method('getDeleteWhereValues')->willReturn(['id1', 'id2']); + $runId = $this->clientWrapper->getBasicClient()->generateRunId(); + $this->clientWrapper->getTableAndFileStorageClient()->setRunId($runId); + $tableDataModifier->updateTableData($source, $destination); $newTable = $this->clientWrapper->getTableAndFileStorageClient()->getTable($this->firstTableId); $this->assertEquals(1, $newTable['rowsCount']); + + $jobs = array_values( + array_filter( + $this->clientWrapper->getBasicClient()->listJobs(), + function (array $job) use ($runId): bool { + return $job['runId'] === $runId; + }, + ), + ); + + self::assertCount(1, $jobs); + + $job = $jobs[0]; + + self::assertArrayHasKey('operationParams', $job); + self::assertSame( + [ + 'queue' => 'main', + 'request' => [ + 'changedSince' => null, + 'changedUntil' => null, + 'whereFilters' => [ + [ + 'column' => 'Id', + 'values' => ['id1', 'id2'], + 'operator' => 'eq', + ], + ], + ], + 'backendConfiguration' => [], + ], + $job['operationParams'], + ); + + self::assertArrayHasKey('results', $job); + self::assertSame( + [ + 'deletedRows' => 2, + ], + $job['results'], + ); } public static function provideDeleteTableRowsFromDeleteWhereConfig(): Generator @@ -271,9 +318,127 @@ public function testWhereColumnNotSet(): void $this->assertEquals(3, $newTable['rowsCount']); } - public function testaDeleteTableRowsFromDeleteWhereConfigWithWorkspace(): void + public static function provideDeleteTableRowsFromDeleteWhereConfigWithWorkspace(): Generator { - $this->markTestIncomplete('Not implemented yet on Storage API side.'); + yield 'normal situation' => [ + 'deleteFilterForTableInWorkspace' => [ + 'whereColumn' => 'Id', + 'whereOperator' => 'eq', + 'whereValues' => ['id2'], + ], + 'expectedRowsCount' => 1, + 'expectedDeletedRowsCount' => 2, + ]; + yield 'table in workspace is empty' => [ + 'deleteFilterForTableInWorkspace' => [ + 'whereColumn' => 'Id', + 'whereOperator' => 'eq', + 'whereValues' => ['id1', 'id2', 'id3'], + ], + 'expectedRowsCount' => 3, // no records will be deleted + 'expectedDeletedRowsCount' => 0, + ]; + } + + /** + * @dataProvider provideDeleteTableRowsFromDeleteWhereConfigWithWorkspace + */ + #[NeedsTestTables(count: 2)] + public function testaDeleteTableRowsFromDeleteWhereConfigWithWorkspace( + array $deleteFilterForTableInWorkspace, + int $expectedRowsCount, + int $expectedDeletedRowsCount, + ): void { + $workspace = $this->getWorkspaceStagingFactory()->getTableOutputStrategy( + AbstractStrategyFactory::WORKSPACE_SNOWFLAKE, + ); + self::assertInstanceOf(SqlWorkspaceTableStrategy::class, $workspace); + + $workspaces = new Workspaces($this->clientWrapper->getBasicClient()); + + $workspaceId = $workspace->getDataStorage()->getWorkspaceId(); + + $this->clientWrapper->getTableAndFileStorageClient()->deleteTableRows( + $this->secondTableId, + $deleteFilterForTableInWorkspace, + ); + + $workspaces->cloneIntoWorkspace((int) $workspaceId, [ + 'input' => [ + [ + 'source' => $this->secondTableId, + 'destination' => 'deleteFilter', + ], + ], + ]); + + $deleteWhere = [ + new MappingFromConfigurationDeleteWhere([ + 'where_filters' => [ + [ + 'column' => 'Id', + 'operator' => 'eq', + 'values_from_workspace' => [ + 'workspace_id' => $workspaceId, + 'table' => 'deleteFilter', + ], + ], + ], + ]), + ]; + + $tableDataModifier = new TableDataModifier($this->clientWrapper); + + $destination = new MappingDestination($this->firstTableId); + + $source = $this->createMock(MappingFromProcessedConfiguration::class); + $source->method('getDeleteWhere')->willReturn($deleteWhere); + + $runId = $this->clientWrapper->getBasicClient()->generateRunId(); + $this->clientWrapper->getTableAndFileStorageClient()->setRunId($runId); + + $tableDataModifier->updateTableData($source, $destination); + + $newTable = $this->clientWrapper->getTableAndFileStorageClient()->getTable($this->firstTableId); + $this->assertEquals($expectedRowsCount, $newTable['rowsCount']); + + $jobs = array_values( + array_filter( + $this->clientWrapper->getBasicClient()->listJobs(), + function (array $job) use ($runId): bool { + return $job['runId'] === $runId; + }, + ), + ); + + self::assertCount(1, $jobs); + self::assertJobParams( + [ + 'operationParams' => [ + 'queue' => 'main', + 'request' => [ + 'changedSince' => null, + 'changedUntil' => null, + 'whereFilters' => [ + [ + 'column' => 'Id', + 'operator' => 'eq', + 'valuesByTableInWorkspace' => [ + 'table' => 'deleteFilter', + 'column' => 'Id', + 'workspaceId' => (int) $workspaceId, + ], + ], + ], + ], + 'backendConfiguration' => [], + ], + 'results' => [ + 'deletedRows' => $expectedDeletedRowsCount, + ], + ], + $jobs[0], + ); } private static function assertJobParams(array $expectedJobParams, array $actualJobParams): void