query()->scalar( 'SELECT COALESCE(MAX([Id]), 0) FROM [dbo].[jst_inoutWater]', ); } /** @return list> */ public function headerRange(int $afterId, int $throughId): array { if ($throughId <= $afterId) { return []; } return $this->query()->select(<<<'SQL' SELECT h.[Id] AS source_id, h.[io_id], h.[wms_co_id], h.[wh_id], h.[status], h.[type] AS io_type, CONVERT(varchar(27), h.[io_date], 126) AS occurred_at, CONVERT(varchar(27), h.[modified], 126) AS modified_at FROM [dbo].[jst_inoutWater] h WHERE h.[Id] > ? AND h.[Id] <= ? AND h.[io_date] >= ? AND NOT EXISTS ( SELECT 1 FROM [dbo].[jst_inoutWater] newer WHERE newer.[io_id] = h.[io_id] AND newer.[Id] > h.[Id] ) ORDER BY h.[Id] SQL, [$afterId, $throughId, self::START_DATE]); } /** @return list> */ public function modifiedHeadersBatch( string $cursorAt, int $cursorId, string $throughAt, int $limit, ): array { $limit = min(max($limit, 1), 5000); return $this->query()->select('SELECT TOP (' . $limit . ') ' . <<<'SQL' [Id] AS source_id, [io_id], [wms_co_id], [wh_id], [status], [type] AS io_type, CONVERT(varchar(27), [io_date], 126) AS occurred_at, CONVERT(varchar(27), [modified], 126) AS modified_at FROM [dbo].[jst_inoutWater] WHERE [modified] >= ? AND [modified] < ? AND ([modified] > ? OR ([modified] = ? AND [Id] > ?)) ORDER BY [modified], [Id] SQL, [$cursorAt, $throughAt, $cursorAt, $cursorAt, $cursorId]); } /** * Reload the current version for every changed document before replacing * its mirror rows. A document can have more than one historical header Id. * * @param list $ioIds * @return list> */ public function latestHeadersForIoIds(array $ioIds, string $throughAt): array { $ioIds = array_values(array_unique(array_filter( array_map(static fn (mixed $id): int => (int) $id, $ioIds), static fn (int $id): bool => $id > 0, ))); if ($ioIds === []) { return []; } $rows = []; foreach (array_chunk($ioIds, 500) as $chunk) { $placeholders = implode(',', array_fill(0, count($chunk), '?')); $batch = $this->query()->select( 'SELECT h.[Id] AS source_id, h.[io_id], h.[wms_co_id], h.[wh_id], h.[status], ' . 'h.[type] AS io_type, CONVERT(varchar(27), h.[io_date], 126) AS occurred_at, ' . 'CONVERT(varchar(27), h.[modified], 126) AS modified_at ' . 'FROM [dbo].[jst_inoutWater] h ' . 'INNER JOIN (SELECT [io_id], MAX([Id]) AS latest_id ' . 'FROM [dbo].[jst_inoutWater] ' . "WHERE [io_id] IN ({$placeholders}) AND [modified] < ? GROUP BY [io_id]) latest " . 'ON latest.[latest_id] = h.[Id] ' . 'WHERE h.[io_date] >= ? ' . 'ORDER BY h.[Id]', [...$chunk, $throughAt, self::START_DATE], ); array_push($rows, ...$batch); } usort($rows, static fn (array $left, array $right): int => ((int) ($left['source_id'] ?? 0)) <=> ((int) ($right['source_id'] ?? 0)) ); return $rows; } /** * The upstream item table has an index on io_id. Fetching items only after * the warehouse headers have been selected avoids scanning unrelated rows. * * @param list $ioIds * @return list> */ public function itemsForIoIds(array $ioIds): array { $ioIds = array_values(array_unique(array_filter( array_map(static fn (mixed $id): int => (int) $id, $ioIds), static fn (int $id): bool => $id > 0, ))); if ($ioIds === []) { return []; } $rows = []; foreach (array_chunk($ioIds, 500) as $chunk) { $placeholders = implode(',', array_fill(0, count($chunk), '?')); $batch = $this->query()->select( 'SELECT [Id] AS source_id, [io_id], [sku_id] AS sku_code, [qty], [qty_type] ' . 'FROM [dbo].[jst_inoutWaterItem] ' . "WHERE [io_id] IN ({$placeholders}) ORDER BY [Id]", $chunk, ); array_push($rows, ...$batch); } usort($rows, static fn (array $left, array $right): int => ((int) ($left['source_id'] ?? 0)) <=> ((int) ($right['source_id'] ?? 0)) ); return $rows; } public function inventoryHighWatermark(): int { return (int) $this->query()->scalar( 'SELECT COALESCE(MAX([Id]), 0) FROM [dbo].[jst_inventoryInfo]', ); } /** @return list> */ public function inventoryBatch(int $afterId, int $highWatermark, int $limit): array { if ($highWatermark <= $afterId) { return []; } $limit = min(max($limit, 1), 5000); return $this->query()->select('SELECT TOP (' . $limit . ') ' . <<<'SQL' [Id] AS source_id, [wms_co_id], [sku_id] AS sku_code, [qty], [return_qty], [in_qty], [defective_qty], [customize_qty_1], [customize_qty_2], [customize_qty_3], CONVERT(varchar(27), [modified], 126) AS source_modified_at FROM [dbo].[jst_inventoryInfo] WHERE [Id] > ? AND [Id] <= ? ORDER BY [Id] SQL, [$afterId, $highWatermark]); } private function query(): UpstreamReadOnlyQuery { return $this->query ??= $this->queries->forSource(self::SOURCE_KEY); } }