Skip to content

Timescale hypertable BM25 scans fail when parent/chunk indexed-column attnums drift #288

@creatorrr

Description

@creatorrr

Bug Description

On a TimescaleDB hypertable, parent-table BM25 queries can fail with:

ERROR:  tpquery index mismatch
HINT:  Query specifies index OID <parent_oid> but scan is on index "<chunk_index>" (OID <child_oid>)

The failure happens when the hypertable parent and its chunks have different physical attnum values for the indexed column, even though the column name is the same and the chunk inherits from the parent.

Direct chunk queries still work. Parent-table queries fail.

This appears to come from pg_textsearch matching parent/chunk BM25 indexes by raw indkey.values[0] / attnum rather than by column identity/name in the Timescale fallback paths.

Environment

  • PostgreSQL: 18.3
  • TimescaleDB: 2.25.2
  • pg_textsearch extension version: 0.6.1
  • pg_textsearch loaded library version: 0.6.1
  • OS: Ubuntu 22.04

Observed Catalog State

On the affected database:

-- Parent hypertable
SELECT attnum, attname, attisdropped
FROM pg_attribute
WHERE attrelid = 'public.episodes'::regclass
  AND attnum > 0
  AND (attisdropped OR attname IN ('search_metadata', 'trigram_text'))
ORDER BY attnum;

-- Result
14 | ........pg.dropped.14........ | t
15 | search_metadata              | f
16 | trigram_text                 | f
-- Child chunk
SELECT attnum, attname, attisdropped
FROM pg_attribute
WHERE attrelid = '_timescaledb_internal._hyper_1_20_chunk'::regclass
  AND attnum > 0
  AND (attisdropped OR attname IN ('search_metadata', 'trigram_text'))
ORDER BY attnum;

-- Result
14 | search_metadata | f
15 | trigram_text    | f
-- Parent BM25 index uses attnum 16
SELECT c.relname, ix.indkey::text
FROM pg_index ix
JOIN pg_class c ON c.oid = ix.indexrelid
WHERE c.relname = 'idx_episodes_trigram_text_bm25';

-- Result
idx_episodes_trigram_text_bm25 | 16
-- Chunk BM25 index uses attnum 15
SELECT c.relname, ix.indkey::text
FROM pg_index ix
JOIN pg_class c ON c.oid = ix.indexrelid
WHERE c.relname = '_hyper_1_20_chunk_idx_episodes_trigram_text_bm25';

-- Result
_hyper_1_20_chunk_idx_episodes_trigram_text_bm25 | 15

The chunk table still inherits from the parent table:

SELECT c.oid, c.relname, p.oid AS parent_oid, p.relname AS parent_relname
FROM pg_class c
LEFT JOIN pg_inherits i ON i.inhrelid = c.oid
LEFT JOIN pg_class p ON p.oid = i.inhparent
WHERE c.relname IN ('episodes', '_hyper_1_20_chunk')
ORDER BY c.oid;

Queries

This parent-table query fails:

SELECT id, trigram_text <@> 'memory store'
FROM public.episodes
WHERE store_id = 4
  AND created_at >= '2026-02-25 00:00:00+00'
  AND created_at <  '2026-02-26 00:00:00+00'
  AND COALESCE(trigram_text, '') <> ''
ORDER BY trigram_text <@> 'memory store' ASC
LIMIT 5;

EXPLAIN shows a chunk-local BM25 index scan:

Limit
  ->  Result
        ->  Index Scan using _hyper_1_20_chunk_idx_episodes_trigram_text_bm25 on _hyper_1_20_chunk
              Order By: (trigram_text <@> 'idx_episodes_trigram_text_bm25:memory store'::bm25query)
              Filter: ...

But the query errors with tpquery index mismatch.

The same query directly against the chunk table works and returns non-zero BM25 scores:

SELECT id, trigram_text <@> 'memory store'
FROM _timescaledb_internal._hyper_1_20_chunk
WHERE store_id = 4
  AND COALESCE(trigram_text, '') <> ''
ORDER BY trigram_text <@> 'memory store' ASC
LIMIT 5;

Relevant Upstream Code Paths

This looks like the attnum comparison in two places:

  • src/am/scan.c::indexes_match_by_attribute()
    • compares scan_attnum == query_attnum
  • src/types/query.c
    • child-index selection for a parent index also compares child_idx_form->indkey.values[0] != indexed_attnum

In the affected database, those checks fail even though the chunk index is the correct BM25 index for the same inherited column name.

Expected Behavior

For Timescale hypertables, if the child table inherits from the parent and both indexes are BM25 indexes on the same logical column name, the scan should be allowed even if raw physical attnum values differ.

Actual Behavior

Parent-table BM25 scans fail with tpquery index mismatch.

Notes

  • This does not reproduce on another environment with the same PostgreSQL / TimescaleDB / pg_textsearch versions where the parent and chunk attnum values still match.
  • So this does not look like a generic hypertable failure in 0.6.1; it looks like a schema-drift edge case that the current parent/chunk matching logic does not tolerate.
  • I do not have a clean SQL-only reproduction for creating the parent/chunk attnum drift on a fresh cluster. The drift appears to be the result of historical schema evolution on the hypertable. But once the drift exists, the failure is deterministic.

Suggested Fix

For the Timescale fallback paths, match parent/chunk BM25 indexes by:

  1. BM25 access method
  2. child table inherits from parent table
  3. indexed column name (or a more robust logical-column identity), not raw attnum

Using raw attnum seems too brittle for historical hypertables where parent and chunk physical layouts can diverge.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions