114 lines
5.8 KiB
PHP
114 lines
5.8 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\EcommerceFinance;
|
|
|
|
use Mockery;
|
|
use Modules\EcommerceFinance\Contracts\BusinessMirrorDataset;
|
|
use Modules\EcommerceFinance\Repositories\ColumnarBusinessMirrorRepository;
|
|
use Modules\EcommerceFinance\Services\BusinessDataPlacement;
|
|
use Modules\EcommerceFinance\Services\BusinessMirrorSchemaManager;
|
|
use Modules\EcommerceFinance\Services\ClickHouseClient;
|
|
use Modules\EcommerceFinance\Services\ClickHouseSchemaManager;
|
|
use Tests\TestCase;
|
|
|
|
class ColumnarBusinessMirrorRepositoryTest extends TestCase
|
|
{
|
|
public function test_backfill_preserves_source_fields_and_adds_generation_metadata(): void
|
|
{
|
|
$dataset = $this->dataset();
|
|
$client = Mockery::mock(ClickHouseClient::class);
|
|
$client->shouldReceive('insertJsonEachRow')->once()->withArgs(
|
|
static function (string $table, array $columns, array $rows): bool {
|
|
return $table === 'fact_rows'
|
|
&& $columns === ['_mirror_key', 'Id', 'name', '_mirror_generation', '_mirror_synced_at', '_mirror_version']
|
|
&& $rows[0]['_mirror_key'] === 'key-1'
|
|
&& $rows[0]['Id'] === 1
|
|
&& $rows[0]['name'] === '记录一'
|
|
&& $rows[0]['_mirror_generation'] === '11111111-1111-4111-8111-111111111111'
|
|
&& is_int($rows[0]['_mirror_version']);
|
|
},
|
|
);
|
|
$repository = new ColumnarBusinessMirrorRepository($client);
|
|
|
|
$repository->storeBackfillBatch($dataset, [
|
|
['column_name' => 'Id', 'data_type' => 'int'],
|
|
['column_name' => 'name', 'data_type' => 'nvarchar'],
|
|
], [[
|
|
'_mirror_key' => 'key-1',
|
|
'Id' => 1,
|
|
'name' => '记录一',
|
|
'_mirror_synced_at' => '2026-09-07 12:00:00.000000',
|
|
]], '11111111-1111-4111-8111-111111111111');
|
|
}
|
|
|
|
public function test_columnar_schema_uses_replacing_merge_tree_and_sql_server_safe_types(): void
|
|
{
|
|
$dataset = $this->dataset();
|
|
$source = Mockery::mock(BusinessMirrorSchemaManager::class);
|
|
$source->shouldReceive('columns')->once()->andReturn([
|
|
['column_name' => 'Id', 'data_type' => 'int'],
|
|
['column_name' => 'flag', 'data_type' => 'tinyint'],
|
|
['column_name' => 'amount', 'data_type' => 'decimal', 'numeric_precision' => 20, 'numeric_scale' => 4],
|
|
['column_name' => 'updated_at', 'data_type' => 'datetime2'],
|
|
]);
|
|
$placements = Mockery::mock(BusinessDataPlacement::class);
|
|
$placements->shouldReceive('targetStore')->once()->with('source.schema.fact')->andReturn('clickhouse');
|
|
$client = Mockery::mock(ClickHouseClient::class);
|
|
$client->shouldReceive('identifier')->times(4)->andReturnUsing(static fn (string $name): string => '`' . $name . '`');
|
|
$client->shouldReceive('qualifiedTable')->once()->with('fact_rows')->andReturn('`analytics`.`fact_rows`');
|
|
$client->shouldReceive('scalar')->once()->withArgs(static fn (string $sql, array $parameters): bool =>
|
|
str_contains($sql, 'SELECT sorting_key FROM system.tables')
|
|
&& $parameters['database'] === 'caiwu_business_analytics'
|
|
&& $parameters['table'] === 'fact_rows'
|
|
)->andReturn('_mirror_generation, _mirror_key');
|
|
$client->shouldReceive('command')->once()->withArgs(static fn (string $sql): bool =>
|
|
str_contains($sql, '`flag` Nullable(UInt8)')
|
|
&& str_contains($sql, '`amount` Nullable(Decimal(20, 4))')
|
|
&& str_contains($sql, "`updated_at` Nullable(DateTime64(6, 'Asia/Shanghai'))")
|
|
&& str_contains($sql, 'ENGINE = ReplacingMergeTree(_mirror_version)')
|
|
&& str_contains($sql, 'PARTITION BY modulo(cityHash64(_mirror_key), 32)')
|
|
&& str_contains($sql, 'ORDER BY (_mirror_generation, _mirror_key)')
|
|
);
|
|
|
|
$columns = (new ClickHouseSchemaManager($source, $client, $placements))->ensure($dataset);
|
|
|
|
$this->assertCount(4, $columns);
|
|
}
|
|
|
|
public function test_columnar_schema_rejects_a_generation_unsafe_existing_sorting_key(): void
|
|
{
|
|
$dataset = $this->dataset();
|
|
$source = Mockery::mock(BusinessMirrorSchemaManager::class);
|
|
$source->shouldReceive('columns')->once()->andReturn([
|
|
['column_name' => 'Id', 'data_type' => 'int'],
|
|
]);
|
|
$placements = Mockery::mock(BusinessDataPlacement::class);
|
|
$placements->shouldReceive('targetStore')->once()->with('source.schema.fact')->andReturn('clickhouse');
|
|
$client = Mockery::mock(ClickHouseClient::class);
|
|
$client->shouldReceive('identifier')->once()->with('Id')->andReturn('`Id`');
|
|
$client->shouldReceive('qualifiedTable')->once()->with('fact_rows')->andReturn('`analytics`.`fact_rows`');
|
|
$client->shouldReceive('command')->once();
|
|
$client->shouldReceive('scalar')->once()->andReturn('_mirror_key');
|
|
|
|
$this->expectException(\RuntimeException::class);
|
|
$this->expectExceptionMessage('business_data_columnar_generation_key_mismatch');
|
|
|
|
(new ClickHouseSchemaManager($source, $client, $placements))->ensure($dataset);
|
|
}
|
|
|
|
private function dataset(): BusinessMirrorDataset
|
|
{
|
|
return new class implements BusinessMirrorDataset {
|
|
public function key(): string { return 'source.schema.fact'; }
|
|
public function sourceKey(): string { return 'source'; }
|
|
public function sourceSchema(): string { return 'schema'; }
|
|
public function sourceObject(): string { return 'fact'; }
|
|
public function targetTable(): string { return 'fact_rows'; }
|
|
public function keyColumns(): array { return ['Id']; }
|
|
public function watermarkColumn(): ?string { return 'updated_at'; }
|
|
public function fixedFilters(): array { return []; }
|
|
public function indexDefinitions(): array { return []; }
|
|
};
|
|
}
|
|
}
|