Repository: risingwavelabs/risingwave
File: src/frontend/src/catalog/system_catalog/rw_catalog/rw_fragment_backfill_progress.rs (Rust)

Code before:
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use risingwave_common::types::Fields;
use risingwave_frontend_macro::system_catalog;

/// Provides fragment level backfill progress.
// source backfill progress has the following schema:
// `{"num_consumed_rows": 530, "state": {"Backfilling": "529"}, "target_offset": "9999"}`
#[system_catalog(
view,
"rw_catalog.rw_fragment_backfill_progress",
"with
table_backfill_progress as (select
  progress.job_id,
  progress.fragment_id,
  scan_info.backfill_target_relation_id,
  case
    when scan_info.backfill_type = 'SNAPSHOT_BACKFILL' AND progress.min_epoch > scan_info.backfill_epoch
      then concat('100% (', stats.total_key_count, '/', stats.total_key_count, ')')
    when stats.total_key_count is null
      then '100% (0/0)'
  else
    concat(
      coalesce(progress.current_row_count::numeric / stats.total_key_count::numeric * 100, 0),
      '%',
      ' ',
      '(',
      coalesce(progress.current_row_count, 0),
      '/',
      stats.total_key_count,
      ')'
    )
  end as progress
FROM internal_backfill_progress() progress
JOIN rw_backfill_info scan_info ON progress.job_id = scan_info.job_id AND progress.fragment_id = scan_info.fragment_id
LEFT JOIN rw_table_stats stats ON scan_info.backfill_target_relation_id = stats.id
),
aggregated_source_backfill_progress as (
  select
    source_backfill_progress.job_id,
    source_backfill_progress.fragment_id,
    sum((backfill_progress -> 'num_consumed_rows')::int) as backfill_progress
  from
    internal_source_backfill_progress() source_backfill_progress
  group by
    source_backfill_progress.job_id,

Review comment (suggestion):
The CASE statement has a logic issue. When `stats.total_key_count` is NULL in the first condition (line 30-31), the THEN clause tries to concat with `stats.total_key_count`, which would result in NULL instead of the intended '100%' display.

The NULL check (lines 32-33) should come before the SNAPSHOT_BACKFILL check to prevent this issue. Consider reordering:

```sql
case
  when stats.total_key_count is null
    then '100% (0/0)'
  when scan_info.backfill_type = 'SNAPSHOT_BACKFILL' AND progress.min_epoch > scan_info.backfill_epoch
    then concat('100% (', stats.total_key_count, '/', stats.total_key_count, ')')
  else
    ...
end
```
```suggestion
    when stats.total_key_count is null
      then '100% (0/0)'
    when scan_info.backfill_type = 'SNAPSHOT_BACKFILL' AND progress.min_epoch > scan_info.backfill_epoch
      then concat('100% (', stats.total_key_count, '/', stats.total_key_count, ')')
```

Code after:
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use risingwave_common::types::Fields;
use risingwave_frontend_macro::system_catalog;

/// Provides fragment level backfill progress.
// source backfill progress has the following schema:
// `{"num_consumed_rows": 530, "state": {"Backfilling": "529"}, "target_offset": "9999"}`
#[system_catalog(
view,
"rw_catalog.rw_fragment_backfill_progress",
"with
table_backfill_progress as (select
  progress.job_id,
  progress.fragment_id,
  scan_info.backfill_target_relation_id,
  case
    when stats.total_key_count is null
      then '100% (0/0)'
    when scan_info.backfill_type = 'SNAPSHOT_BACKFILL' AND progress.min_epoch > scan_info.backfill_epoch
      then concat('100% (', stats.total_key_count, '/', stats.total_key_count, ')')
  else
    concat(
      coalesce(progress.current_row_count::numeric / stats.total_key_count::numeric * 100, 0),
      '%',
      ' ',
      '(',
      coalesce(progress.current_row_count, 0),
      '/',
      stats.total_key_count,
      ')'
    )
  end as progress
FROM internal_backfill_progress() progress
JOIN rw_backfill_info scan_info ON progress.job_id = scan_info.job_id AND progress.fragment_id = scan_info.fragment_id
LEFT JOIN rw_table_stats stats ON scan_info.backfill_target_relation_id = stats.id
),
aggregated_source_backfill_progress as (
  select
    source_backfill_progress.job_id,
    source_backfill_progress.fragment_id,
    sum((backfill_progress -> 'num_consumed_rows')::int) as backfill_progress
  from
    internal_source_backfill_progress() source_backfill_progress
  group by
    source_backfill_progress.job_id,