scalarResult = 987654; $repository = $this->repository($query); $this->assertSame(987654, $repository->headerHighWatermark()); $repository->headerRange(100, 200); $this->assertStringContainsString('MAX([Id])', $query->scalarCalls[0]['sql']); $this->assertStringContainsString('[dbo].[jst_inoutWater]', $query->scalarCalls[0]['sql']); $this->assertStringNotContainsString('[wms_co_id] = ?', $query->scalarCalls[0]['sql']); $this->assertSame([], $query->scalarCalls[0]['bindings']); $this->assertStringContainsString('[Id] > ?', $query->selectCalls[0]['sql']); $this->assertStringContainsString('[Id] <= ?', $query->selectCalls[0]['sql']); $this->assertStringNotContainsString('[wms_co_id] = ?', $query->selectCalls[0]['sql']); $this->assertStringContainsString('h.[wh_id]', $query->selectCalls[0]['sql']); $this->assertStringContainsString('[io_date]', $query->selectCalls[0]['sql']); $this->assertStringContainsString('AS occurred_at', $query->selectCalls[0]['sql']); $this->assertStringContainsString('NOT EXISTS', $query->selectCalls[0]['sql']); $this->assertStringContainsString('newer.[io_id] = h.[io_id]', $query->selectCalls[0]['sql']); $this->assertStringContainsString('newer.[Id] > h.[Id]', $query->selectCalls[0]['sql']); $this->assertStringContainsString('ORDER BY h.[Id]', $query->selectCalls[0]['sql']); $this->assertStringContainsString('h.[io_date] >= ?', $query->selectCalls[0]['sql']); $this->assertSame([100, 200, '2026-07-01'], $query->selectCalls[0]['bindings']); } public function test_items_are_parameterized_and_split_into_bounded_io_id_batches(): void { $query = new RecordingThirdPartyWarehouseQuery(); $query->selectResults = [[ ['source_id' => 2, 'io_id' => 2], ], [ ['source_id' => 1, 'io_id' => 501], ]]; $repository = $this->repository($query); $rows = $repository->itemsForIoIds(range(1, 501)); $this->assertCount(2, $query->selectCalls); $this->assertCount(500, $query->selectCalls[0]['bindings']); $this->assertCount(1, $query->selectCalls[1]['bindings']); $this->assertStringContainsString('[dbo].[jst_inoutWaterItem]', $query->selectCalls[0]['sql']); $this->assertStringContainsString('[qty_type]', $query->selectCalls[0]['sql']); $this->assertStringContainsString('[io_id] IN (', $query->selectCalls[0]['sql']); $this->assertStringNotContainsString('[io_id] IN (1,2', $query->selectCalls[0]['sql']); $this->assertSame([1, 2], array_column($rows, 'source_id')); } public function test_incremental_headers_use_a_bounded_modified_id_cursor_without_a_warehouse_filter(): void { $query = new RecordingThirdPartyWarehouseQuery(); $repository = $this->repository($query); $repository->modifiedHeadersBatch( '2026-09-09 03:20:00.000', 123, '2026-09-11 03:20:00.000', 99999, ); $call = $query->selectCalls[0]; $this->assertStringContainsString('SELECT TOP (5000)', $call['sql']); $this->assertStringContainsString('[modified] >= ?', $call['sql']); $this->assertStringContainsString('[modified] < ?', $call['sql']); $this->assertStringContainsString('[modified] = ? AND [Id] > ?', $call['sql']); $this->assertStringContainsString('ORDER BY [modified], [Id]', $call['sql']); $this->assertStringNotContainsString('[wms_co_id] = ?', $call['sql']); $this->assertSame([ '2026-09-09 03:20:00.000', '2026-09-11 03:20:00.000', '2026-09-09 03:20:00.000', '2026-09-09 03:20:00.000', 123, ], $call['bindings']); } public function test_changed_documents_reload_only_the_latest_header_in_bounded_batches(): void { $query = new RecordingThirdPartyWarehouseQuery(); $query->selectResults = [[ ['source_id' => 2, 'io_id' => 2], ], [ ['source_id' => 1, 'io_id' => 501], ]]; $repository = $this->repository($query); $rows = $repository->latestHeadersForIoIds(range(1, 501), '2026-09-11 03:20:00.000'); $this->assertCount(2, $query->selectCalls); $this->assertCount(502, $query->selectCalls[0]['bindings']); $this->assertCount(3, $query->selectCalls[1]['bindings']); $this->assertStringContainsString('MAX([Id]) AS latest_id', $query->selectCalls[0]['sql']); $this->assertStringContainsString('[io_id] IN (', $query->selectCalls[0]['sql']); $this->assertStringContainsString('[modified] < ?', $query->selectCalls[0]['sql']); $this->assertStringContainsString('ON latest.[latest_id] = h.[Id] WHERE h.[io_date] >= ?', $query->selectCalls[0]['sql']); $this->assertStringNotContainsString('[modified] < ? AND [io_date] >= ?', $query->selectCalls[0]['sql']); $this->assertStringNotContainsString('[wms_co_id] = ?', $query->selectCalls[0]['sql']); $this->assertSame('2026-09-11 03:20:00.000', $query->selectCalls[0]['bindings'][500]); $this->assertSame('2026-07-01', $query->selectCalls[0]['bindings'][501]); $this->assertSame([1, 2], array_column($rows, 'source_id')); $repository->latestHeadersForIoIds([], '2026-09-11 03:20:00.000'); $this->assertCount(2, $query->selectCalls); } public function test_inventory_uses_all_warehouse_keyset_pagination_and_a_bounded_limit(): void { $query = new RecordingThirdPartyWarehouseQuery(); $query->scalarResult = 191818; $repository = $this->repository($query); $this->assertSame(191818, $repository->inventoryHighWatermark()); $repository->inventoryBatch(12350, 191818, 99999); $this->assertStringContainsString('[dbo].[jst_inventoryInfo]', $query->scalarCalls[0]['sql']); $this->assertStringNotContainsString('[wms_co_id] = ?', $query->scalarCalls[0]['sql']); $this->assertSame([], $query->scalarCalls[0]['bindings']); $this->assertStringContainsString('SELECT TOP (5000)', $query->selectCalls[0]['sql']); $this->assertStringContainsString('[dbo].[jst_inventoryInfo]', $query->selectCalls[0]['sql']); $this->assertStringContainsString('[Id] > ?', $query->selectCalls[0]['sql']); $this->assertStringContainsString('[Id] <= ?', $query->selectCalls[0]['sql']); $this->assertStringNotContainsString('[wms_co_id] = ?', $query->selectCalls[0]['sql']); $this->assertStringContainsString('ORDER BY [Id]', $query->selectCalls[0]['sql']); $this->assertStringContainsString('AS source_modified_at', $query->selectCalls[0]['sql']); foreach (['return_qty', 'in_qty', 'defective_qty', 'customize_qty_1', 'customize_qty_2', 'customize_qty_3'] as $column) { $this->assertStringContainsString("[{$column}]", $query->selectCalls[0]['sql']); } $this->assertSame([12350, 191818], $query->selectCalls[0]['bindings']); $repository->inventoryBatch(191818, 191818, 2000); $this->assertCount(1, $query->selectCalls); } private function repository(UpstreamReadOnlyQuery $query): ThirdPartyWarehouseStorageUpstreamRepository { $factory = new class($query) extends UpstreamReadOnlyQueryFactory { public function __construct(private readonly UpstreamReadOnlyQuery $query) { } public function forSource(string $source): UpstreamReadOnlyQuery { TestCase::assertSame('jiansun', $source); return $this->query; } }; return new ThirdPartyWarehouseStorageUpstreamRepository($factory); } } class RecordingThirdPartyWarehouseQuery extends UpstreamReadOnlyQuery { public mixed $scalarResult = 0; /** @var list>> */ public array $selectResults = []; /** @var list */ public array $selectCalls = []; /** @var list */ public array $scalarCalls = []; public function __construct() { parent::__construct('jiansun'); } public function select(string $sql, array $bindings = []): array { $this->selectCalls[] = compact('sql', 'bindings'); return array_shift($this->selectResults) ?? []; } public function scalar(string $sql, array $bindings = []): mixed { $this->scalarCalls[] = compact('sql', 'bindings'); return $this->scalarResult; } }