true, 'upstreams.sources.catalog_test' => [ 'label' => '测试上游库', 'config' => 'catalog_test', ], 'catalog_test.enabled' => true, ]); } public function test_complete_metadata_result_replaces_one_source_catalog(): void { DB::table('upstream_schema_columns')->insert($this->existingRow()); $query = Mockery::mock(UpstreamReadOnlyQuery::class); $query->shouldReceive('select')->once()->andReturn([ [ 'schema_name' => 'rpt', 'object_name' => 'finance_summary', 'object_type' => 'view', 'column_name' => 'company_code', 'ordinal_position' => 1, 'data_type' => 'nvarchar', 'max_length' => 50, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'NO', ], [ 'schema_name' => 'rpt', 'object_name' => 'finance_summary', 'object_type' => 'view', 'column_name' => 'amount', 'ordinal_position' => 2, 'data_type' => 'decimal', 'max_length' => null, 'numeric_precision' => 18, 'numeric_scale' => 2, 'is_nullable' => 'YES', ], ]); $factory = Mockery::mock(UpstreamReadOnlyQueryFactory::class); $factory->shouldReceive('forSource')->once()->with('catalog_test')->andReturn($query); $result = (new UpstreamSchemaCatalogSynchronizer( app(UpstreamSourceRegistry::class), $factory ))->sync('catalog_test'); $this->assertSame(1, $result['objects']); $this->assertSame(2, $result['columns']); $this->assertDatabaseMissing('upstream_schema_columns', ['column_name' => 'old_column']); $this->assertDatabaseHas('upstream_schema_columns', [ 'source_key' => 'catalog_test', 'schema_name' => 'rpt', 'object_name' => 'finance_summary', 'column_name' => 'amount', 'data_type' => 'decimal', 'numeric_precision' => 18, 'numeric_scale' => 2, 'is_nullable' => true, ]); } public function test_empty_metadata_result_keeps_previous_catalog(): void { DB::table('upstream_schema_columns')->insert($this->existingRow()); $query = Mockery::mock(UpstreamReadOnlyQuery::class); $query->shouldReceive('select')->once()->andReturn([]); $factory = Mockery::mock(UpstreamReadOnlyQueryFactory::class); $factory->shouldReceive('forSource')->once()->andReturn($query); try { (new UpstreamSchemaCatalogSynchronizer( app(UpstreamSourceRegistry::class), $factory ))->sync('catalog_test'); $this->fail('空目录不应覆盖原有数据字典。'); } catch (\RuntimeException) { $this->assertDatabaseHas('upstream_schema_columns', [ 'source_key' => 'catalog_test', 'column_name' => 'old_column', ]); } } public function test_duplicate_metadata_rows_do_not_abort_catalog_sync(): void { $duplicate = [ 'schema_name' => 'dbo', 'object_name' => 'tuihuo', 'object_type' => 'table', 'column_name' => '发货量', 'ordinal_position' => 3, 'data_type' => 'decimal', 'max_length' => null, 'numeric_precision' => 18, 'numeric_scale' => 2, 'is_nullable' => 'YES', ]; $query = Mockery::mock(UpstreamReadOnlyQuery::class); $query->shouldReceive('select')->once()->andReturn([ $duplicate, $duplicate, [ ...$duplicate, 'column_name' => '序号', 'ordinal_position' => 1, 'data_type' => 'int', 'numeric_precision' => 10, 'numeric_scale' => 0, ], ]); $factory = Mockery::mock(UpstreamReadOnlyQueryFactory::class); $factory->shouldReceive('forSource')->once()->with('catalog_test')->andReturn($query); $result = (new UpstreamSchemaCatalogSynchronizer( app(UpstreamSourceRegistry::class), $factory ))->sync('catalog_test'); $this->assertSame(1, $result['objects']); $this->assertSame(2, $result['columns']); $this->assertSame(1, DB::table('upstream_schema_columns') ->where('source_key', 'catalog_test') ->where('schema_name', 'dbo') ->where('object_name', 'tuihuo') ->where('column_name', '发货量') ->count()); } private function existingRow(): array { return [ 'source_key' => 'catalog_test', 'schema_name' => 'dbo', 'object_name' => 'old_table', 'object_type' => 'table', 'column_name' => 'old_column', 'ordinal_position' => 1, 'data_type' => 'int', 'max_length' => null, 'numeric_precision' => 10, 'numeric_scale' => 0, 'is_nullable' => false, 'synchronized_at' => now()->subDay(), 'created_at' => now()->subDay(), 'updated_at' => now()->subDay(), ]; } }