BUG #19370: PG18 returns incorrect array slice results when slice bounds depend on another array expression

Поиск
Список
Период
Сортировка
От PG Bug reporting form
Тема BUG #19370: PG18 returns incorrect array slice results when slice bounds depend on another array expression
Дата
Msg-id 19370-7fb7a5854b7618f1@postgresql.org
обсуждение исходный текст
Ответы Re: BUG #19370: PG18 returns incorrect array slice results when slice bounds depend on another array expression
Список pgsql-bugs
The following bug has been logged on the website:

Bug reference:      19370
Logged by:          Zepeng Zhang
Email address:      redraiment@gmail.com
PostgreSQL version: 18.1
Operating system:   macOS 15.6.1 & Debian Linux 12
Description:

I have encountered a behavioral regression in PostgreSQL 18 related to array
slicing when the slice start index is computed from another array expression
in the same SELECT list.

The attached SQL query (included below in full for reproducibility)
implements a recursive Sudoku constraint-propagation algorithm purely in
SQL. The final SELECT computes several derived arrays from a base
`candidates bigint[]` array:

* `unsorted_candidates`: the original candidate array, with no defined
ordering
* `sorted_candidates`: the same candidates sorted in ascending numeric order
* `counts bigint[]`: for each Sudoku cell (in row-major order), the number
of candidates belonging to that cell

From these, two array slices are compared:

1. `sorted_candidates[2 + 1:]`
   This is a constant-expression slice that removes the first two elements
from `sorted_candidates`.
   The result is correct and identical in PostgreSQL 17 and PostgreSQL 18.

2. `sorted_candidates[counts[1] + 1:]`
   Since `counts[1] = 2` in this query, this slice is semantically
equivalent to the expression above and **should produce the same result**.

### Observed behavior

* **PostgreSQL 17**:
  Both expressions return identical results, as expected.

* **PostgreSQL 18**:
  The result of `sorted_candidates[counts[1] + 1:]` is incorrect.
  Instead of slicing `sorted_candidates`, PostgreSQL 18 appears to slice
`unsorted_candidates`, preserving the original (unsorted) order and merely
removing the first two elements of that unsorted array.

In other words, PostgreSQL 18 returns a slice that corresponds to:

```
unsorted_candidates[counts[1] + 1:]
```

even though the slice expression explicitly references `sorted_candidates`.

### Why this appears to be a bug

* The array being sliced (`sorted_candidates`) is explicitly named and
independently computed in the SELECT list.
* `counts[1]` is a scalar expression that evaluates deterministically to
`2`.
* Replacing `counts[1]` with the literal constant `2` restores the correct
behavior in PostgreSQL 18.
* The query relies only on documented SQL semantics; no undefined ordering
assumptions are made for `sorted_candidates`, as it is explicitly ordered
using `ORDER BY` inside `array(...)`.

This suggests that PostgreSQL 18 may be incorrectly reusing or misbinding
array expressions when evaluating array slices whose bounds depend on other
output expressions in the same SELECT list.

### Reproducibility

* Reproduced on:

  * macOS (Apple Silicon)
  * Debian Linux
* PostgreSQL versions tested:

  * PostgreSQL 17: correct behavior
  * PostgreSQL 18: incorrect behavior

The full SQL query used to reproduce the issue is included verbatim below.

```sql
with recursive coordinates as (
  select
    row * 10 + col + 1 as id,
    row + 1 as row,
    col + 1 as col,
    row / 3 * 3 + col / 3 + 1 as box
  from
    generate_series(0, 8) as rs(row),
    generate_series(0, 8) as cs(col)
), propagations as (
  select
    sources.id as source_id,
    targets.id as target_id
  from
    coordinates as sources
  inner join
    coordinates as targets
  on
    sources.row = targets.row
    or sources.col = targets.col
    or sources.box = targets.box
), puzzles as (
  select

'{26,59,63,75,84,91,111,129,137,155,164,234,246,261,279,328,345,416,441,467,472,569,613,649,651,674,692,793,836,888}'::bigint[]
as pendings,

'{11,12,13,14,15,16,17,18,19,31,32,33,34,35,36,37,38,39,41,42,43,44,45,46,47,48,49,141,142,143,144,145,146,147,148,149,171,172,173,174,175,176,177,178,179,181,182,183,184,185,186,187,188,189,191,192,193,194,195,196,197,198,199,211,212,213,214,215,216,217,218,219,221,222,223,224,225,226,227,228,229,251,252,253,254,255,256,257,258,259,281,282,283,284,285,286,287,288,289,291,292,293,294,295,296,297,298,299,311,312,313,314,315,316,317,318,319,331,332,333,334,335,336,337,338,339,351,352,353,354,355,356,357,358,359,361,362,363,364,365,366,367,368,369,371,372,373,374,375,376,377,378,379,381,382,383,384,385,386,387,388,389,391,392,393,394,395,396,397,398,399,421,422,423,424,425,426,427,428,429,431,432,433,434,435,436,437,438,439,451,452,453,454,455,456,457,458,459,481,482,483,484,485,486,487,488,489,491,492,493,494,495,496,497,498,499,511,512,513,514,515,516,517,518,519,521,522,523,524,525,526,527,528,529,531,532,533,534,535,536,537,538,539,541,542,543,544,545,546,547,548,549,551,552,553,554,555,556,557,558,559,571,572,573,574,575,576,577,578,579,581,582,583,584,585,586,587,588,589,591,592,593,594,595,596,597,598,599,621,622,623,624,625,626,627,628,629,631,632,633,634,635,636,637,638,639,661,662,663,664,665,666,667,668,669,681,682,683,684,685,686,687,688,689,711,712,713,714,715,716,717,718,719,721,722,723,724,725,726,727,728,729,731,732,733,734,735,736,737,738,739,741,742,743,744,745,746,747,748,749,751,752,753,754,755,756,757,758,759,761,762,763,764,765,766,767,768,769,771,772,773,774,775,776,777,778,779,781,782,783,784,785,786,787,788,789,811,812,813,814,815,816,817,818,819,821,822,823,824,825,826,827,828,829,841,842,843,844,845,846,847,848,849,851,852,853,854,855,856,857,858,859,861,862,863,864,865,866,867,868,869,871,872,873,874,875,876,877,878,879,891,892,893,894,895,896,897,898,899}'::bigint[]
as candidates

  union all

  select
    array(
      select
        min(candidate)
      from
        unnest(candidates) as _(candidate)
      group by
        candidate / 10
      having
        count(*) = 1
    ) as pendings,
    candidates
  from (
    select
      array(
        select unnest(candidates)

        except all

        select
          propagations.target_id * 10 + digit
        from (
          select
            pending / 10,
            pending % 10
          from
            unnest(pendings) as _(pending)
        ) as pendings(coordinate_id, digit)
        inner join
          propagations
        on
          pendings.coordinate_id = propagations.source_id

        except all

        select
          pending / 10 * 10 + id
        from
          unnest(pendings) as _0(pending),
          generate_series(1, 9) as _1(id)
      ) as candidates
    from
      puzzles
    where
      cardinality(pendings) > 0
  ) as _
), solutions as (
  select
    array(
      select
        count(*)
      from
        unnest(candidates) as _(candidate)
      group by
        candidate / 10
      order by
        candidate / 10
    ) as counts,
    array(
      select unnest(candidates) order by 1
    ) as sorted_candidates,
    candidates as unsorted_candidates
  from
    puzzles
  where
    cardinality(pendings) = 0
    and cardinality(candidates) > 0
)
select
  counts[1] as numbers,
  sorted_candidates,
  unsorted_candidates,
  sorted_candidates[2 + 1:] as expect_candidates,
  sorted_candidates[counts[1] + 1:] as actual_candidates
from
  solutions
```


В списке pgsql-bugs по дате отправления: